Ember properties with multiple dependencies dont update as expected - javascript

I have following issue concerning understanding ember properties:
If i have a propertyA:
propertyA: function() {
return this.get("propertyB.someObject.someValue");
}.property("propertyB")
and a propertyB:
propertyB: function() {
return this.get("propertyX.someObject");
}.property("propertyX", "propertyY", "propertyZ")
And i have a binding for propertyA in some template like:
{{propertyA}}
Then in 90% of the cases in my code it happens that propertyA does not get updated properly when i set i.e. propertyX.
If i understand it correctly, then propertyB should become dirty as soon as one of the dependent properties (like propertyX) changes. This should automatically make propertyA dirty and thus update it automatically since it has a binding.
What happens in my code is, that propertyA remains the old cached value even when i called it in the console, but when i call propertyB it revaluates and returns the updated code, since it was dirty.
The question is, why does propertyA not automatically become dirty when propertyB does? Is it because propertyB has no binding in a template? I thought it is not necessary if propertyA has the dependence.
I also figured out that this problem does not occur when propertyB just depends on propertyX, so the multi-dependency must somehow mess things up.
Sorry for this quite complicated explanation but i tried to abstract my actual code as simple as possible.
UPDATE:
Ok here some actual code:
Controller:
styling: function() {
var clipValues = this.get("clip.styling") || {};
var infoValues = this.get("clip.info.styling") || {};
return Ember.Object.create(jQuery.extend({}, clipValues, infos));
}.property("clip.styling", "clip.info.styling"),
showBottombar: function() {
return (!!this.get("bottombarSrc") || !!this.get("styling.bottombar.fancyStuff"));
}.property("styling"),
Somewhere else the clip gets set for this controller. And later its info gets updated in the clip model which is a simple Ember.Object:
getInfo: function(url) {
var self = this;
return App.ajax(url).then(function(response) {
self.set("info", response);
});
}
Now after getInfo gets called, the {{showBottombar}} in the template shows "false" even if "bottombarSrc" and "...fancyStuff" is true. When i call "styling" from the console, it reevaluates the styling code which indicates that it was marked as dirty after clip.getInfo happened (which sets the "info"). But this does not effect the showBottombar. It just does not get called afterwards.
UPDATE 2
There are two strange ways of making it work, but i dont understand why:
First one is adding a styling binding to a template:
{{styling}}
That causes showBottombar to get called after the styling changes.
Second one is removing other dependencies from the styling property:
styling: function() {
var clipValues = this.get("clip.styling") || {};
var infoValues = this.get("clip.info.styling") || {};
return Ember.Object.create(jQuery.extend({}, clipValues, infos));
}.property("clip.info.styling"),
(no more "clip.styling" dependency). Which also causes the showBottombar property to work properly. Both ways work individually.

propertyA: function() {
return this.get("propertyB.someObject.someValue");
}.property("propertyB").volatile()
http://emberjs.com/api/classes/Ember.ComputedProperty.html#method_volatile

Related

Function inside of for loop not being tested

I have a function im trying to test:
vm.clearArray = function(){
for (var id=0; id<vm.copyArray.length;id++){
vm.styleIcon(vm.copyArray[id],'black')
}
vm.copyObjArray = [];
vm.copyArray = [];
}
I'm trying to test it like:
it('should have cleared copyArray on function call', function(){
var ctrl = $componentController('copy', null);
spyOn(ctrl, 'clearArray').and.callThrough();
spyOn(ctrl, 'styleIcon').and.callThrough();
ctrl.copyArray = [123];
ctrl.clearArray();
expect(ctrl.clearArray).toHaveBeenCalled();
// expect(ctrl.styleIcon).toHaveBeenCalled();
expect(ctrl.copyObjArray).toEqual([]);
expect(ctrl.copyArray).toEqual([]);
});
If I uncomment the above expect I get an error and the vm.styleIcon call is never covered in my coverage report. By setting copyArray to contain a value in the array I would think that the for loop would then trigger when running the test. That does not seem to be the case.
Thanks.
I believe there is some kind of inheritance scheme that is causing your error. My assumption is that your controller is extended by a base controller.
From what few code I see, I can make two assumptions:
1) clearArray() is overridden in the child controller eg.
vm.clearArray = function(){
...
vm.copyArray = [];
}
so you are trying to test the wrong clearArray()
or
2) ctrl.copyArray is not writable because of the way inheritance was implemented, eg.
function ParentController() {
var vm = this;
vm.copyArray = [];
vm.copyObjArray = [];
vm.clearArray = function() {
for (var id=0; id<vm.copyArray.length;id++){
vm.styleIcon(vm.copyArray[id],'black')
}
vm.copyObjArray = [];
vm.copyArray = [];
}
vm.styleIcon = function(index, color) {
}
};
function ChildController() {
ParentController.call(this);
}
ChildController.prototype = Object.create(ParentController.prototype, {copyArray:{ value: [] } });
var ctrl = new ChildController();
Using the above will yield the error, copyArray is defined as a non writable property, so line:
ctrl.copyArray = [123];
does not change its value.
Object.defineProperty()
Difference between Configurable and Writable attributes of an Object
Anyway, without more of the code, it is difficult to get what is causing the error.
The code of the loop looks good, so I think that the property vm.copyArray might not be set at all. If you add a console.log(vm.copyArray), what is the result?
Perhaps vm and $componentController('copy', null) are not references to the same object, but call each other's functions through some library? is there any other way to reference vm from the test script, rather than using $componentController('copy', null)?
Your loop must be triggered when you pass the array in the function as an argument. Of course your actually code will fail unless the pass vm.copyArray as an argument in the actual code, but it will show you if the loop is the problem, or the reference to the vm from the test script:
//tested function
vm.clearArray = function(copyArray){
for (var id=0; id<copyArray.length;id++){
vm.styleIcon(copyArray[id],'black')
}
}
//test
ctrl.clearArray([123]);
It's difficult to determine the exact reason because you've shown a sample taken straight from your test, and not a minimal, complete, and verifiable example. Also, you didn't specify what the error(s) or expect results are, so we're going off very limited information.
That said, I strongly suspect vm is undefined/null or not a prototype instantiatable through $componentController. If this is the case, you should be receiving an error at spyOn(ctrl, 'clearArray').and.callThrough() or ctrl.clearArray(), never running the loop and thus never calling vm.styleIcon. In this scenario you'd need to verify that ctrl is in fact an instance of whatever prototype vm is a part of(is it actually a global variable?).
If this is not the case and both the vm prototype is correct and $componentController('copy', null); is creating the object you think it is, perhaps styleIcon is undefined/null, unable to be called and creating essentially the same problem. In this scenario, ensure styleIcon is set and that it's the function you think it is.
When all else fails, debuggers are your friend.
Please specify what the error(s) are and where they're occuring(in more detail) for a better answer.

Why are jQuery objects in property initialization empty?

'use strict';
var Controller = function Controller() {};
Controller.init = function() {
if (!Controller.PROPERTIE.element || !Controller.PROPERTIE.indicator) {
return 'Expected to find [data-controller] and [data-controller-to]';
}
Controller._start();
};
Controller.PROPERTIE = {
element: $('[data-controller]'),
indicator: $('[data-controller-to]'),
state: false
};
Controller._start = function() {
Controller.PROPERTIE.indicator.bind('click', Controller._toggle);
};
Controller._toggle = function() {
Controller.PROPERTIE.element
.animate({
bottom: (Controller.PROPERTIE.state = !Controller.PROPERTIE.state) ? '-110' : '0'
});
};
apparently the elements in the object property does not exist, but they do exist ! Could someone tell me if I can not use javascript like that?
Maybe there is something with hoisting that is breaking the script?
I already try put the object before the init and the result is the same.
I know that i can extend prototype, but i have my reasons to use like this.
Thanks.
I can think of three possible causes:
The code is being initialized before the DOM has been parsed.
Your HTML does not actually contain elements that will match these selectors.
The desired elements are being created dynamically after this code is being initialized.
In this declaration:
Controller.PROPERTIE = {
element: $('[data-controller]'),
indicator: $('[data-controller-to]'),
state: false
};
the two jQuery expressions will be evaluated immediately as this code is parsed. If this code is not placed at the very end of the body, then those elements will likely not exist yet.
The usual solution to this is to not initialize those elements in a static declaration, but to initialize them by calling a function after the page has been loaded.
If you really intend for them to be globals, then you could just do this:
$(document).ready(function() {
Controller.PROPERTIE = {
element: $('[data-controller]'),
indicator: $('[data-controller-to]'),
state: false
};
});
But, you will also have to make sure that you don't try to use them until after this code runs.
The only other possible cause I can think of is that your DOM does not contain elements that match your two selectors. It seems like it's likely one of these two issues. Since you haven't shown us either the overall page structure of the HTML you intend to match, it's impossible for us to anything other than tell you what causes it might be.
I have a feeling you are thinking the jQuery object is not defined because of this:
if (!Controller.PROPERTIE.element || !Controller.PROPERTIE.indicator) {
return 'Expected to find [data-controller] and [data-controller-to]';
}
You must remember that jQuery constructor $() will always return an object. If you really want to test if there's a selected item you must use the length property of the jQuery object.
if (!Controller.PROPERTIE.element.length || !Controller.PROPERTIE.indicator.length) {
//element or indicator not there.
return 'Expected to find [data-controller] and [data-controller-to]';
}

What are the nuances of observing a "property" that's actually derived by a getter?

I am using PolymerJS.
It looks like observing a property that is derived by a getter works.
For example, the following code seems to work:
<div>Direct binding to property: {{internalData.value}}</div>
<div>Binding to computed value: {{computedValue}}</div>
<div>Binding to observed value: {{observedValue}}</div>
<script>
var externalData = {
get value() {
return 'static value';
}
};
Polymer('my-element', {
internalData: externalData,
computed: {
computedValue: 'internalData.value'
},
observe: {
'internalData.value': 'valueChanged'
},
valueChanged: function() {
this.observedValue = this.internalData.value;
}
});
</script>
However, what if my getter defines something more complex? I have found that if the getter returns a new value on every call, then attempting this sort of binding will result my browser tab crashing (this is Chrome 39, so I believe it's a result of native object observation).
For example:
var externalData = {
get changingValue() {
return Math.random();
}
}
Why is this? What else should I be worried about if I attempt this pattern?
Here's a more-complete rundown of the different permutations of the problem:
http://jsbin.com/reder/28/edit?html,output
Note, btw, that this issue could come up more commonly than you think. If one returns an Object from a getter, it's easy to accidentally create a new one on each access, e.g.:
var externalData = {
get changingValue() {
return { foo: 'bar' };
}
}
Not a complete answer, but he's a start:
Consider when changingValueChanged runs. It runs every time changingValue changes.
Consider when changingValue changes. It changes every time it is accessed.
The final piece to understand the crash is that changingValueChanged accesses the changingValue:
this.observedChangingValue = this.internalData.changingValue;
When accessing the value for that assignment, you change the value and casue the listener to run again, which repeats forever.
It would appear that you cannot meaningfully observe a value that will always be different on each access. Not mention that trying to capture the value of that variable after you know it has changed is meaningless: the variable is functionally a generator. You can't ask the variable what value it just had, because it can only give you new values. On the other hand, you can observe a getter-based variable that simply changes occasionally, because it changes its value based on something other than the act of accessing the value.
If you change your listener to do something not related to the variable (e.g., simply do console.log("hello")) and never access this.internalData.changingValue, there is no infinite loop. Note that the new observed value is in the first argument provided to the change listener, so you can safely get the value that way . However, that's not the value that is ultimately displayed on the page by Polymer; it appears that Polymer does yet another access to read the value to put it into the DOM.
However, note that the change listener doesn't appear to run when you access externalData.changingValue in the console, but it does run when accessing externalData, which shows all the properties of the object.
I can't speak to why the compute property crashes the page, because I don't know what that does.

Force KO to update automatically

So I have two viewModels, one has a document style database in an observable:
var databaseViewModel = function () {
var self = this;
self.database = ko.observableArray([]).publishesTo("database");
}
var calcViewModel = function () {
var self = this;
self.replicatedDatabase = ko.observableArray([]).subscribeTo("database");
}
These get applied thusly:
ko.applyBindings({
databaseViewModel: new databaseViewModel(),
calcViewModel: new calcViewModel()
});
The only problem is, that the drop down box tied to replicatedDatabase doesn't show any items. I know I can force a binding update:
database.valueHasMutated();
But I don't know where and when.
I have tried after the ko.applyBindings however, I'm not sure how to access the newly created databaseViewModel. I've tried inside databaseViewModel after it has been created, but I believe that KO automatically updates the bindings when they've been binded, so not sure this actually makes a difference, it didnt on the dropdowns anyways.
I'm not really sure what I should be doing here.
For reference, I'm using knockout-postbox to do message bus style communications, subscribeTo and publishesTo. So as soon as the database observable is changed it will notify all subscribers, so I thought that maybe replicatedDatabase would have been update in the instance that databaseViewModel was initiated.
So, rather than force knockout to update the values I chose a different approach.
Realistically speaking the page would initially be populated with some data from a server, so with this in mind I proceeded by making a global variable holding the initial data:
var serverData = [{}];
Then just simply populate the observableArray's using Ryan Niemeyer mapping function:
ko.observableArray.fn.map = function ( data, Constructor) {
var mapped = ko.utils.arrayMap(data, function (item) {
return new Constructor(item);
});
this(mapped);
return this;
};
This way both viewModel's start off with the initial data, and when the database viewModel gets updated this permeates through to the other viewModel's

knockout computed dependency is empty initially

I have this viewmodel, on my web I have a dropdown that updates the sortedallemployees option. It works fine except my table is empty initially. Once I sort the first time I get data. Seems like when the vm is created it doesn't wait for allemployees to be populated.
var vm = {
activate: activate,
allemployees: allemployees,
sortedallemployees:ko.computed( {
return allemployees.sort(function(f,s) {
var ID = SelectedOptionID();
var name = options[ ID - 1].OptionText;
if (f[name] == s[name]) {
return f[name] > s[name] ? 1 : f[name] < s[name] ? -1 : 0;
}
return f[name] > s[name] ? 1 : -1;
});
}
Without the rest of your code, its difficult to tell exactly how this will behave. That being said, you are doing several very odd things that I would recommend you avoid.
First, defining all but the simplest viewmodels as object literals will cause you pain. Anything with a function or a computed will almost certainly behave oddly, or more likely not at all, when defined this way.
I would recommend using a constructor function for your viewmodels.
var Viewmodel = function(activate, allEmployees) {
var self = this;
self.activate = activate;
self.allEmployees = ko.observableArray(allEmployees);
self.sortedEmployees = ko.computed(function() {
return self.allEmployees().sort(function(f,s) {
//your sort function
});
});
};
var vm = new Viewmodel(activate, allemployees);
This method has several advantages. First, it is reusable. Second, you can reference its properties properly during construction, such as during the computed definition. It is necessary for a computed to reference at least one observable property during definition for it to be reactive.
Your next problem is that your computed definition is not a function, but an object. It isn't even a legal object, it has a return in it. This code shouldn't even compile. This is just wrong. The Knockout Documentation is clear on this point: computed's are defined with a function.
Your last problem is that your sort function is referencing things outside the viewmodel: SelectedOptionID(). This won't necessarily stop it from working, but its generally bad practice.

Categories

Resources