emberjs two-way-binding temporarily broken after value assignment at init - ember.js

I am trying to assign default values for one of my components at init lifecycle hook; in case an undefined value is passed from parent component. Initially everything seems to be working as expected; however when my component is forced a re-render (possibly via another property value update at parent component); undefined value at parent component is written back to my component.
This means; the value assignment I made during initialization is not reflected to parent component, in other words two-way-binding is temporarily not working (the values of parent and child components are not synchronized). Is it the expected behavior or am I missing sth. important about init event? Where is the appropriate place to initialize undefined values of a component? See the twiddle for a simple illustration.

Okay, first a way to work around is to use the update function on the attr. Checkout this twiddle.
I replaced this.set('name', 'tom') with this.attrs.name.update('tom') in the .js and {{name}} with {{attrs.name}} in the .hbs.
Another way to work around is just to wrap the asignment in a Ember.run.later like I've done here, where I replaced this.set('name', 'tom') with that:
Ember.run.later(() => {
this.set('name', 'tom');
});
So that are the workarounds.
The fact that the bindings are not completely set up on init has a long history. But generally its an anti pattern to initialize a value in a child component and give this value up to the parent. It makes your code less readable and is agains the DDAU (Data down, Actions up) principle.
I recommend to initialize the data explicit when you create your models, not implicit during your component evaluation cycle.
For default values that you don't want to store I recommend you to write a computed property:
nameWithDefault: computed('name', {
get() {
return get(this, 'name') || 'tom';
},
set(key, val) {
set(this, 'name', val);
}
})
This is explicit, does not write down the data after a component is viewed, and works for the user.

I agree with the solution from Lux but if you want to try something else, maybe on your child component you could add a computed property that checks the name or default to another string. This allow you to have different default values on the different child templates (assuming you have different child components that use the same name property).
child-component.js
export default Ember.Component.extend({
child_name: Ember.computed('name', function() {
return this.get('name') || 'tom';
}),
});
child-component.hbs
{{child_name}}
<br>
{{surname}}

Related

Curious behavior when observing Ember's dirtyType property

I try to observe (in my controller) if my Ember model has changed.
personChanged: function() {
// do stuff
}.observes('person.dirtyType'),
This observer is never triggerd unless I will access the isDirty property before. For example if I get the property in the route (where the model is fetched) the observer is triggerd exactly 1 time.
model.people.get('firstObject').get('dirtyType');
controller.set('person', model.people.get('firstObject'));
If I want to get the observer triggered every time the model changed I need to access dirtyType within the observer again.
personChanged: function() {
this.get('person.dirtyType');
// do stuff
}.observes('person.dirtyType'),
The value of dirtyType in the observer is always as expected.
Maybe I'm doing it completely wrong but I can't follow the behavior above.
There is something unpredictable is going on when we use firstObject based on this question Ember computed alias on array firstObject not working
I haven't experienced it to confirm. May be until then you can try the below workaround,
controller.set('person', model.people.get('firstObject'));
Instead of the above, you can define computed property,
person:Ember.computed('model.people.[]',function(){
return return this.get('model.people.firstObject');
})
Now your below observer will work all the time.
personChanged:Ember.observer('person.dirtyType',function() {
// do stuff
}),

Transform label attribute in handlebars from Ember component

I am attempting to fix an issue caused by the 1.13 upgrade with my internationalization/localization implementation. Nine times out of ten, the default label attribute will appear, but occasionally, we will need to serve up an alternate label based on the user's language settings.
The handlebars template, as it is now, has the following:
{{button-widget label="sample" action="actionHandler"}}
Pre 1.13, we would add a function in the component, called on init, that will look up the current langauge setting, and set "this.label" to whatever the result is within our locale file.
i18n: function() {
...
}.on('init')
The {...} looks up the appropriate locale file for the key "test", and updates the label attribute with the result if one is found. For instance, if the user were french, a lookup of "test" might return "échantillon".
From some research, as of 1.13, Ember no longer allows updates to attributes on init. They did, however, create a few new hooks to help (including didInitAttrs and didReceiveAttrs). Unfortunately though, none of them seem to allow me to override the label attribute.
I am attempting to do this without relying on an ugly jQuery hack, and since this is a project being consumed by other applications, button labels may not always be available within the button, so just adding the label to a model and referencing a variable likely isn't a reliable solution.
Any help would be greatly appreciated.
Jason
As Kitler mentioned you could use didInsertElement for all that.
App.GenericComponent = Ember.Component.extend({
didInsertElement: function() {
// Your magic here
}
}
Furthermore, if you need to get access to the store, you can use it as a service by defining a property containing the service:
App.GenericComponent = Ember.Component.extend({
store: Ember.inject.service(),
didInsertElement: function() {
// Your magic here
// Like so
this.get("store").find("model");
}
}
If your language settings are not located in a data store, you could simply attach the object containing the information to the component like you're used to.

Can an Ember component observe a controller property?

I have a controller and a component. when the component is rendered, it is passed on in this manner:
{{modal-filter feature=feature parentController=this.controller}}
where feature is a param passed in via controller to handlebars, and parentController is the controller.
Now, in the controller itself, there is an property (an array). let's call that array requiredValues.
Now within a controller/component itself, we can easily set:
valueObserver : function(){
...
}.observes('requiredValues')
However, I need to observe this controller property from a the modal-filter component. So in the modal-filter component, what would I put as the observer function:
valueObserver : function(){
...
}.observes(???)
Passing an entire controller to a component is a massive code smell. It violates the basic principle of component encapsulation. If the "component" is so tightly coupled to the controller, then it's a view, from where you can access the controller by simply saying this.controller. Input to components should be strictly through parameters passed in when they are invoked. Output from components is through send, which the controller can map to some behavior of its choosing in its view's template by saying {{my-component action='eraseHardDisk'}}.
You don't need to directly observe anything on the controller from within the component. If you call the component with {{my-component param=someProperty}}, then any change to the controller's someProperty will automatically be propagated to the param of the component. The component can then define some computed property on param, or observe it, or use it in its own template where it will be automatically kept up-to-date.
What you shouldn't do, but I'll tell you how for completeness
If you're passing the controller in, you can just watch an item on the parentController property, though I wouldn't recommend this at all.
valueObserver : function(){
...
}.observes('parentController.requiredValues')
This would assume the entire array is being replaced, not just an item added, or changed.
Item Added or Removed
valueObserver : function(){
...
}.observes('parentController.requiredValues.[]')
Item Property foo changed on one of the requiredValues items
valueObserver : function(){
...
}.observes('parentController.requiredValues.#each.foo')
What you should do
Instead of passing in the controller, just pass in the property, and observe the property.
{{modal-filter feature=feature property=someProperty}}
propertyObserver : Ember.observer('property', function(){
...
})

Observing controller property from view only works if get('controller') called from didInsertElement

Note the below Ember view definition. If I remove the didInsertElement call or comment out the get('controller') call, the setupMultiselect observer never gets called. Is this a feature or a bug? Confused...
Discourse.KbRelatedObjView = Discourse.View.extend({
...
didInsertElement: function() { var self = this;
// for some reason this needs to be here else the observer below never fires
self.get('controller');
},
setupMultiselect: function() { var self = this;
...
}.observes('controller.objPage')
});
I wouldn't say it's a feature or a bug, more like a quirk. It is the expected behavior though. It's noted here.
UNCONSUMED COMPUTED PROPERTIES DO NOT TRIGGER OBSERVERS
If you never get a computed property, its observers will not fire even if its dependent keys change. You can think of the value changing from one unknown value to another.
This doesn't usually affect application code because computed properties are almost always observed at the same time as they are fetched. For example, you get the value of a computed property, put it in DOM (or draw it with D3), and then observe it so you can update the DOM once the property changes.
If you need to observe a computed property but aren't currently retrieving it, just get it in your init method.

ember computed properties executed when

I have inherited a codebase written in Ember 0.9.8.1
In some cases it can be very slow.
I'm in the process of narrowing down why it is slow.
I have noticed the following.
A function in an ArrayController is called to load data.
To load the data it gets json from the backend (fast) and then for every row it creates an (previously defined) Ember.Object (slow) and push that to the content[] of the ArrayController.
Example:
App.ExOb = Ember.Object.extend({
data1: null,
data2: null,
func1: function () { // statements }.property('data1').cacheable()
func2: function () { // statements }.property('data2').cacheable()
..etc..
})
App.lotsOfData = Ember.ArrayController.create({
content: [],
loaddata: function() {
var self=this;
get_data().forEach(function (row, index) {
var d = App.ExOb.create(row.data);
self.pushObject(d);
}
}
})
I'm trying to figure out why the creation and push of the Ember.Object is slow.
What I did notice was that on creation of the object (in the example App.ExOb.create()) every property function of the object (in the example func1() and func2()) is called.
I've tried a small bit of ember code to see why this would happen but can't seem to emulate this. The only time I can see the computed property being executed is when I do a get() of that property.
Can anyone tell me (or point me to documentation that I missed) when a computed property function is executed (other than doing a get() ofcourse :-) )?
Edit:
So far I found the following reasons to execute a computed property:
1. Calling/using the property directly
2. Using the propery in a handlebars template that is shown in the browser.
This is just a quick stab at the problem, but have you tried to create the ExOb and initialize its properties manually, something like:
self.pushObject(App.ExOb.create({
data1: row.data.data1,
data2: row.data.data2,
... etc ...
});
That might be faster, as Ember doesn't have to guess at what to copy from the row.data object and into the App.ExOb object.
Generally, the func1() and func2() functions wouldn't need to be overloaded upon object creation in my mind, but your requirements might dictate otherwise.