Understanding Backbone Model set, validate and change callbacks - backbone.js

The Backbone documentation says:
Model.set will fail if validation fails - it won't set the value therefore it won't trigger any callback. We can pass { silent: true } to Model.set - then it will set the value but won't trigger any callback neither.
So,
Why does Backbone Model require a valid state to simply set an attribute value? What if we want to set attributes as the user interacts with the UI, but the model is not valid yet? It means change callbacks are unavailable unless we pass { silent: true } then manually trigger the change?!
Please say you know a better way of handling this :)

I'm not sure how to answer the Why questions but you could say that there are arguments for why it is good that set runs validations. For instance, it makes it dead simple to do client side validation in real time.
If your problem could be solved by only validating the value that is currently being changed by the user, you can do that by combining your validate method with the hasChanged method.
For example something like this:
Backbone.Model.extend({
defaults : { name : "" },
validate : function (attrs) {
var errors = {};
if(this.hasChanged("name") && attr.name.length == 0) {
errors.name = "Need a name yo!";
}
//...
if(_.keys(errors).length > 0) {
return errors;
}
}
})

In Backbone whenever you call set on model , It keeps track of what attributes of model has been changed and what attributes are newly added.Calling validate allows it be more efficient in doing it .Passing {silent:true} as options in set function causes validate and change to not execute so if doesnt fire any change events.
If you want to set attributes as the user interacts with the UI, but the model is not valid yet
In this case you can set the change in a plain object make sure object keys are sames as model's attribute and then at some point just set in your model.
var uiChanges = {name:'x'}; //just fill it with your changes
ur_model.set(uiModel); //then set it ,this way it fires change events only once
To check the diff between your plain object and model you can use the
ur_model.changedAttributes(uiChanges);
changedAttributes -
Return an object containing all the attributes that have changed, or false if there are no changed attributes.
You can further use it save only those attributes that have changed rather than saving entire model again.

Related

Why does binding trigger a $watch although no data was changed?

I have an entity in the scope that is being edited by the user. Each time it gets modified, I want to trigger some custom validation. So I have:
// validate the position if anything has changed
$scope.$watch("entity", function() {
if ($scope.entity.Id) {
$scope.validate();
}
}, true /* watch "by value" (see: https://docs.angularjs.org/guide/scope#scope-life-cycle) */);
So far so good. Now, since this is a big entity with quite some fields, not all of the fields participate initially in data-binding. Only portions of the fields are visible to the user by using a tab control. When the user switches tabs, another portion of the entity is shown.
However, when the additional controls get bound to the corresponding fields of the entity, the $watch gets triggered even though the binding doesn't change any value on the entity.
Why is this the case and how could I prevent it?
Note:
I thought that the data-binding is possibly adding some internal $... fields to entity but these are hopefully disregarded (at least this is the case in angular.equals so they should probably be disregarded in the $watch too, I assume).
$watch with object equality flag true compares the properties of the old object copy with the current object. Therefore the listener fires when a property name is changed, added, or removed. You can try something like the following:
$scope.$watch("entity", function(newVal, oldVal) {
if(Object.keys(newVal).length !== Object.keys(oldVal).length)
return; //Detect added properties
if ($scope.entity.Id) {
$scope.validate();
}
}, true

Set model value programmatically in Angular.js

I'm an author of angular-input-modified directive.
This directive is used to track model's value and allows to check whether the value was modified and also provides reset() function to change value back to the initial state.
Right now, model's initial value is stored in the ngModelController.masterValue property and ngModelController.reset() function is provided. Please see the implementation.
I'm using the following statement: eval('$scope.' + modelPath + ' = modelCtrl.masterValue;'); in order to revert value back to it's initial state. modelPath here is actually a value of ng-model attribute. This was developed a way back and I don't like this approach, cause ng-model value can be a complex one and also nested scopes will break this functionality.
What is the best way to refactor this statement? How do I update model's value directly through the ngModel controller's interface?
The best solution I've found so far is to use the $parse service in order to parse the Angular's expression in the ng-model attribute and retrieve the setter function for it. Then we can change the model's value by calling this setter function with a new value.
Example:
function reset () {
var modelValueSetter = $parse(attrs.ngModel).assign;
modelValueSetter($scope, 'Some new value');
}
This works much more reliably than eval().
If you have a better idea please provide another answer or just comment this one. Thank you!
[previous answer]
I had trouble with this issue today, and I solved it by triggering and sort of hijacking the $parsers pipeline using a closure.
const hijack = {trigger: false; model: null};
modelCtrl.$parsers.push( val => {
if (hijack.trigger){
hijack.trigger = false;
return hijack.model;
}
else {
// .. do something else ...
})
Then for resetting the model you need to trigger the pipeline by changing the $viewValue with modelCtrl.$setViewValue('newViewValue').
const $setModelValue = function(model){
// trigger the hijack and pass along your new model
hijack.trigger = true;
hijack.model = model;
// assuming you have some logic in getViewValue to output a viewValue string
modelCtrl.$setViewValue( getViewValue(model) );
}
By using $setViewValue(), you will trigger the $parsers pipeline. The function I wrote in the first code block will then be executed with val = getViewValue(model), at which point it would try to parse it into something to use for your $modelValue according the logic in there. But at this point, the variable in the closure hijacks the parser and uses it to completely overwrite the current $modelValue.
At this point, val is not used in the $parser, but it will still be the actual value that is displayed in the DOM, so pick a nice one.
Let me know if this approach works for you.
[edit]
It seems that ngModel.$commitViewValue should trigger the $parsers pipeline as well, I tried quickly but couldn't get it to work.

Backbone Validation with Backbone stickit - All attributes being validated when one is changed

I am attempting to use Backbone Validation with Backbone Stickit, I wish to validate one attribute at a time as the user enters them. However, when a user enters a value all attributes on the model get validated instead of just the one the user has changed. What am I doing wrong?
My View:
bindings:{
'#username' : {
observe:'username',
setOptions: {
validate:true
}
},
'#email' : {
observe:'email',
setOptions: {
validate:true
}
},
'#firstname' : {
observe:'firstName',
setOptions: {
validate:true
}
},
.......
onShow: function(){
Backbone.Validation.bind(this, {
valid: function(view, attr) {
alert('VALID - ' + attr);
},
invalid: function(view, attr, error) {
alert('INVALID - ' + attr);
}
});
this.stickit();
},
Everything you pass through setOptions is used when setting the value in the model (1). When you pass validate: true to the set function of a Backbone model it will validate the values in the model as well as the values passed to the set function (2) meaning it will validate the whole model every time you set a new value causing the problem you're seeing now. You're not doing anything wrong.
You could probably solve this by splitting up your validation into multiple separate functions and calling only the required ones on attribute change and then changing the validate function to call all those separate functions to validate the entire model.
This happened to me as well. In my case, I was setting default values in the model as '' (blank). Removed them and it worked
For this to work you should remove defaults (at least for the attributes your validating) values from your model
Try using the backbone.validation forceUpdate parameter on your backbone.stickit setOptions object in yout view bindings. That worked for me and I had a kind of similar problem.
Just like yousefcisco mentioned, backbone will validate all the attributes in the model on set or save, depending of your options passed, but in my case is not that I needed to validate each attribute separately, but the attributes didn't get set even if only one attribute was invalid, then I tried the forceUpdate: true, and it did its magic.
check it here: http://thedersen.com/projects/backbone-validation/#configuration/force-update

How can I call validate on no change in Backbone?

Set data that will fail validation
Model.set({phoneNumber : 'meow meow'})
Validation that applies a border color
validate: function(attr){
if(attr.phoneNumber){
var phoneNumber = attr.phoneNumber,
regex = /^\([0-9]{3}\)\s[0-9]{3}-[0-9]{4}/;
if(!regex.test(val)){
View.showError('businessNumber');
}
}
Later the user focus on the input field, I remove the error styling
The user doesn't change anything and the blur event is fired
Validation doesn't run because nothing changed.
Do not reference your view directly from your model. This is a completely deep and fundamental violation of the core tenet of the Model/View/* design. When you call set and validation fails, the model will emit an error event (as of Backbone 0.9.9) which your view should listen for and respond to by updating the view accordingly. You may also alternately pass a callback to set to handle the error, but events are the better choice in most situations. Note that your model needs to actually return an error object from validate, which will refuse to update the data, so after the user fixes the input and blur occurs, the data will actually change. With your code as is, Backbone thinks the validate call succeeded since no error is returned.
In your view, here's some pseudocode showing how to translate the model's error object into UI warnings:
initialize: function () {
_.bindAll(this);
this.model.on('error', this.showError);
},
showError: function (error) {
if (error.businessNumber) {
this.$businessNumber.addClass('error');
}
}
As a side note, don't feel bad about being confused with how to use backbone for forms. The fact that out of the box set won't accept invalid data is a huge stumbling block and non-intuitive. There are plugins to help with both forms and validation (https://github.com/documentcloud/backbone/wiki/Extensions,-Plugins,-Resources), but out of the box this is not one of the obvious/easy parts of backbone.

Saving only one attribute

I have a model for which I would like to save only the attribute title. This is what I have tried:
myBook.model.save(['title']);
The problem is that request.body is the whole myBook.toJSON() object, instead of just the relevant attribute title. Is that by design, or am I doing something stupid?
It's by design.
save calls Backbone.sync to persist changes to your backend, which in turn does, among other things:
if (!params.data && model && (method == 'create' || method == 'update')) {
params.contentType = 'application/json';
params.data = JSON.stringify(model.toJSON()); // <-- jsonifies the entire model
}
There are plenty of ways to override this behavior. You can give your model a sync method, in which case it will be called instead of Backbone's default sync. Or you could just override Backbone.sync to do what you want.
However, most server-side frameworks will be able to handle receiving the full JSON object and only updating the changed content. Why do you need to only send the changed attribute to the server?
Side note: the first parameter to save should be a hash of attributes: so {title: newBookTitle} as opposed to ['title']. But I'm guessing that was probably just a quick example typo.

Resources