Observe/Detect Change in Ember Component {{yield}} Block - ember.js

Is there any way for a component to listen to, or observe, changes in yielded content?
I have a component which serves as an isotope.js wrapper, and would like to be able to call some necessary clean-up isotope methods (such as .isotope('layout')) in case wrapped content changes (e.g. through filtering).
I've been able to do something similar with a View by observing controller properties, but would like to keep things less coupled if possible.

Well, I don't know exactly what it is you want to do nor have I used isotope.js. But here's what I can tell you. The {{yield}} helper calls this function:
_yield: function(context, options, morph, blockArguments) {
var view = options.data.view;
var parentView = this._parentView;
var template = get(this, 'template');
if (template) {
Ember.assert("A Component must have a parent view in order to yield.", parentView);
view.appendChild(Ember.View, {
isVirtual: true,
tagName: '',
template: template,
_blockArguments: blockArguments,
_contextView: parentView,
_morph: morph,
context: get(parentView, 'context'),
controller: get(parentView, 'controller')
//expose parent to children components?
});
}
}
Which means in theory you could expose a handle to your child components to set properties on the parent component. This clearly couples the two components. You can also make both the children and the parent take the "wrapped content" as attributes so that you can observe and manipulate in both places. I have used both approaches to great success. Which I used is dictated by context

Related

How do I call a controller function from a template in Ember?

Let's say I have a template which iterates over a collection of items, and I want to call a function with each item which is specific to the controller, and not a model-level concern:
{{#each people as |person|}}
icon name: {{findIconFor(person)}}
{{/each}}
I'd like to define findIconFor in the controller, because this is something specific to this particular view.
export default Ember.Controller.extend({
findIconFor: function(person) {
// figure out which icon to use
}
);
But that doesn't work. The template fails to compile. Parse error: Expecting 'STRING', 'NUMBER', 'ID', 'DATA', got 'INVALID'
What is the "ember way" to do this?
As i spent almost entire day on a similar problem here is my solution.
Because Ember for some reason just doesn't allow you to run a controller functions directly from the template (which is ridiculous and ties your hands in some very stupid ways and i don't know who on earth decided this is a good idea ...) the thing that makes most sense to me is to create an universal custom helper, that allows you to run functions from the template :) The catch here is that you should always pass the current scope (the "this" variable) to that helper.
So the helper could be something like this:
export default Ember.Helper.helper(function([scope, fn]) {
let args = arguments[0].slice(2);
let res = fn.apply(scope, args);
return res;
});
Then, you can make a function inside your controller, that you want to run, for example:
testFn: function(element){
return element.get('name');
}
and then in your template you just call it with the custom helper:
{{#each items as |element|}}
{{{custom-helper this testFn element}}}
{{/each}}
The first two arguments to the helper should always be "this" and the name of the function, that you want to run, and then you can pass as many extra arguments as you wish.
Edit: Anyway, every time when you think you need to do this, you should think if it will not be better to create a new component instead (it will be in 90% of the cases)
I'd use a computed property in the controller:
iconPeople: Ember.computed('people.#each', function(){
var that = this;
return this.get('people').map(function(person){
return {
'person': person,
'icon': that.findIconFor(person)
};
});
})
Now you could get the icon from {{person.icon}} and the name from {{person.person.name}}. You might want to improve on that (and the code is untested), but that's the general idea.
If the icon is something associated with a person, then since the person is represented by a model, it is best to implement it as a computed property on the person model. What is your intent in trying to put it into the controller?
// person.js
export default DS.Model.extend({
icon: function() { return "person-icon-" + this.get('name'); }.property('name')
..
};
Then assuming that people is an array of person:
{{#each people as |person|}}
icon name: {{person.icon}}
{{/each}}
The alternative provided by #jnfingerle works (I assume you figured out that he is proposing that you loop over iconPeople), but it seems like a lot of extra work to go to to create a new array containing objects. Does the icon depend on anything known only to the controller? If not, as I said, why should the logic to compute it be in the controller?
Where to put things is a a matter of philosophy and preference. Some people like bare-bones models that contain nothing more than fields coming down from the server; other people compute state and intermediate results in the model. Some people puts lots of stuff in controllers, whereas others prefer light-weight controllers with more logic in "services". Personally, I'm on the side of heavier models, lighter controllers, and services. I'm not claiming that business logic, or heavy data transformations, or view preparations should go in the model, of course. But remember, the model represents an object. If there's some interesting facet to the object, whether it come down from the server or be computed somehow, to me it makes a lot of sense to put that in the model.
Remember also that controllers are part of a tightly-coupled route/controller/view nexus. If there's some model-specific thing that you compute in one controller, you might have to then add it to some other controller that happens to be handling the same model. Then before you know it you're writing controller mixins that share logic across controllers that shouldn't have been in them in the first place.
Anyway, you say your icon comes from an "unrelated data store". That sounds asynchronous. To me, that hints that maybe it's a sub-model called PersonIcon which is a belongsTo in the person model. You can make that work with the right mix of adapters and serializers for that model. The nice thing about that approach is that all the asynchronicity in retrieving the icon is going to be handled semi-magically, either when the person model is created, or when you actually need the icon (if you say async: true).
But perhaps you're not using Ember Data, or don't want to go to all that trouble. In that case, you could consider adorning the person with the icon in the route's model hook, making use of Ember's ability to handle asynchronous model resolution, by doing something like the following:
model: function() {
return this.store.find('person') .
then(function(people) {
return Ember.RSVP.Promise.all(people.map(getIcon)) .
then(function(icons) {
people.forEach(function(person, i) {
person.set('icon') = icons[i];
});
return people;
})
;
})
;
}
where getIcon is something like
function getIcon(person) {
return new Ember.RSVP.Promise(function(resolve, reject) {
$.ajax('http://icon-maker.com?' + person.get('name'), resolve);
});
}
Or, if it is cleaner, you could break the icon stuff out into an afterModel hook:
model: function() { return this.store.find('person'); },
afterModel: function(model) {
return Ember.RSVP.Promise.all(model.map(getIcon)) .
then(function(icons) {
model.forEach(function(person, i) {
person.set('icon') = icons[i];
});
})
;
}
Now Ember will wait for the entire promise to resolve, including getting the people and their icons and sticking the icons on the people, before proceeding.
HTH.

In Ember.js how to notify an ArrayController's corresponding itemController, when a property on the ArrayController changes

I have an EmailsController (ArrayController), which stores all the emails. I have an EmailController (ObjectController) that has a parameter that stores if the actual Email is selected or not. I am trying to implement a button in the emails template, that selects or deselects all the Emails. So somehow I need to notify the EmailController via an action of the EmailsController and change the EmailController's isChecked parameter.
I am trying to use the itemController, the needs, and the controllerBinding parameters, but nothing works.
Here are the controllers:
App.EmailsController = Ember.ArrayController.extend({
needs: ["Email"],
itemController: 'Email',
checkAll: true,
actions: {
checkAllEmails: function() {
this.toggleProperty("checkAll");
console.log(this.get("checkAll"));
}
}
});
App.EmailController = Ember.ObjectController.extend({
needs: ["Emails"],
controllerBinding: 'controllers.Emails',
isChecked: true,
checkAllChanged: function() {
//this should execute, but currently it does not
this.set("isChecked",this.get('controller.checkAll'));
}.property("controller")
});
Here is the corresponding jsFiddle: http://jsfiddle.net/JqZK2/4/
The goal would be to toggle the selection of the checkboxes via the Check All button.
Thanks!
Your mixing a few different mechanisms and your using a few wrong conventions. It's not always easy to find this stuff though, so don't fret.
Referencing Controllers
Even though controllers are created with an Uppercase format, the are stored in the lowercase format and your needs property should be:
needs: ['emails'],
You then access other controllers through the controllers property:
this.get('controllers.emails.checkAll')
Computed Properties
Computed properties can be used as a getter/setter for a variable and also as a way to alias other properties. For example, if you wanted the isChecked property on the Email controller to be directly linked to the value of the checkAll property of the Emails controller, you could do this:
isChecked: function() {
return this.get('controllers.emails.checkAll');
}.property('controllers.emails.checkAll')
Although computed properties can do much more, this basic form is really just a computed alias, and there is a utility function to make it easier:
isChecked: Ember.computed.alias('controllers.emails.checkAll')
Observables
An observable basically creates a method that will be called when the value it observes changes. A computed alias would cause all items to uncheck or check whenever you clicked on any one of them, since their isChecked property is linked directly to the checkAll property of the parent controller. Instead of your checkAllChanged method identifying as a property it should use observes:
checkAllChanged: function() {
this.set("isChecked",this.get('controllers.emails.checkAll'));
}.observes("controllers.emails.checkAll")
This way when the checkAll property changes on the parent controller, this method updates the isChecked properties of all items to its value, but if you uncheck or check an individual item, it doesn't affect the other items.
Bindings
Bindings are somewhat deprecated; from reading issues on the Ember github repository I believe the creators of Ember seem to favor using computed properties, aliases, and observables instead. That is not to say they don't work and if your goal was to avoid having to type out controllers.emails every time, you could create one like you did (I wouldn't call it controller though, cause thats really quite ambiguous):
emailsBinding: 'controllers.emails'
Using a computed alias instead:
emails: Ember.computed.alias('controllers.emails')
You could then change your observer to:
checkAllChanged: function() {
this.set("isChecked",this.get('emails.checkAll'));
}.observes("emails.checkAll")
Heres an updated version of your jsFiddle: http://jsfiddle.net/tMuQn/
You could just iterate through the emails, changing their properties from the parent controller. You don't need to specify needs or observe a variable.
App.EmailsController = Ember.ArrayController.extend({
itemController: 'email',
actions: {
checkAllEmails: function() {
this.forEach(function(email) {
email.toggleProperty("isChecked");
});
}
}
});
Also, you typically don't set initial values like you did with isChecked = true; I believe that's creating a static shared property on the prototype (not what you intended). Instead, set the property on init, or pass it in from your original json data.
See the code: http://jsfiddle.net/JqZK2/5/

How to access parent view and controller from ember eventManager

The ember way:
According to ember's documentation about views' eventManagers, they must be created in the parent classes definition like so:
AView = Ember.View.extend({
eventManager: Ember.Object.create({
which encapsulates and isolates them from their parent view (AView).
The only way of accessing the context of events is through the view parameter that gets passed in along with each event
dragEnter: function(event, view) {
My situation:
I'm doing a lot of work with the various drag events inside a large view with many subviews, inputs, checkboxes, etc.
Following this form, my code is beginning to go to great lengths to determine which sub-view each event originated from, and then taking different paths to access the common parent controller:
drop: function(event, view) {
var myController;
if(view.$().hasClass('is-selected') ||
view.$().hasClass('list-map-container')) {
myController = view.get('controller.controllers.myController');
} else if(view.$().hasClass('ember-text-field')) {
myController = view.get('parentView.parentView.controller');
} else {
myController = view.get('controller');
}
// do work with myController
}
My hack:
In order to simplify I used the didInsertElement hook in the parent view to assign the desired controller as a property on the eventManager:
App.MyView = Ember.View.extend({
didInsertElement: function() {
this.set('eventManager.controller', this.get('controller'));
},
eventManager: Ember.Object.create({
controller: null,
// ...
This works to significantly simplify my event handlers:
drop: function(event, view) {
var myController = this.get('controller');
// do work with myController
My question:
My intuition tells me this hack-around isn't the best solution.
Perhaps I shouldn't be doing all the work in the eventManager? Rather move all this work to a controller and just forward the events from the view?
But if the eventManager is an acceptable workspace, then what is the best way to access the parent view's controller?
I know this is a late answer but this SO question appears as a result of google. Here is how I did this when searching through emberjs examples.
To access the view within the eventManager, you have to specify two argument in the event function handler :
eventManager: Ember.Object.create({
keyUp: function(event, view){
view = view.get('parentView'); // The view parameter might not be the current view but the emberjs internal input view.
view.get('controller'); // <-- controller
}
}),
Correct me if I'm wrong, but it looks like all the controller logic is encapsulated to a text-field--if so, I think a component might better suited for this use case. It's essentially a controller and view as one, and the eventManager's callbacks' view parameter gives you control over the component/controller itself.
If you need access to the component's parent controller, you might want to bind to events on the component from the parent controller, because the component really shouldn't know about anything outside its scope.

Handlebars template with multiple states

I'm trying to write an Ember view that has three states. Specifically, a submit button that transitions from "Submit" to "Saving..." to "Finished!" There are many ways to accomplish this goal, but I was wondering what the "best practice" would be from an Ember standpoint to accomplish this without writing crappy code.
Currently I have the following code:
UiControls.SubmitButton = Ember.View.extend({
template: function() {
var template = '{{#if view.isNotStarted}}Submit{{/if}}';
template += '{{#if view.isStarted}} <i class="icon-spinner icon-spin"></i>Saving...{{/if}}';
template += '{{#if view.isFinished}} <i class="icon-check-sign"></i>Finished!{{/if}}'
return Ember.Handlebars.compile(template);
}.property(),
isNotStarted: true,
isStarted: null,
isFinished: null,
classNames: ['btn', 'btn-green'],
isDisabled: false,
click: function(){
if (!this.get('disabled')){
this.set('isNotStarted', false);
this.set('isStarted', true);
this.set('isFinished', false);
this.timer();
}
},
/* Simulates a server call */
timer: function(){
(function(self){
setTimeout(function(){
self.set('isStarted', false);
self.set('isFinished', true);
}, 500);
})(this);
}
});
To me this is really ugly -- we're setting individual boolean values based off of events in order to work with handlebars' purposefully restricted conditional syntax.
What I want is a handlebars construct that accepts something like an Ember StateManager property (not possible with Handlebars syntax). Or, at the very least, I want to alter my template based off of a computed property from a StateManager (again, not possible). So my question is, is there any better way to write the above code to prevent code duplication handling state transitions manually through lots of little boolean flag manipulations?
To me this is really ugly -- we're setting individual boolean values based off of events in order to work with handlebars' purposefully restricted conditional syntax.
Totally agreed, this is a sign that some refactoring is needed.
What I want is a handlebars construct that accepts something like an Ember StateManager property (not possible with Handlebars syntax).
It's possible if you write a custom handlebars helper, but honestly I would not recommend that approach.
Or, at the very least, I want to alter my template based off of a computed property from a StateManager (again, not possible)
Why not? Guessing you mean even if you had that property it's not possible to alter the template without all the booleans.
So my question is, is there any better way to write the above code to prevent code duplication handling state transitions manually through lots of little boolean flag manipulations?
Yes. The reason handlebars has this restriction is to prevent complexity and logic from being part of your templates. For example any time you need to show like 1-of-3 versions based on some value. That kind of logic belongs in the view or controller layer.
So looking at your example, there are two aspects of the template that need to change
text: Should be either "Submit", "Saving..." or "Finished!"
iconClassNames: Either empty, "icon-spinner icon-spin" or "icon-check-sign"
With this in mind we can simplify the template to be:
<i {{bindAttr class="view.iconClassNames"></i>{{view.text}}
And add the properties to the view
UiControls.SubmitButton = Ember.View.extend({
template: Ember.Handlebars.compile('<i {{bindAttr class="view.iconClassNames"></i>{{view.text}}'),
classNames: ['btn', 'btn-green'],
isDisabled: false,
text: "Submitted",
iconClassNames: "",
click: function(){
if (!this.get('disabled')){
this.set('text', 'Saving...');
this.set('iconClassNames', 'icon-spinner icon-spin');
this.timer();
}
},
/* Simulates a server call */
timer: function(){
(function(self){
setTimeout(function(){
this.set('text', 'Finished!');
this.set('iconClassNames', 'icon-check-sign');
}, 500);
})(this);
}
});
This works for the simulation but is not ideal. Really you want text and iconClassNames to be bound to the stateManager. That means changing the text and iconClassNames to be computed properties. Ideally they would be computed based on the underlying state of the model object, and click() would be defined on controller, but for simulation it would be something like this:
UiControls.SubmitButton = Ember.View.extend({
template: Ember.Handlebars.compile('<i {{bindAttr class="view.iconClassNames"></i>{{view.text}}'),
classNames: ['btn', 'btn-green'],
isDisabled: false,
state: 'new',
text: function() {
//return appropriate button text based on state
}.property('state'),
iconClassNames: function() {
//calculate text based on state
}.property('state'),
/* Simulates a server call */
click: function(){
if (!this.get('disabled')){
this.set('state', 'saving');
this.timer();
}
},
/* Simulates a server call */
timer: function(){
(function(self){
setTimeout(function(){
self.set('state', 'finished');
}, 500);
})(this);
}
});

Templating engine that binds model attributes to view?

When rendering a template through a Backbone view you will often end up with some code that looks something like this:
ShirtView = {
template: JST["/templates/shirt_template"],
el: ".shirt-element"
render: function() {
var html = this.template({color: this.model.color, size: this.model.size});
this.$el.html(html);
}
}
This is all well and good and your template will render with the attributes you wanted. But if this.model.color changes then it will not be reflected in the view. You can then use something like modelbinder to explicitly bind elements in the view to your model, but this means introducing extra code to your view.
What I am wondering is if there are any templating engines, like Moustache or Handlebars, that automatically updates the elements belonging to the fields in the attributes object as the model changes, without me having to specify it in the view?
As the comments have suggested, there are several libraries you can use for this ... but I'd like to suggest that you don't need to. I work on a Backbone-powered site with thousands (heck, probably tens or hundreds of thousands) of lines of code, and all we use is our own custom base class.
In essence, all you need to do is:
var TemplatedView = Backbone.View.extend({
render: function() {
this.renderTemplate();
}.
renderTemplate: function() {
this.$el.html(this.template(this.model.toJSON()));
}
});
then you can make any new view a templated one with just:
var ShirtView = TemplatedView.extend({
template: JST["/templates/shirt_template"],
el: ".shirt-element"
});
or, if you have some custom render logic, you just need to call renderTemplate:
var AdvancedShirtView = TemplatedView.extend({
template: JST["/templates/shirt_template"],
el: ".shirt-element",
render: function() {
this.model.fold();
this.renderTemplate();
this.model.press();
}
});
Now we have a few enhancements beyond that (eg. if someone specifies a "rawTemplate" property on one of our views, our renderTemplate will compile it in to a proper template), but that's the beauty of rolling your own solution for something like this: you get exactly what you want.
Or you can use a library :-) But personally, for something that's both so simple and so integral to your site, I don't see why you'd want to.