Ember computed property doesn't fire when bound to deeply nested property - javascript

This JSBin isolates a problem I ran into in my code. I have a hierarchy of embedded models and a computed property (data) that is supposed to fire whenever a value at the very bottom of the chain changes (symbol). The example displays the property directly as well as the result of the computed property. A button changes the value on click. You'll see that it updates the property but the computed property doesn't fire. Why doesn't selectedAllocation.positions.#each.instrument.symbol work to trigger the computation when any instrument.symbol changes?
If the example seems contrived, it's only because I tried to abstract something that is more complex in reality, e.g. there is more than just one object in these arrays and data is necessary because another library expects a simple JS object in a particular format.

Note that #each only works one level deep. You cannot use nested forms
like todos.#each.owner.name or todos.#each.owner.#each.name.
http://emberjs.com/guides/object-model/computed-properties-and-aggregate-data/
You'll need to create an alias to bring symbol up one level (Not a coffeescript guy, hopefully you can read through the hacking, the positions alias below is for kicks and giggles, makes no difference).
App.Position = Ember.Object.extend({
instrumentSymbol: Em.computed.alias('instrument.symbol')
})
App.IndexController = Ember.ArrayController.extend
selectedAllocation: null
positions: Em.computed.alias('selectedAllocation.positions'),
data: (->
#get("positions").map (position) ->
symbol: position.get "instrumentSymbol"
).property "positions.#each.instrumentSymbol"
...
http://jsbin.com/bivoyako/1/edit

Related

binding updates in knockout.js

This may well be a very basic problem for anyone familiar with knockout.js, however it is causing me a problem.
I have a situation where I have a model containing an array of items that is dynamically added to and displayed in the view.
So far no problem, I can add entries into the model and the view is updated appropriately.
However. each item in the array itself has an array as a property, this is an array of object, and when I update the properties on these objects the view is not updated.
It is difficult to demonstrate this is a short code snippet so I have created a JsFiddle to show the problem:
https://jsfiddle.net/mikewardle/t0nvwqvL/1/
I have tries making the properties generated by calling
ko.observable()
rather than initializing them directly, but to no avail.
clicking the add button adds items to the array on the model itself.
either of the change... buttons alters the properties of the objects in the inner array.
As Ko2r stated your properties are not declared as observables and therefore updates will not be noticed by knockout.
To fix your changecolors() function you just need to change your linePusher function to create the color as an observable:
var linePusher = function (color, name) {
self.lines.push({ color: ko.observable(color), name: name, current:0 });
};
and then update usages of the color property to box/unbox the observable instead of replacing its value with the standard assignment operator, "="
for (i=0;i<counters.length;i++){
var lines = counters[i].lines();
for (j=0;j<lines.length;j++){
//lines[j].color = color;
lines[j].color(color); //sets the existing observable to the new value
}
}
Unfortunately I can't seem to make sense of your code enough to figure out what the increment() function is supposed to be doing so I can't tell you how to fix it, but hopefully the fixes to changecolors() put you on the right track.
You might want to read up on working with observables

Ember GroupableMixin by using groupProperty

I am trying to implement GroupableMixin (https://github.com/ahawkins/ember.js/blob/groupable-mixin/packages/ember-runtime/lib/mixins/groupable.js) on EmberJS but I have trouble using it.
I want to specify a groupProperty in my ArrayController (just like how we have sortProperties) and I want my groupings to be calculated based on that property.
I did try http://emberjs.jsbin.com/cawaq/3/edit and this is what I got:
App.UsersIndexController = Ember.ArrayController.extend({
sortProperties: ['created_at'],
groupProperty: 'role',
groupedContent: groupBy('role'), //How do it reference groupProperty in groupBy?
//How can groupedContent be updated whenever groupProperty is changed?
});
I have seen http://discuss.emberjs.com/t/ember-enumerable-no-group-by/3594/6 and http://jsbin.com/ukagip/2/edit but still cannot quite figure out how to make that work properly.
The intent and behavior of Mr. Hawkins' GroupableMixin is very different from the one that's used discussed in Ember's Discourse forum. It seems that you're confusing the two methods.
Mixin Approach (a la Mr. Hawkins)
Note this is the approach I'd recommend
The GroupableMixin is an instance of Ember.Mixin, which is used to extend an object's prototype. For a more in depth explanation of mix-in's see Coding Value's explanation.
We can determine the mix-ins requirements and behavior from reading the tests for the mix-in:
the class must behave like an ArrayProxy (so ArrayController's are fine)
a groupBy property must be set for the class
So we can include this mix-in in an array controller as follows:
App.UsersIndexController = Ember.ArrayController.extend(Ember.GroupableMixin, {
groupBy: 'role'
});
Now that we've satisfied the mix-in's requirements, we'll have access to a few new computed properties and functions such as groupedContent for free. Without adding anything else to the controller, we could write something like this in the template to access the groups:
{{#each group in groupedContent}}
<h1>{{group.name}}</h1>
<ul>
{{#each user in content}}
<li>{{user.name}}</li>
{{/each}}
</ul>
{{/each}}
Here's an example that groups words in an array controller by first letter.
Computed Helper Approach (a la Ember Sherpa)
This method creates a helper which defines a computed property based on the key provided to the function. We can create a similar function which maps to the group interface of the mix-in:
Sherpa = {};
Sherpa.groupBy = function (groupBy) {
var dependentKey = 'content.#each.' + groupBy;
return Ember.computed(dependentKey, function(){
var result = [];
this.get('content').forEach(function(item){
var hasGroup = !!result.findBy('name', get(item, groupBy));
if (!hasGroup) {
result.pushObject(Ember.Object.create({
name: get(item, groupBy),
content: []
}));
}
result.findBy('name', get(item, groupBy)).get('content').pushObject(item);
});
return result;
});
};
Now we can use this function to create a groupedContent computed property: groupedContent: Sherpa.groupBy('role')
Here's an example which uses the same template as the previous example and has only swapped the mix-in with this method.
Recommendation
Use the GroupableMixin, it's far more robust than the method discussed in the forum:
The method discussed in the forum...
is simple and easy to understand
is inefficient
Recomputes the entire groups array whenever a change is made to the array or relevant property
Doesn't implement map structures for lookups and instead searches iteratively through the array
Nukes all itemControllers on any computed change: If you check some boxes in the second example and then add a new word, all of the checkboxes will be cleared
The mix-in...
is a much more robust, albeit complex, implementation
handles inserts and removals to groups instead of recomputing the entire map
backs the groups with a map for better performance and efficiency
behaves as expected when using an itemController: just tick of checkboxes and add some words in the first example, and you'll notice that the checkboxes are never cleared

Getting ember-i18n.js Translation Keys from Controller Properties

I just started working on an existing Ember.js project that uses ember-i18n.js for localization.
Ember.Mixin.create({
setupController: function (controller, model) {
controller.set("somePropertyName", "my.translation.key");
}
});
I defined another helper that lets me get a translation key from a context property, instead of passing it in directly, e.g. I can use {{tr someContext.someProperty}} instead of {{t "my.translation.key"}}.
Ember.Handlebars.registerBoundHelper('tr', function(property, options) {
return Ember.I18n.t(property);
});
I'm fairly new to Ember, so my thought process was that I would simply be able to swap out replace {{somePropertyName}} in my template with {{tr somePropertyName}}. That didn't work. When I set a breakpoint inside the helper, I see that property is always undefined.
I also tried modifying my helper to use Ember.Handlebars.get() as demonstrated here, but that also is resolved undefined.
Assuming I'm not able to change where the translation key is coming from, is there a proper way to get the translation key from the controller to the helper?
What you are doing should be fine - the property argument to the helper is just a string, and passing a string from a controller to a template (and then to a helper) is straightforward.
So, I'm guessing (and it's just a guess without a jsbin) that either you have a coding issue somewhere. Or something weird is going on with your use of the mixin, perhaps.
I'd see how far the string property is getting from your mixin to the controller to the template to the helper.
If you change {{tr property}} does the string get written out into your template ok? If not, then the property isn't being set on your controller.
If you have a jsbin, then that would make it easier to see what is going on.

What does template.data._id do in Meteor?

Beginner Meteor/JS question:
When associating objects in Meteor I see small line of code that I'm not understanding. For example, post with associated comments.
var $body = $(e.target).find('[name=body]');
var comment = {
body: $body.val(),
postId: template.data._id
};
So get the content of the comment, put it in the variable "comment", and also create a postId to go into this comment so you know what post the comment belongs to. This postId is being called in with *'template.data._id'*
My questions are:
So you call template, then wouldn't you want to call the template name? Not data? Where is data coming from?
That aside, so you call data...and then ._id, are there other options to 'data'? IE
template.data.(option)
This isn't working for me, haha, *console.log(template.data._id);* is coming back undefined. So it's not grabbing the object ID as advertised. I'm sure I messed something up.
Here's the surrounding code if you need more context:
https://github.com/DiscoverMeteor/Microscope/blob/master/client/views/comments/comment_submit.js
Beginning with the easiest first, under your point 3 it should read:
console.log(template.data._id);
As to points 1 and 2, the key idea to note is that your code is being called inside of Template.commentSubmit.events({}). Inside of this object (the "{}"), you are working with an instance of the template in the document, including whatever data is being passed to that instance. In a different template, you will receive different data and hence template.data will consist of different keys and values. You can check out more in the documentation here, http://docs.meteor.com/#template_inst.
So in answer to your question, the reason you can invoke "data" rather than the template name is that the template name has already been provided by Template.commentSubmit. Note that inside of events({}), "this" will generally be equivalent to "template.data." So
console.log(template.data._id);
will generally be equivalent to
console.log(this._id);
The properties you can access on the data will always vary based on the instance received by the template. For example, if template.data consists of {_id: 1, name: "x", location: "y"}, you can retrieve these values by calling the keys, e.g. "template.data.name" or "template.data._id" etc. In your example, you are correct that you are setting postID to template.data._id.
The data arriving to the template comes from your Meteor.subscriptions. Hope this helps.
The template has a data context. When you use an {{#each}} block you are iterating through data, in this case posts.
So using template.data retrieves the data context for the template. It would refer to a post from where the comment form is. So template.data._id is the equivalent of this post._id (where post is the post you're commenting on).
The thing is I think this was removed from meteor. It was there a couple of versions back. I'm not sure about this but that's what I thought. I would have thought the correct code should be this._id. Where this ends up being the data context of the form (which would again be the post).
Could you check if that gives you undefined if you changed it out?
The .events method of a template takes in an event map:
http://docs.meteor.com/#eventmaps
The callbacks for each of the events (in this case 'submit form') can take two arguments. The first one 'e' is the javascript event object and the second one 'template' is the instance of the template where the event occurred. That template instance has a bunch of utility methods/properties (see http://docs.meteor.com/#template_inst) one of which is .data (see http://docs.meteor.com/#template_data). That .data property returns the data that the template was bound to (in your Microscope example it looks like it's bound to a comment object) and that object (in this case) has an _id property.
Note that the 'template' (little T) is not the same as Template (big T).

Knockout mapping plugin [create, update]: objects created, cannot update

I've posted my code here: http://jsfiddle.net/HYDU6/6/
It's a pretty stripped-down version of what I'm actually working with, but captures the essence of my problem. My view model is like so:
var viewModel = {
objects: {
foo: [
{ text: "Foo's initial" },
],
bar: [
{ text: "Bar's initial" },
]
}
}
I'm using the ko.mapping plugin and my create handler for objects instantiates Obj from objects.foo and then objects.bar, returning the resulting two items in an array. This part works fine; I use
var view = {};
ko.mapping.fromJS(viewModel, mapping, view);
My issue is updating based on new data. (i.e., getting data from the server). I have an object of new data and I attempt
ko.mapping.fromJS(new_model, mapping, view);
I suspect this is incorrect but I have not been able to get it working despite extensive searching. (Trust me, it's been days. ): Anyway, thanks for any help.
EDIT: So I've mostly figured it out - I was depending too heavily on mapping.fromJS and certain things were not being wrapped into observables. I also realized that I didn't need the create(), only the update(), as it is called after create() anyway. If you have a similar problem let me know!
John,
When updating your data using ko.mapping be sure you don't create a new item. Your UI is already bound to the existing items, so you just want to update the values of the existing item properties; not create new ones. For the example you posted, you'll want to adjust your "update" method of your map to insert the new values into the correct ko.observable property, rather than creating a new object in it's place. The ko.mapping "update" method has a few different parameter lists depending on usage, with the third parameter being the target object of the map. You would want to update that object's properties.
obj.target[label].items[0].text(obj.data[label][0].text);
But, that's a bit of a mess. You'll probably want to create a second level of mappings (create / update) to handle "deep" object hierarchies like in your fiddle. For example one map for objects at the "foo/bar" level, and another call to ko.fromJS from within "update" with another map for the child Obj() objects.
After fixing that, you'll run into a couple simple binding errors that you can fix using another "with" binding, or a "foreach" binding for the child arrays.
Overall, you've just run into a couple common pitfalls, but nothing too severe. You can learn a bit more about a few of these pitfalls on my blog here : http://ryanrahlf.com/getting-started-with-knockout-js-3-things-to-know-on-day-one/
I hope this helps!

Categories

Resources