I'm struggling with putting a map on a second segment. It's a typical problem, and I've found several solutions, but they don't work for me because the events seem to get fired before the segment is build completely. I don't know if this is because of a breaking change in Ionic, or because I am doing it wrong.
page.html:
<ion-toolbar>
<ion-segment [(ngModel)]="view" (ionChange)="changeSegment()">
<ion-segment-button value="list">
<ion-icon name="list-box" isActive="false"></ion-icon>
</ion-segment-button>
<ion-segment-button value="map" (ionSelect)="selectMap()">
<ion-icon name="map" isActive="false"></ion-icon>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
<ion-content [ngSwitch]="view">
<div *ngSwitchCase="'list'"></div>
<div *ngSwitchCase="'map'">
<div id='map'>Here the map comes.</div>
</div>
</ion-content>
page.js:
import {Component} from "#angular/core";
#Component({
templateUrl: 'build/pages/page/page.html'
})
export class Timeline {
constructor() {
this.view = "list";
this.selectMap = function() {
console.log("selectMap:", document.getElementById("map"));
}
this.changeSegment = function() {
console.log("changeSegment:", document.getElementById("map"));
}
}
}
(Yeah, that project is using JavaScript, not TypeScript.)
With both the change and the select events, on first click they return "null" for the map element, and only on a second click they return the correct element.
To me it looks like the events are fired before the segment is complete. (But I may just as well be making a stupid beginners mistake...)
How do I know when the #map element is available?
Related
I have this component set up (stripped down to its minimum):
<a href={{href}}>{{text}}</a>
{{#if template}}
<button {{action "toggleSubmenu"}} class="global-nav-toggle-submenu">
<i class="caret-down"></i>
</button>
{{/if}}
And this test:
test('has a toggle button if a submenu is present', function(assert) {
var component = this.subject({
template: Ember.HTMLBars.compile('<ul class="global-nav-submenu"></ul>')
});
assert.ok(this.$().find('.global-nav-toggle-submenu').length);
});
This runs fine, but I get a deprecation notice from Ember:
Accessing 'template' in <app-name#component:global-nav-item::ember540> is deprecated. To determine if a block was specified to <app-name#component:global-nav-item::ember540> please use '{{#if hasBlock}}' in the components layout.
When I change the template to use hasBlock instead:
<a href={{href}}>{{text}}</a>
{{#if hasBlock}}
<button {{action "toggleSubmenu"}} class="global-nav-toggle-submenu">
<i class="caret-down"></i>
</button>
{{/if}}
The test fails. Logging this.$() to the console shows that the hbs template file seems to be ignoring the template I'm adding programmatically.
In the test, I've tried swapping out template with block, layout, hasBlock, content, etc., with no success. I've tried initializing the subject with hasBlock: true as well.
And the logic runs fine when loading a page in the regular development app that has a block applied:
{{#global-nav-item text="Hello" href="#"}}
Some Content
{{/global-nav-item}}
Is there some other way that I should be setting up my components in unit tests in order to test this logic?
In general you should use the "new" style component integration tests for this type of thing.
See the following resources:
Example from the ember-radio-button addon
Nice blog post - Ember component integration tests
Update: Based on the blog post linked from Robert's answer, here is the new test code in tests/integration/components/global-nav-item-test.js:
import hbs from 'htmlbars-inline-precompile';
import { moduleForComponent, test } from 'ember-qunit';
moduleForComponent('global-nav-item', 'Integration | Component | global-nav-item', {
integration: true
});
test('has a toggle button if a submenu is present', function(assert) {
this.render(hbs`
{{#global-nav-item text="Hello" href="/"}}
<ul class="le-global-nav-submenu"></ul>
{{/global-nav-item}}
`);
assert.ok(this.$('.global-nav-toggle-submenu').length);
});
I'm learning EmberJS, I tried to search the docs and stuff but I couldn't get it right so far I've implemented the component and a action to respond on click event, for now it just print something in the console. I want to get the clicked element so I could change it's style and attributes. I'm using the scaffold generated by ember-cli version 0.2.7 Follows the code:
app/components/heart-like.js
import Ember from 'ember';
export default Ember.Component.extend({
actions:{
click: function (event) {
console.log(event); // undefined
console.log("Hello from component");
}
}
});
app/templates/components/heart-like.hbs
<div class="row">
<span {{action "click"}} class="pull-right" style="color: #B71C1C;"><i class="fa fa-2x fa-heart"></i></span>
</div>
Ember.EventDispatcher delegates some events to the corresponding Ember.View. Since Ember.Component is a subclass of Ember.View, you can capture click events just exposing a click function:
export default Ember.Component.extend({
click: function (event) {
console.log(event.target); // displays the clicked element
console.log("Hello from component");
}
});
Keep in mind that using directly these event hooks isn't a good practice because you're dealing directly with the DOM, and not expressing your intention. Depending of your use case it's a better alternative to just create a property in the component and change it via actions. For instance, to change the color of the icon when it's clicked:
component
export default Ember.Component.extend({
color: 'red',
actions:{
changeColor: function() {
this.set('color', 'black');
}
}
});
template
<div class="row">
<span {{action "changeColor"}} class="pull-right" style="color: {{color}};"><i class="fa fa-2x fa-heart"></i></span>
</div>
Live demo
I want to add tooltips onto a button in a component that can appear based on a set of results back from the server. (i.e. action buttons for delete, edit etc.)
I have created a “search” component that is rendering into the application and when a search button is clicked the server may return a number of rows into that same search component template.
so for example:
My-app/pods/factual-data/template.hbs
Contains:
…
{{#if results}}
<div class="row">
<div class="col-sm-3"><b>Factual ID</b></div>
<div class="col-sm-2"><b>Name</b></div>
<div class="col-sm-2"><b>Town</b></div>
<div class="col-sm-2"><b>Post Code</b></div>
<div class="col-sm-2"><b>Actions</b></div>
</div>
{{/if}}
{{#each result in results}}
<div class="row">
<div class="col-sm-3">{{result.factual_id}}</div>
<div class="col-sm-2">{{result.name}}</div>
<div class="col-sm-2">{{result.locality}}</div>
<div class="col-sm-2">{{result.postcode}}</div>
<div class="col-sm-2">
<button {{action "clearFromFactual" result.factual_id}} class="btn btn-danger btn-cons tip" type="button" data-toggle="tooltip" class="btn btn-white tip" type="button" data-original-title="Empty this Row<br> on Factual" ><i class="fa fa-check"></i></button>
</div>
</div>
{{/each}}
…
However I cannot get the tooltip code to function, due to an element insert detection/timing issue..
In the component
My-app/pods/factual-data/component.js
Contains:
...
didInsertElement : function(){
console.log("COMPONENT: didInsertElement");
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
this.enableToolTips();
},enableToolTips: function() {
var $el = Ember.$('.tip');
console.log("TOOLTIP:", $el);
if($el.length > 0) {
$el.tooltip({
html:true,
delay: { show: 250, hide: 750 }
});
}
}
...
However it seems didInsertElement is only run when the component is first rendered, is there a different function that is called everytime something in the DOM is changed within a component?
I did try to use observes: i.e.
…
enableToolTips: function() {
var $el = Ember.$('.tip');
console.log("TOOLTIP:", $el);
if($el.length > 0) {
$el.tooltip({
html:true,
delay: { show: 250, hide: 750 }
});
}
}.observes('results')
…
Which does trigger when the results variable is changed however it is still triggering before the content is actually rendered. I am assuming this because is I manually run in the console Ember.$('.tip').tooltip() (after the button is displayed) then the tooltips work ok.
Any pointers on this issue?
Try
enableToolTips: function() {
Ember.run.scheduleOnce('afterRender', this, function() {
var $el = Ember.$('.tip');
console.log("TOOLTIP:", $el);
if($el.length > 0) {
$el.tooltip({
html:true,
delay: { show: 250, hide: 750 }
});
}
});
}.observes('results')
Checking Ember.Component API there are two hooks that can do that
willClearRender : When component html is about to change.
willInsertElement : When old html is cleared and new one is going to be placed.
But you need to have a look on scheduleOnce.
Its worth noting that didInsertElement runs every time. But when it runs view was not updated. To solve that you need to run your code inside a Run Loop like this
didInsertElement : function(){
var self = this;
Ember.run.scheduleOnce('afterRender', this, function(){
//run tool tip here
self.$().find(".tip").tooltip({
});
});
}
I have this wrapper around Ember.Select, to activate Select2 features:
App.Select2SelectView = Ember.Select.extend({
prompt: 'Please select...',
classNames: ['input-xlarge'],
didInsertElement: function() {
Ember.run.scheduleOnce('afterRender', this, 'processChildElements');
},
processChildElements: function() {
this.$().select2({
// do here any configuration of the
// select2 component
escapeMarkup: function (m) { return m; } // we do not want to escape markup since we are displaying html in results
});
},
willDestroyElement: function () {
this.$().select2('destroy');
}
});
Sometimes I need to make a drop-down invisible, and I do it like this:
{{#if cityVisible}}
<div class="control-group">
<label class="control-label">City</label>
<div class="controls">
{{view SettingsApp.Select2SelectView
id="city-id"
contentBinding="currentCities"
optionValuePath="content.city"
optionLabelPath="content.city"
selectionBinding="controller.selectedCity"
prompt="Select a city"}}
<i class="help-block">Select the city for your geographical number</i>
</div>
</div>
{{/if}}
But whenever the drop-down is invisible, I get the following error:
Uncaught TypeError: Cannot call method 'select2' of undefined
I guess the element is inserted, but then removed by Ember from the DOM (bound property cityVisible), so that jQuery is not able to find it?
What can I do to avoid that error message? I do not want to make the view visible/invisible, I want to keep the whole control-group under the cityVisible control.
This is normal behaviuor that ember removes the view, as a workaround you could do the following:
HTML
<div {{bindAttr class="view.cityVisible::hideCities"}}>
<div class="control-group">
...
</div>
</div>
CSS
.hideCities {
display: none;
}
Remove the {{#if}} around the html block, and wrap it with a div instead on which you set a css class which contains display: none; you could use the cityVisible or a different property in your view or controller and set it to true/false to toggle it's visibility. This mecanisnm should leave your html markup in the DOM an thus available for jQuery. Note that if your citiesVisible property lives in your controller then remove the view. prefix from view.citiesVisible to be only citiesVisible, this depends on your setup.
See demo here.
Hope it helps.
There are numerous questions that ask in one way or another: "How do I do something after some part of a view is rendered?" (here, here, and here just to give a few). The answer is usually:
use didInsertElement to run code when a view is initially rendered.
use Ember.run.next(...) to run your code after the view changes are flushed, if you need to access the DOM elements that are created.
use an observer on isLoaded or a similar property to do something after the data you need is loaded.
What's irritating about this is, it leads to some very clumsy looking things like this:
didInsertElement: function(){
content.on('didLoad', function(){
Ember.run.next(function(){
// now finally do my stuff
});
});
}
And that doesn't really even necessarily work when you're using ember-data because isLoaded may already be true (if the record has already been loaded before and is not requested again from the server). So getting the sequencing right is hard.
On top of that, you're probably already watching isLoaded in your view template like so:
{{#if content.isLoaded}}
<input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
<div>Loading data...</div>
{{/if}}
and doing it again in your controller seems like duplication.
I came up with a slightly novel solution, but it either needs work or is actually a bad idea...either case could be true:
I wrote a small Handlebars helper called {{fire}} that will fire an event with a custom name when the containing handlebars template is executed (i.e. that should be every time the subview is re-rendered, right?).
Here is my very early attempt:
Ember.Handlebars.registerHelper('fire', function (evtName, options) {
if (typeof this[evtName] == 'function') {
var context = this;
Ember.run.next(function () {
context[evtName].apply(context, options);
});
}
});
which is used like so:
{{#if content.isLoaded}}
{{fire typeaheadHostDidRender}}
<input type="text" id="myTypeahead" data-provide="typeahead">
{{else}}
<div>Loading data...</div>
{{/if}}
This essentially works as is, but it has a couple of flaws I know of already:
It calls the method on the controller...it would probably be better to at least be able to send the "event" to the ancestor view object instead, perhaps even to make that the default behavior. I tried {{fire typeaheadHostDidRender target="view"}} and that didn't work. I can't see yet how to get the "current" view from what gets passed into the helper, but obviously the {{view}} helper can do it.
I'm guessing there is a more formal way to trigger a custom event than what I'm doing here, but I haven't learned that yet. jQuery's .trigger() doesn't seem to work on controller objects, though it may work on views. Is there an "Ember" way to do this?
There could be things I don't understand, like a case where this event would be triggered but the view wasn't in fact going to be added to the DOM...?
As you might be able to guess, I'm using Bootstrap's Typeahead control, and I need to wire it after the <input> is rendered, which actually only happens after several nested {{#if}} blocks evaluate to true in my template. I also use jqPlot, so I run into the need for this pattern a lot. This seems like a viable and useful tool, but it could be I'm missing something big picture that makes this approach dumb. Or maybe there's another way to do this that hasn't shown up in my searches?
Can someone either improve this approach for me or tell me why it's a bad idea?
UPDATE
I've figured a few of the bits out:
I can get the first "real" containing view with options.data.view.get('parentView')...obvious perhaps, but I didn't think it would be that simple.
You actually can do a jQuery-style obj.trigger(evtName) on any arbitrary object...but the object must extend the Ember.Evented mixin! So that I suppose is the correct way to do this kind of event sending in Ember. Just make sure the intended target extends Ember.Evented (views already do).
Here's the improved version so far:
Ember.Handlebars.registerHelper('fire', function (evtName, options) {
var view = options.data.view;
if (view.get('parentView')) view = view.get('parentView');
var context = this;
var target = null;
if (typeof view[evtName] == 'function') {
target = view;
} else if (typeof context[evtName] == 'function') {
target = context;
} else if (view.get('controller') && typeof view.get('controller')[evtName] == 'function') {
target = view.get('controller');
}
if (target) {
Ember.run.next(function () {
target.trigger(evtName);
});
}
});
Now just about all I'm missing is figuring out how to pass in the intended target (e.g. the controller or view--the above code tries to guess). Or, figuring out if there's some unexpected behavior that breaks the whole concept.
Any other input?
UPDATED
Updated for Ember 1.0 final, I'm currently using this code on Ember 1.3.1.
Okay, I think I got it all figured out. Here's the "complete" handlebars helper:
Ember.Handlebars.registerHelper('trigger', function (evtName, options) {
// See http://stackoverflow.com/questions/13760733/ember-js-using-a-handlebars-helper-to-detect-that-a-subview-has-rendered
// for known flaws with this approach
var options = arguments[arguments.length - 1],
hash = options.hash,
hbview = options.data.view,
concreteView, target, controller, link;
concreteView = hbview.get('concreteView');
if (hash.target) {
target = Ember.Handlebars.get(this, hash.target, options);
} else {
target = concreteView;
}
Ember.run.next(function () {
var newElements;
if(hbview.morph){
newElements = $('#' + hbview.morph.start).nextUntil('#' + hbview.morph.end)
} else {
newElements = $('#' + hbview.get('elementId')).children();
}
target.trigger(evtName, concreteView, newElements);
});
});
I changed the name from {{fire}} to {{trigger}} to more closely match Ember.Evented/jQuery convention. This updated code is based on the built-in Ember {{action}} helper, and should be able to accept any target="..." argument in your template, just as {{action}} does. Where it differs from {{action}} is (besides firing automatically when the template section is rendered):
Sends the event to the view by default. Sending to the route or controller by default wouldn't make as much sense, as this should probably primarily be used for view-centric actions (though I often use it to send events to a controller).
Uses Ember.Evented style events, so for sending an event to an arbitrary non-view object (including a controller) the object must extend Ember.Evented, and must have a listener registered. (To be clear, it does not call something in the actions: {…} hash!)
Note that if you send an event to an instance of Ember.View, all you have to do is implement a method by the same name (see docs, code). But if your target is not a view (e.g. a controller) you must register a listener on the object with obj.on('evtName', function(evt){...}) or the Function.prototype.on extension.
So here's a real-world example. I have a view with the following template, using Ember and Bootstrap:
<script data-template-name="reportPicker" type="text/x-handlebars">
<div id="reportPickerModal" class="modal show fade">
<div class="modal-header">
<button type="button" class="close" data-dissmis="modal" aria-hidden="true">×</button>
<h3>Add Metric</h3>
</div>
<div class="modal-body">
<div class="modal-body">
<form>
<label>Report Type</label>
{{view Ember.Select
viewName="selectReport"
contentBinding="reportTypes"
selectionBinding="reportType"
prompt="Select"
}}
{{#if reportType}}
<label>Subject Type</label>
{{#unless subjectType}}
{{view Ember.Select
viewName="selectSubjectType"
contentBinding="subjectTypes"
selectionBinding="subjectType"
prompt="Select"
}}
{{else}}
<button class="btn btn-small" {{action clearSubjectType target="controller"}}>{{subjectType}} <i class="icon-remove"></i></button>
<label>{{subjectType}}</label>
{{#if subjects.isUpdating}}
<div class="progress progress-striped active">
<div class="bar" style="width: 100%;">Loading subjects...</div>
</div>
{{else}}
{{#if subject}}
<button class="btn btn-small" {{action clearSubject target="controller"}}>{{subject.label}} <i class="icon-remove"></i></button>
{{else}}
{{trigger didRenderSubjectPicker}}
<input id="subjectPicker" type="text" data-provide="typeahead">
{{/if}}
{{/if}}
{{/unless}}
{{/if}}
</form>
</div>
</div>
<div class="modal-footer">
Cancel
Add
</div>
</div>
</script>
I needed to know when this element was available in the DOM, so I could attach a typeahead to it:
<input id="subjectPicker" type="text" data-provide="typeahead">
So, I put a {{trigger}} helper in the same block:
{{#if subject}}
<button class="btn btn-small" {{action clearSubject target="controller"}}>{{subject.label}} <i class="icon-remove"></i></button>
{{else}}
{{trigger didRenderSubjectPicker}}
<input id="subjectPicker" type="text" data-provide="typeahead">
{{/if}}
And then implemented didRenderSubjectPicker in my view class:
App.ReportPickerView = Ember.View.extend({
templateName: 'reportPicker',
didInsertElement: function () {
this.get('controller').viewDidLoad(this);
}
,
didRenderSubjectPicker: function () {
this.get('controller').wireTypeahead();
$('#subjectPicker').focus();
}
});
Done! Now the typeahead gets wired when (and only when) the sub-section of the template is finally rendered. Note the difference in utility, didInsertElement is used when the main (or perhaps "concrete" is the proper term) view is rendered, while didRenderSubjectPicker is run when the sub-section of the view is rendered.
If I wanted to send the event directly to the controller instead, I'd just change the template to read:
{{trigger didRenderSubjectPicker target=controller}}
and do this in my controller:
App.ReportPickerController = Ember.ArrayController.extend({
wireTypeahead: function(){
// I can access the rendered DOM elements here
}.on("didRenderSubjectPicker")
});
Done!
The one caveat is that this may happen again when the view sub-section is already on screen (for example if a parent view is re-rendered). But in my case, running the typeahead initialization again is fine anyway, and it would be pretty easy to detect and code around if need be. And this behavior may be desired in some cases.
I'm releasing this code as public domain, no warranty given or liability accepted whatsoever. If you want to use this, or the Ember folks want to include it in the baseline, go right ahead! (Personally I think that would be a great idea, but that's not surprising.)