Story:
I have a lot of properties that needs to be set. Since they have similarities I choose to read out the class the inputbox is part of as property name.
Problem:
A dynamic property of an object can be set, just like an associative array. So I could do
var customattribute = this.$el.parents('div').attr('id');
var value = $(e.currentTarget()).val();
this.model.attributes[customattribute] = value;
But this wouldn't trigger a change event of the model. I could manually trigger a change event, but this wouldn't update this.model.changedAttributes(), I need to set only the changed attribute, not every attribute.
This of course doesn't work either:
this.model.set(customattribute: value);
So how would I handle this problem?
I have ALOT (200+) attributes that can be set, I wouldn't like to make separate eventlisteners for every attribute unless thats the only way.
Code:
var Display = Backbone.View.extend({
className: 'display',
events: {
'slide .slider' : 'sliderHandler'
},
initialize: function(){
_.bindAll(this, 'render', 'sliderHandler','update');
this.model.on('change',this.update, this);
},
render: function(){
this.$el.html(_.template(html, {}));
this.$('.slider').slider();
return this;
},
sliderHandler: function(e){
var slider = $(e.currentTarget);
var property = slider.parents('div').attr('id');
var value = slider.slider('value');
this.model.attributes[property] = value;
},
update: function(){
console.log(this.model.changedAttributes());
//get changed attribute + value here
},
});
Edit:
The two answers below solved it. Map the attributes to an object and give that to Backbone. Also I found another solution. Instead of an object, model.set() also accepts an array.
model.set(customattribute, value, customattribute2, value2);
I'm not sure that I fully understand your problem, but:
If you want to update an attribute, just:
this.model.set({
customattribute: value
});
If you want that the setting not trigger an event, you could pass a silent option, like this:
this.model.set({
customattribute: value
}, {silent:true});
I hope it helps you.
UPDATED:
Another way:
var map = {};
map[customattribute] = value;
this.model.set(map);
You need to do this
var attributeName = this.$el.parents('div').attr('id');
var value = $(e.currentTarget()).val();
var attribute = {};
attribute[attributeName] = value;
this.model.set(attribute);
if attribute was "test" and value 'value' it would be the same as
this.model.set({test: 'value'});
which is what correctly sets the attribute and propagates to your view thanks to model change event
As of Backbone 0.9.0 (Jan. 30, 2012), model.set (source) accepts a key, a value, and an options object.
this.model.set(attributeName, value, { silent: true });
is equivalent to
var attr = {};
attr[attributeName] = value;
this.model.set(attr, { silent: true });
Related
Hy I wrote a quick and dirty list ui in js and html that can be filtered:
https://jsfiddle.net/maxbit89/2jab4fa4/
So the usage of this looks like this (Fiddle line: 96):
var list = new ui_list(document.body, 200, 300, "Test");
var encoder = function(dom, value) {
console.log("begin encoding");
console.log(value)
dom.innerHTML = value.n;
}
list.add({'n': 1}, function() {
this.value.n++;
console.log(this.value.n);
// this.value = this.value;
}, encoder);
So what this basicaly does is create a List and adds a Element to it that has an Object: {'n': 1} as a value and a onClickHandler(second parameter on list.add) wich should increase the value by 1 (fiddle line: 104)
But it won't do this untill you uncomment the line 106 in the fiddle.
(Tested with FireFox 50.1.0, and Edge Browser)
Has any body an idea why js behaves like this?
In a much simplier example this works just fine:
var myObj= {
'onvalueChange' : function() {
console.log('value changed');
},
'print' : function() {
console.log('value:');
console.log(this.value);
console.log(this.value.n);
}
};
Object.defineProperty(myObj, "value", {
get: function() { return this._value; },
set: function(value) {
this.onvalueChange();
this._value = value;
}
});
myObj.value = {'n' : 1};
myObj.value.n++;
myObj.print();
First you have the setter defined like this:
set: function (value) {
this.encoder(this, value);
this._value = value;
}
that means that every time the value is set, the encoder will be called with the new value to update the equivalent DOM element.
But then inside the event listener function you have:
function() {
this.value.n++;
console.log(this.value.n);
//this.value = this.value;
}
where you think that this.value.n++ is setting the value (means it calls the setter which means the encoder will be called to update the DOM element). But it's not true. this.value.n++ is actually calling the getter. To explain more this:
this.value.n++;
is the same as:
var t = this.value; // call the getter
t.n++; // neither call the getter nor the setter. It just uses the reference (to the object) returned by the getter to set n
So, when you uncomment the line this.value = this.value;, the setter gets called, and the encoder gets called to update the DOM element.
So to fix the issue you have to either:
Make a call inside the getter to the encoder as you did for the setter (but this solution is very hacky as it will update the DOM element on every getter call even if nothing is being set).
Change this this.value.n++; to actually call the setter like: this.value = {n: this.value.n + 1}; (but this is hacky too as if value has a lot of key-value pairs then you have to enlist them all here just to set n).
Call the encoder inside the event listener which will be the best way to do it (or if you don't want to pass the parameters to it make another function (for example this.callEncoder()) that will call it and [you] call the new function instead inside the event listener).
I written a function, it will trigger whenever model attribute change just like in the following way.
modelEvents:{
"change:Name":"callback1"
},
callback1:function(){
console.log("This is testing");
}
Initially I set model.set("Name","hi") so automatically callback1 was invoked. Again If I set the same value into model, callback1 not triggering because model attribute not modified. so For this every time I am doing in the following.
model.set({"Name":""},{silent:true});
model.set({"Name":"hi"});
If I do like above it's working fine, but I want to know is there any option(like silent) to forcefully invoke callback.
Thanks.
If you want to go the route of passing an option then the only way to accomplish this would be to override the set method with something like this in your Model, although i haven't done testing on this to make sure it would not produce unexpected results.
set: function(key, val, options) {
//call the origonal set so everything works as normal
Backbone.Model.prototype.set.call(this, key, val, options);
current = this.attributes, prev = this._previousAttributes;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
//new option to always trigger the change on an attribute
if (options.loud) {
for (var key in attrs) {
//if the value is the same as before trigger the change
//otherwise backbone will have already triggered the chage
if (_.isEqual(prev[key] , attrs[key])) {
this.trigger('change:' + key, this, current[key], options);
}
}
}
}
then to make use of it call the normal set but pass loud: true
this.model.set({
name: "Hi"
}, {
loud: true
});
here a fiddle that makes use of it http://jsfiddle.net/leighking2/ktvj0kgp/
To show that the event is triggered even when the attribute is the same i have added an attribute called renders to show how many times it has been called.
Why don't you use Model.hasChanged for this? basically it will listen for changes in an attribute.
Take a look here.
http://backbonejs.org/#Model-hasChanged
Hope it helps
For some reason I cannot comprehend, events are not being listened to by my View. The model IS changing, but the view doesn't seem to acknowledge these changes. Here's my code.
var playerSet = 1;
var bone = function(){
var app = {};
app.BoardModel = Backbone.Model.extend({
defaults: function(){
return{
board:[0,0,0,0,0,0,0,0,0],
allDisabled: false,
p1Score: 0,
p2Score: 0
}
},
setSlot: function(slot, ct){
var b = this.get("board");
b[slot] = ct;
this.set("board", b);
console.log("CHANGED");
}
});
app.Board = new app.BoardModel;
app.BoardView = Backbone.View.extend({
el: $("#ttt-board"),
initialize: function(){
this.listenTo(app.Board, "change", this.renderBoard);
},
renderBoard: function(){
console.log("HELLO THERE");
}
});
var tictac = new app.BoardView;
app.Board.setSlot(0,1);
};
bone();
When I fire setSlot, the model does change as the console outputs CHANGED, however I never see the renderBoard function being called.
This is probably incredibly simple, but it eludes me.
Your problem is that you are only changing the internal components of the array object, not the attribute on your model. Even though you are manually calling set on the model, this is not an actual change and the set logic only triggers a change event if the equality check between the old and new values fails (which in your case it doesn't).
Since you are calling a custom function anyways, why not just use a custom event?
setSlot: function(slot, ct){
this.get("board")[slot] = ct;
this.trigger("custom:change:board", slot, ct);
}
Now listen for the custom event (or both) instead of just change.
initialize: function(){
this.listenTo(app.Board, "change custom:change:board", this.renderBoard);
}
Sure this is a very easy question to answer but is there an easy way to determine if any property of a knockout view model has changed?
Use extenders:
ko.extenders.trackChange = function (target, track) {
if (track) {
target.isDirty = ko.observable(false);
target.originalValue = target();
target.setOriginalValue = function(startingValue) {
target.originalValue = startingValue;
};
target.subscribe(function (newValue) {
// use != not !== so numbers will equate naturally
target.isDirty(newValue != target.originalValue);
});
}
return target;
};
Then:
self.MyProperty= ko.observable("Property Value").extend({ trackChange: true });
Now you can inspect like this:
self.MyProperty.isDirty()
You can also write some generic viewModel traversing to see if anything's changed:
self.isDirty = ko.computed(function () {
for (key in self) {
if (self.hasOwnProperty(key) && ko.isObservable(self[key]) && typeof self[key].isDirty === 'function' && self[key].isDirty()) {
return true;
}
}
});
... and then just check at the viewModel level
self.isDirty()
You can subscribe to the properties that you want to monitor:
myViewModel.personName.subscribe(function(newValue) {
alert("The person's new name is " + newValue);
});
This will alert when personName changes.
Ok, so you want to know when anything changes in your model...
var viewModel = … // define your viewModel
var changeLog = new Array();
function catchChanges(property, value){
changeLog.push({property: property, value: value});
viewModel.isDirty = true;
}
function initialiseViewModel()
{
// loop through all the properties in the model
for (var property in viewModel) {
if (viewModel.hasOwnProperty(property)) {
// if they're observable
if(viewModel[property].subscribe){
// subscribe to changes
viewModel[property].subscribe(function(value) {
catchChanges(property, value);
});
}
}
}
viewModel.isDirty = false;
}
function resetViewModel() {
changeLog = new Array();
viewModel.isDirty = false;
}
(haven't tested it - but you should get the idea)
Consider using Knockout-Validation plug-in
It implements the following:
yourProperty.isModified() - Checks if the user modified the value.
yourProperty.originalValue - So you can check if the value really changed.
Along with other validation stuff which comes in handy!
Cheers
You might use the plugin below for this:
https://github.com/ZiadJ/knockoutjs-reactor
The code for example will allow you to keep track of all changes within any viewModel:
ko.watch(someViewModel, { depth: -1 }, function(parents, child) {
alert('New value is: ' + child());
});
PS: As of now this will not work with subscribables nested within an array but a new version that supports it is on the way.
Update: The sample code was upgraded to work with v1.2b which adds support for array items and subscribable-in-subscribable properties.
I had the same problem, i needed to observe any change on the viewModel, in order to send the data back to the server,
If anyone still intersted, i did some research and this is the best solution iv'e managed to assemble:
function GlobalObserver(viewModel, callback) {
var self = this;
viewModel.allChangesObserver = ko.computed(function() {
self.viewModelRaw = ko.mapping.toJS(viewModel);
});
viewModel.allChangesObserver.subscribe(function() {
callback(self.viewModelRaw);
});
self.dispose = function() {
if (viewModel.allChangesObserver)
viewModel.allChangesObserver.dispose();
delete viewModel.allChangesObserver;
};
};
in order to use this 'global observer':
function updateEntireViewModel() {
var rawViewModel = Ajax_GetItemEntity(); //fetch the json object..
//enter validation code here, to ensure entity is correct.
if (koGlobalObserver)
koGlobalObserver.dispose(); //If already observing the older ViewModel, stop doing that!
var viewModel = ko.mapping.fromJS(rawViewModel);
koGlobalObserver = new GlobalObserver(viewModel, Ajax_Submit);
ko.applyBindings(viewModel [ ,optional dom element]);
}
Note that the callback given (in this case 'Ajax_Submit') will be fired on ANY change that occurs on the view model, so i think it's really recommended to make some sort of delay mechanism to send the entity only when the user finished to edit the properties:
var _entitiesUpdateTimers = {};
function Ajax_Submit(entity) {
var key = entity.ID; //or whatever uniquely related to the current view model..
if (typeof _entitiesUpdateTimers[key] !== 'undefined')
clearTimeout(_entitiesUpdateTimers[key]);
_entitiesUpdateTimers[key] =
setTimeout(function() { SendEntityFunction(entity); }, 500);
}
I'm new to JavaScript and the knockout framework, (only yestarday i started to work with this wonderfull framework), so don't get mad at me if i did something wrong.. (-:
Hope this helps!
I've adapted #Brett Green code and extended it so that we can have AcceptChanges, marking the model as not dirty plus having a nicer way of marking models as trackables. Here is the code:
var viewModel = {
name: ko.observable()
};
ko.track(viewModel);
http://jsfiddle.net/david_freire/3HZEu/2/
I did this by taking a snapshot of the view model when the page loads, and then later comparing that snapshot to the current view model. I didn't care what properties changed, only if any changed.
Take a snapshot:
var originalViewModel = JSON.stringify(ko.toJS(viewModel));
Compare later:
if(originalViewModel != JSON.stringify(ko.toJS(viewModel))){
// Something has changed, but we don't know what
}
Consider a view model as follows
function myViewModel(){
var that = this;
that.Name = ko.observable();
that.OldState = ko.observable();
that.NewState = ko.observable();
that.dirtyCalcultions - ko.computed(function(){
// Code to execute when state of an observable changes.
});
}
After you Bind your Data you can store the state using ko.toJS(myViewModel) function.
myViewModel.Name("test");
myViewModel.OldState(ko.toJS(myViewModel));
You can declare a variable inside your view model as a computed observable like
that.dirtyCalculations = ko.computed(function () {});
This computed function will be entered when there is change to any of the other observables inside the view model.
Then you can compare the two view model states as:
that.dirtyCalculations = ko.computed(function () {
that.NewState(that);
//Compare old state to new state
if(that.OldState().Name == that.NewState().Name()){
// View model states are same.
}
else{
// View model states are different.
}
});
**Note: This computed observable function is also executed the first time when the view model is initialized. **
Hope this helps !
Cheers!!
I like Brett Green's solution. As someone pointed out, the isDirty comparison doesn't work with Date objects. I solved it by extending the subscribe method like this:
observable.subscribe(function (newValue) {
observable.isDirty(newValue != observable.originalValue);
if (newValue instanceof Date) {
observable.isDirty(newValue.getTime() != observable.originalValue.getTime());
}
});
I am trying to fill my view model with some drop down options from the database. I want to keep track of the selected object because it has properties on it that I am using in a custom binding elsewhere.
I initialize the observable with a "blank" value so that it's properly set when the binding occurs and my custom binding works. Once the server responds, I morph the data to an observable array and the drop down list displays the options.
Here is the JavaScript code:
ko.bindingHandlers.jq_fade = {
init: function (element, valueAccessor) {
// Initially set the element to be instantly visible/hidden depending on the value
var value = valueAccessor();
$(element).toggle(ko.utils.unwrapObservable(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable
},
update: function (element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the element in or out
var value = valueAccessor();
ko.utils.unwrapObservable(value) ? $(element).fadeIn() : $(element).fadeOut();
}
};
function Firm(id, name, outsideCounsel) {
this.name = name;
this.id = id;
this.outsideCounsel = outsideCounsel;
}
function RequestViewModel() {
var self = this,
ONE_DAY = 1000 * 60 * 60 * 24;
self.firm = ko.observable(new Firm(-1, "", false));
$.post(ajaxAddress + 'LoadFirms', function (data) {
var mappedFirms = $.map(data.d, function (item) {
return new Firm(item.OrganizationLookupID, item.OrganizationLookupName, item.IsExternalCounselFirm);
});
self.availableFirms(mappedFirms);
self.firm(self.availableFirms()[0]);
});
}
$(document).ready(function () {
model = new RequestViewModel();
ko.applyBindings(model);
});
Here is the relevant HTML
<span data-bind="jq_fade: firm().outsideCounsel">
Outside Counsel
<input type="text" id="outsideCounsel" data-bind="value: outsideCounsel" />
</span>
I want that DIV to show only if the selected firm is an outside counsel. If remove the line data-bind="jq_fade: firm().outsideCounsel, everything works fine. If I make the $.post() calls synchronously, it works. I'm thinking it's something wrong with my init function in jq_fade.
The error I receive is:
Uncaught Error: Unable to parse bindings.
Message: TypeError: Cannot call method 'outsideCounsel' of undefined;
Bindings value: jq_fade: firm().outsideCounsel()
I understand what Knockout is telling me, I'm just not sure how firm() can ever be undefined because I set up an initial value.
If you're binding availableFirms() to a dropdownlist, I'm assuming you've also bound firm() to the same list so that when another is selected from the list, firm() gets automatically updated and all your bindings update automatically.
If this is the case, you do not need to set firm() at all as it will be set to the first element in the dropdownlist anyway.
See example 3 here:
http://knockoutjs.com/documentation/options-binding.html
var viewModel = {
availableCountries : ko.observableArray([
new country("UK", 65000000),
new country("USA", 320000000),
new country("Sweden", 29000000)
]),
selectedCountry : ko.observable() // Nothing selected by default
};
Try it like above without specifically setting firm() and see if it errors again