Backbone.js: smart update of a list of changed attributes - javascript

I know this problem have been discussed several times, but I could not find an answer to one of its many aspects, that I'll try to explain here.
Model save() keeps sync between client and server. Backbone.js pushes you to use it in one of these 2 ways (afaik):
Save all the items of a model: this will send everything to the
server, even attributes used only on the client side and attributes
that have not been changed. This is the default for new models (when
model.isNew() returns true).
Save with patch:true will only send changed attributes of the
model to the server. In Backbone, it means that ONLY the
attributes returned by changedAttributes() will be sent.
The point is that changedAttributes() ONLY returns the changes since the LAST change event. This is useful for models that are not updating all the time, and actually can afford to automatically make an ajax request on any change.
But if you have a model that is constantly changing (for example a code/text editor or an element that can be dragged/dropped and its position should be tracked), you cannot just save at every single change event. You need to save at time intervals, ALL the changed attributes since the last time you called save().
Do you think Backbone.js actually provides a good support for this kind of synchronization? Does Bakcbone.js track changes since last save() (and not only since last change event)? Or you have to do it "manyally"?

You'll have to extend backbone models with your own base model.
var BaseModel = Backbone.Model.extend({
save: function(key, val, options){
var attrs
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
attrs = _.extend(this.unsaved || {}, attrs);
this.unsaved = {};
return Backbone.Model.prototype.save.call(this, attrs, options);
},
set: function(key, val, options) {
var attr
if (key == null) return this;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
this.unsaved = _.extend(this.unsaved || {}, attrs);
return Backbone.Model.prototype.save.call(this, attrs, options);
}
});
Totally untested code, but should be very close to what you need to do.

Related

Knockoutjs valueHasMutated not working correctly

Hopefully this will be a quick one for a knockout guru....
I'm writing a couple of custom bindings to help me translate a UI using a custom translation engine in the project I'm working on.
One is to translate text, the other is to translate the 'placeholder' attribute on HTML5 input elements.
Both bindings are identical apart from the last statement, where one updates the text in the element and the other updates an attribute value.
The text one works perfectly, but the place holder one does not, and I'm stuck for an answer as to why.
The binding code is as follows:
Translated Text Binding
ko.bindingHandlers.translatedText = {
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Set up an event handler that will respond to events telling it when our translations have finished loading
// the custom binding will instantly update when a key matching it's translation ID is loaded into the
// local session store
window.addEventListener("TranslationsLoaded", (e) => {
//associatedObservable(" "); // Force an update on our observable, so that the update routine below is triggered
associatedObservable.valueHasMutated();
}, false);
},
update: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Ask local storage if we have a token by that name
var translatedText = utilityLib.getTranslatedString(translationToken);
// Check if our translated text is defined, if it's not then substitute it for a fixed string that will
// be seen in the UI (Whatever you put into the 'associatedObservable' at this point WILL appear in the element
if (undefined === translatedText || translatedText === "" || translatedText === null) {
if (sessionStorage["translations"] === undefined) {
// No translations have loaded yet, so we blank the text
translatedText = "";
} else {
// Translations have loaded, and the token is still not found
translatedText = "No Translation ID";
}
}
associatedObservable(translatedText);
ko.utils.setTextContent(element, associatedObservable());
}
} // End of translatedText binding
Translated Placeholder Binding
ko.bindingHandlers.translatedPlaceholder = {
// This one works pretty much the same way as the translated text binding, except for the final part where
// the translated text is inserted into the element.
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
window.addEventListener("TranslationsLoaded", (e) => {
debugger;
associatedObservable.valueHasMutated();
}, false);
},
update: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
var translatedText = utilityLib.getTranslatedString(translationToken);
debugger;
if (undefined === translatedText || translatedText === "" || translatedText === null) {
if (sessionStorage["translations"] === undefined) {
translatedText = "";
} else {
translatedText = "No Translation ID";
}
}
associatedObservable(translatedText);
element.setAttribute("placeholder", translatedText);
}
} // End of translatedPlaceholder binding
The idea is a simple one, if the binding runs and the translations are already present in sessionStorage, then we pick up the translated string and plug it in to the observable associated with the element.
If the translations have loaded, but the translation is not found "No Translation ID" is plugged into the observable bound to the element.
If the translations have NOT yet loaded, plug an empty string into the observable, then wait for the event 'TranslationsLoaded' to fire. When this event is raised, the observable bound to the element is mutated, causing an update to happen, which in turn re-checks the translations, which it then finds have loaded and so acts accordingly.
However.....
It doesn't matter how hard I try, the translated placeholder binding just will not fire it's update.
I can clearly see in the debugger that the event is recieved on both bindings, and the mutate function IS called.
On the translated text binding, I get the following sequence...
'init' -> 'update' -> 'event' -> 'mutate' -> 'update'
Which is exactly what I expect, and it occurs on every element+observable bound to that binding.
On the translated placeholder i get
'init' -> 'update' -> 'event' -> 'mutate'
but the final update never occurs.
As a result, the translated string for the placeholder is never looked up correctly, the text one with identical code works perfectly!!
For those who'll ask, i'm using the bindings like this:
<input type="text" class="form-control" data-bind="value: userName, translatedPlaceholder: { observable: namePlaceHolderText, translationToken: 'loginBoxNamePlaceholderText'}">
<span class="help-block" data-bind="translatedText: {observable: nameErrorText, translationToken: 'loginBoxUserNameEmptyValidationText'}"></span>
and inside the view model, the 'observable' parameters are just normal ko.observable variables holding strings.
Cheers
Shawty
I believe you have event bubbling issues... try putting a 'return true' after your call to valueHasMutated like this:
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
window.addEventListener("TranslationsLoaded", (e) => {
associatedObservable.valueHasMutated();
return true; // allow event to bubble
}, false);
},
That being said, I am thinking all this manual eventing is against-the-grain for what knockout can do for you. You should be observing your data, binding to it, and letting knockout do all the internal eventing for you... that's what it does.
An example building on user3297291's fiddle:
ko.bindingHandlers.translatedPlaceholder = {
init: function(element, valueAccessor) {
var va = valueAccessor();
var obs = ko.utils.unwrapObservable(va.obs);
var placeholderStr = obs[va.key];
console.log(placeholderStr);
element.setAttribute("placeholder", placeholderStr);
},
update: function(element, valueAccessor) {
var va = valueAccessor();
var obs = ko.utils.unwrapObservable(va.obs);
var placeholderStr = obs[va.key];
console.log(placeholderStr);
element.setAttribute("placeholder", placeholderStr);
}
};
var vm = function() {
var self = this;
self.dictionary = ko.observable({
"placeholder": "Initial State"
});
self.switchTranslations = function() {
// Set the 'new' dictionary data:
self.dictionary({
"placeholder": "My Translated Placeholder"
});
};
}
ko.applyBindings(new vm());
Fiddle: https://jsfiddle.net/brettwgreen/5pmmd0va/
Trigger the update handler
From the Knockout documentation:
Knockout will call the update callback initially when the binding is applied to an element and track any dependencies (observables/computeds) that you access. When any of these dependencies change, the update callback will be called once again.
The key is that you have to access the observable within the update function to get updates. In your translatedText binding you do so:
ko.utils.setTextContent(element, associatedObservable());
But there is no such access of associatedObservable in the update function of translatedPlaceholder. You'll need to add it like this:
associatedObservable(translatedText);
associatedObservable(); // get notified of updates to associatedObservable
element.setAttribute("placeholder", translatedText);
A better approach
For your case, there really isn't a need for an update handler because you don't need to update the view based on changes to the viewmodel. Instead your updates just come from events, which can be set up in the init handler.
ko.bindingHandlers.translatedPlaceholder = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
function loadTranslation() {
var translationToken = valueAccessor(),
translatedText = utilityLib.getTranslatedString(translationToken);
element.setAttribute("placeholder", translatedText || "No Translation ID");
window.removeEventListener("TranslationsLoaded", loadTranslation);
}
if (sessionStorage["translations"] === undefined)
window.addEventListener("TranslationsLoaded", loadTranslation, false);
} else {
loadTranslation();
}
}
}
Usage:
data-bind="value: userName, translatedPlaceholder: 'loginBoxNamePlaceholderText'"

Knockout: Rules for custom binding triggers firing update method

Can someone tell me why this works?
<div id="test">
<label data-bind="dateText: TheDate"></label>
</div>
ko.bindingHandlers.dateText = {
update:
function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
{
// Get current value of supplied value
var val = ko.unwrap(f_valueaccessor());
console.log('dt is ' + (typeof(val.dt) == 'function' ? '' : 'NOT ') + 'observable');
var dt = ko.unwrap(val.dt);
$(el).html(dt ? moment(dt).format('DD/MM/YYYY') : '');
}
};
function Model(data, mapping)
{
var _this = this;
if(data)
ko.mapping.fromJS(data, mapping, _this);
this.TheDate = this.TheDate || { dt: ko.observable() }
}
var model = new Model({ TheDate: { dt: null } });
ko.applyBindings(model);
model.TheDate.dt('2001-01-01T01:01:01');
Fiddle: http://jsfiddle.net/qkLzc5u5/2
The reason I ask is that I am having problems making this custom binding work in a real application - there, it displays any initial date fine but updates are not reflected in the text - however the IsDated computed does update, so the label is displayed; just blank.
I thought that the problem was that the binding is attached to TheDate, which is not an observable itself; only dt is.
However, the code above will in fact fire the update, so the date 01/01/2001 is shown, whereas I was expecting nothing to be displayed due to the null initial value.
Now this is actually exactly the behaviour I want, so I don't have a problem with this code - I am hoping to get a bit more of an insight into how the custom bindings know when to fire so I can debug my real world code better.
(Note that the object model for passing dates is fixed by a web service that I cannot change)

How to invoke model events forcefully

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

Update ordered list when array changes in Javascript

So I've used Backbone and Angular quite a bit and gotten used to data binding / view updating within those ecosystems, but I don't know how I would achieve this in plain JS (no frameworks/libraries).
Right now I have a simple UserList, and I would like to watch for changes to it and trigger and update of an unordered list when it happens.
var ContactList = {
list: [],
push: function(obj) {
this.storage.push(obj);
},
remove: functon(obj) {
return this.storage.splice(this.storage.indexOf(obj), 1);
}
};
var Contact = function(attributes) {
this.attributes = attributes || {};
};
Contact.prototype.get = function(property) {
return this.attributes[property];
};
Contact.prototype.set = function(property, value) {
this.attributes[property] = value;
};
Ideally the following would automatically add to a list. I could just add a callback to the push and remove methods, but that seems like it doesn't scale very well if I get to a point where I'm adding more methods to operate on my list. I've been reading a bit about the observer pattern, but not sure if that's really what I'm looking for here.
You don't want to pass the callback to every call of ContactList.push and ContactList.remove and all the ContactList methods you are yet to write. Instead, the ContactList will know when he has changed and then announce that fact to the world. In a simple implementation, ContactList could have his own onChange method which he could call:
var ContactList = {
list: [],
push: function (obj) {
this.list.push(obj);
this.onChange();
},
remove: function (obj) {
var index = this.list.indexOf(obj);
if (index > -1) {
var removed = this.list.splice(index, 1);
this.onChange();
return removed;
} else {
return null;
}
}
};
You would then, obviously, have to define ConactList.onChange:
ContactList.onChange = function () {
console.log(this.list);
/* update your ul element */
};
This solution will not allow you to add subscribers dynamically to Contact List's change event, but it might be a helpful starting point.

Detecting change to Knockout view model

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());
}
});

Categories