Computed Properties and #each - javascript

I've been reading through the docs and API and I'm having a hard time finding an explanation for the following:
export default Ember.Controller.extend({
collectionTotal: function() {
var games = this.get('model');
return games.length
}.property('#each')
});
What is actually happening with the .property('#each')? I know I am getting the computed property back, I don't understand what #each is.

What is #each?
#each observes individual properties on each item in an array.
For example, if I observe users.#each.name, I will receive an event if:
the users property is replaced e.g. this.set('users', ...)
an item is added or removed from users
the name property changes on any of the items
You can observe multiple properties using this syntax: users.#each.{name,email}
You cannot nest them. This will not work: users.#each.friends.#each.mood
Read more in the official documentation:
Computed Properties and Aggregate Data with #each
Ember.ArrayProxy Class
To answer your question
#each by itself doesn't make sense. You can observe the [] property if you only need to watch for items being added or removed.
Typically you should observe the same properties that are used in the body of the function. In your example that would be model and length:
collectionTotal: function() {
var games = this.get('model');
return games.get('length');
}.property('model.length')
Or equivalently:
collectionTotal: function() {
return this.get('model.length');
}.property('model.length')
Or equivalently:
collectionTotal: Ember.computed.reads('model.length')

Related

Adding an item to array does not re-render polymer template

I am trying to render a dynamic list of items using a template of dom-repeat like this:
<template is="dom-repeat" items={{numbers}} as="anumber" >
<div>
{{anumber}}
<paper-button class="deleteThisNumber" index={{index}}></paper-button>
</div>
</template>
<paper-button id="addNumber"></paper-button>
Each item has a button which will delete this item.
There is also a button outside of the dom-repeat template that tries to add an entry to array numbers. The JS looks like this:
Polymer ({
is: "something",
properties: {
numbers: {
type: Array,
value: ["1"]
}
},
removeByIndex: function (array, index) {
return array.filter(function (elem, _index) {
return index != _index;
});
},
attached: function () {
var myself = this;
$(this).on('click', '.deleteThisNumber', {}, function (e) {
myself.numbers = myself.removeByIndex(myself.numbers, this.index)
});
this.$.addNumber.addEventListener("click", function (e) {
myself.numbers.push("123");
})
},
...
});
The result is: deleting works, but adding does not.
By saying "works", I mean the list reflects the change by adding/removing an entry in the DOM. I checked the property numbers it is correctly modified all the time. So why does Polymer not reflect changes of an array property to a template if the change is addition(array.push)? How should I fix this? (I am open to any suggestions other than manually adding divs.)
My Polymer version is 1.X
Change the code for array push to :
this.$.addNumber.addEventListener("click", function(e) {
myself.push("numbers", "123");
})
There has to be an observable change in order to render the updated property or subproperty. An observable change is a data change that Polymer can associate with a path.
If you manipulate an array using the native methods (like Array.prototype.push), you must notify Polymer after the fact. OR, use the Polymer methods for array mutations.
When modifying arrays, a set of array mutation methods are provided on
Polymer element prototypes which mimic Array.prototype methods, with
the exception that they take a path string as the first argument. The
path argument identifies an array on the element to mutate, with the
following arguments matching those of the native Array methods.
These methods perform the mutation action on the array, and then
notify other elements that may be bound to the same array of the
changes. You must use these methods when mutating an array to ensure
that any elements watching the array (via observers, computed
properties, or data bindings) are kept in sync.
Every Polymer element has the following array mutation methods
available:
push(path, item1, [..., itemN])
pop(path)
unshift(path, item1, [...,
itemN])
shift(path)
splice(path, index, removeCount, [item1, ..., itemN])
Learn More
I found out the solution is to force a notifyPath with a little more:
myself.numbers.push("123"); //before only has this
myself.notifyPath('numbers', myself.numbers.slice()); //added
Referred to https://github.com/Polymer/polymer/issues/2068#issuecomment-120767748
Answer from #miyconst
More than one way you can fix your code.
1) The way you've already been using when you delete an item.
this.$.addNumber.addEventListener("click", function (e) {
myself.numbers.push("123");
myself.numbers = myself.numbers.slice();
})
2) Answer from yourself (with slight changes)
this.$.addNumber.addEventListener("click", function (e) {
myself.numbers.push("123");
myself.notifyPath("numbers");
})
3) Answer from #Ofisora
this.$.addNumber.addEventListener("click", function(e) {
myself.push("numbers", "123");
})
Here's why the fixes work, https://www.polymer-project.org/1.0/docs/devguide/data-system#observable-changes

Knockout component updating observable not being subscribed to by parent view model?

I've written a component called Upload which allows users to upload files and then report back with a JSON object with these files. In this particular instance, the Upload component has a parameter which comes from a parent view model:
<upload params="dropzoneId: 'uploadFilesDropzone', postLocation: '/create/upload', uploadedFiles: uploadedFiles"></upload>
The one of importance is called uploadedFiles. The parameter binding here means I can reference params.uploadedFiles on my component and .push() new objects onto it as they get uploaded. The data being passed, also called uploadedFiles, is an observableArray on my parent view model:
var UploadViewModel = function () {
// Files ready to be submitted to the queue.
self.uploadedFiles = ko.observableArray([]);
};
I can indeed confirm that on my component, params.uploadedFiles is an observableArray, as it has a push method. After altering this value on the component, I can console.log() it to see that it has actually changed:
params.uploadedFiles.push(object);
console.log(params.uploadedFiles().length); // was 0, now returns 1
The problem is that this change does not seem to be reflected on my parent viewmodel. self.uploadedFiles() does not change and still reports a length of 0.
No matter if I add a self.uploadedFiles.subscribe(function(newValue) {}); subscription in my parent viewmodel.
No matter if I also add a params.uploadedFiles.valueHasMutated() method onto my component after the change.
How can I get the changes from my array on my component to be reflected in the array on my parent view model?
Why do you create a new observable array when the source already is one? You can't expect a new object to have the same reference as another one: simply pass it to your component viewModel as this.uploads = params.uploads. In the below trimmed-down version of your example, you'll see upon clicking the Add button that both arrays (well the same array referenced in different contexts) stay in sync.
ko.components.register('upload', {
viewModel: function(params) {
this.uploads = params.uploads;
this.addUpload = function() { this.uploads.push('item'); }.bind(this);
},
template: [
'<div><button type="button" data-bind="click: addUpload">Add upload</button>',
'<span data-bind="text: uploads().length + \' - \' + $root.uploads().length"></span></div>'].join('')
});
var app = {
uploads: ko.observableArray([])
};
ko.applyBindings(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: {name: 'upload', params: {uploads: uploads}}"></div>
It is only in case your source array is not observable that things get a little more complicated and you need to have a manual subscription to update the source, eg. you would insert the following in the viewModel:
this.uploads.subscribe(function(newValue) { params.uploads = newValue; });
Additionally the output in the text binding would not be updated for the source because it is not observable. If for some reason that I cannot conceive of you would want to have 2 different observableArrays (1 source & 1 component), you should still be able to do with the line above, but replace the function code with params.uploads(newValue)
The problem may be related to this bug (to be confirmed): https://github.com/knockout/knockout/issues/1863
Edit 1: So this was not a bug. You have to unwrap the raw param to access the original observable. In your case, it would be:
params.$raw.uploadedFiles() //this would give you access to the original observableArray and from there, you can "push", "remove", etc.
The problem is that when you pass a param to a component, it gets wrapped in a computed observable and when you unwrap it, you don't have the original observableArray.
Reference: http://knockoutjs.com/documentation/component-custom-elements.html#advanced-accessing-raw-parameters
While Binding Property that involves Parent --> Child Relation
Use Binding in this way
If You want to bind data to Child Property
data-bind='BindingName : ParentViewmodel.ChildViewModel.ObservableProperty'
Here it seems you want to subscibe to a function when any data is pushed in Array for that you can write subscribe on Length of Observable array which can help you capture event that you want.
This should solve your problem.

Computed vs Observes on Controller in Ember

It was always my understanding that .observes('someProperty') and .property('someProperty') worked exactly the same, except that the former is used for triggering function calls and the latter is used to keep object properties up to date.
But now I'm having a problem. My controller code looks like this:
_logChange: function(){
console.log('model array observer fired');
}.observes('model.#each'),
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.#each')
The observer and computed property both watch model.#each but for some reason, the observer fires on every model change and the property only updates TWICE before mysteriously going dead. statsData is calculated once on initial page load, and once on the first route transition, then after that, none of the transitions (with the changes in the underlying model they make) affect it.
What's going on here? Shouldn't they respond to change in the same way?
Note that I am using the statsData property in my template.
observers fire immediately, computed's fire as part of the run loop and scheduled in a debounced fashion. Currently all you're watching is that you add or remove an item to the collection, not whether or not a property on one of the items in the collection has changed. If you want to watch a particular property, you need to specify it.
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.#each.cost')
if you just want to watch the collection changing you should just use []
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.[]')
Thanks to the lovely folks on Ember IRC, I was able to figure it out. The problem was that I was passing statsData to a component, like this: {{common-statistics values=statsData}} and in the component, I had this function:
_validateValues: function(){
var values = this.get('values');
if(!values || !Ember.isArray(values) || values.length === 0)
{
this.set('values',[]);
}
}.on('willInsertElement')
which is, as you can see, setting values if it's not what the component is expecting. Unfortunately, this was affecting statsData on the controller as well, thanks to this JavaScript language feature. By setting statsData in the component, I was breaking the computed property on the controller.
So it was never a problem with Ember at all. I just failed to realize that object properties on Ember objects behave the same way they do on "regular JavaScript objects."

Ember.js: How one model can observe other models?

I have a model for my sources and a model for each segment. Each source has many segments. Each segment has an action that toggles an isSelected property.
I need to keep an updated list of selected segments. My first thought was to do something like...
App.Source = Ember.Object.extend({
selectedSourceList = Em.A(),
selectedSourcesObserver: function() {
// code to update array selectedSourceList
}.observes('segment.isSelected')
});
.. but that observes() function isn't right. I'm new to ember, so my approach may be completely wrong.
How can a method in one model observe the properties of many other models?
EDIT: corrected names to indicate that the segment model is for a single segment - not a collection of segments (that is what I plan to do in the sources model).
I think there are three parts to your question:
how to observe a collection
observers in general
managing relationships
observing a collection
The #each property helps observe properties for items in a collection: segments.#each.isSelected
observers in general
.observes() on a function is a shorthand to set up an observer function. If your goal for this function is to update a collection you might be better served by using .property() which sets up an observer and treats the function like a property:
selectedSegments: function() {
return this.get('segments').filterProperty('isSelected', true);
}.property('segments.#each.isSelected')
This means selectedSegments is the subset of segments from this object that are selected and is automatically managed as items are dropped or marked selected.
managing relationships
For plain Ember Objects you will need to manage the relationships, pushing new items into the array, etc.
segments = Em.A(), //somehow you are managing this collection, pushing segments into it
Also note the difference between Ember Objects and Ember Models. Ember Data is an optional additional library that allows specifying models and relationships and helps to manage the models for you. With Ember data you might have something like:
App.Source = DS.Model.extend({
//ember-data helps manage this relationship
// the 'segment' parameter refers to App.Segment
segments: DS.hasMany('segments'),
selectedSegments: function() {
return this.get('segments').filterProperty('isSelected', true);
}.property('segments.#each.isSelected')
});
And App.Semgent
App.Segment = DS.Model.extend({
selection: DS.belongsTo('selection')
});

Missing view.context or templateContext in an Ember event handler

I'm trying to push the object that populated a view into an array, but the reference is somehow getting lost. I've got an Ember view, with a defined eventManager:
FrontLine.NewProductButton = Em.View.extend({
tagName: 'button',
classNames: ['addtl_product',],
templateName: 'product-button',
eventManager: Ember.Object.create({
click: function(event, view) {
FrontLine.ProductsController.toggleProductToCustomer(event, view);
}
})
})
That view renders a bunch of buttons that are rendered with properties that come from objects in the ProductsController using the #each helper. That part works great. And when I click on any of those buttons, the click event is firing and doing whatever I ask, including successfully calling the handler function (toggleProductToCustomer) I've designated from my ProductsController:
FrontLine.ProductsController = Em.ArrayController.create({
content: [],
newProduct: function(productLiteral) {
this.pushObject(productLiteral);
},
toggleProductToCustomer: function(event, view){
FrontLine.CustomersController.currentCustomer.productSetAdditional.pushObject(view.context);
}
});
I'm trying to use that function to push the object whose properties populated that view into an array. Another place in my app (a simple search field), that works perfectly well, using pushObject(view.context). Here, however, all that gets pushed into the array is undefined. I tried using view.templateContext instead, but that doesn't work any better. When I try console.log-ing the button's view object from inside those functions, I get what I'd expect:
<(subclass of FrontLine.NewProductButton):ember623>
But either view.context or view.templateContext return undefined. How do I access the object I'm after, so I can add it to my array?
The simple answer is that it was one letter's difference:
view.content
or:
view.get('content')
provides the source object in that particular situation, rather than view.context.
(My only real challenge with Ember so far is that accessors for objects and properties vary so much from situation to situation, and there's no real documentation for that. Sometimes the object is at view.context, sometimes it's at view.content, sometimes _parentView.content, etc., etc. It would be awesome if there were a chart with the umpteen different syntaxes for accessing the same data, depending on which particular aperture you're reaching through to get it. I'm still discovering them...)

Categories

Resources