Backbone.js - validating model constructor parameters - validation

I want to do simple thing with a model:
use constructor parameters if they are valid (but not just assign them, I have to map them before as they are in a different format)
in other case, use defaults
What's the recomended solution for this?

You can achieve this by calling isValid() in initialize. If it's valid, then proceed as normal; otherwise, clear the model and reset it with the default values:
initialize: function() {
console.log("initializing model...");
if (!this.isValid()) {
console.log("Model is not valid, using defaults");
this.clear({ silent: true });
this.set(this.defaults, { silent: true });
}
console.log("Model is valid");
},
See this working demo.
Edit
It's possible to use objects in the model constructor, for example:
var model = new Backbone.Model({
title: "test",
hsa: {
h: 120,
s: "100%",
a: "50%"
}
});
You could also specify it as hsa: "120, 100%, 50%" or something, and convert that value to an object in the constructor.
See here.

Related

RxJS Filtering and selecting/deselecting list of checkbox

I have a list of checkboxes, and I need to be able to select them ( check them) and I should also be able to filter them and check them while they are filtered. I am able to select the item, filter the item, but as soon as I filter them and then check any value it unchecks the previously selected value. I know the reason why it unchecks because every time user checks/unchecks the value I start with the original checkbox value set. So when I check/uncheck a value in filtered set, it starts again with default set where checkbox value is set to false.
Any suggestions on how to make it work? That is works seamlessly with filtering and checking/unchecking the values.
It looks like a really simple issue but i am stuck with it from past few days. Replicated the issue here. Please take a look. Any suggestions are appreciated .
https://stackblitz.com/edit/angular-wmyxtd
variables = [
{
"label": "Phone",
"value": "phone",
checked: false
},
{
"label": "Machine Id",
"value": "machine_id",
checked: false
},
{
"label": "Address",
"value": "address",
checked: false
},
{
"label": "Store",
"value": "store",
checked: false
},
{
"label": "Email",
"value": "email",
checked: false
},
{
"label": "Name",
"value": "name",
checked: false
},
{
"label": "Credit Card",
"value": "credit_Card",
checked: false
},
{
"label": "Bank Account",
"value": "bank_account",
checked: false
}
]
variables$ = of(this.variables).pipe(shareReplay(1),delay(1000));
filteredFlexibleVariables$ = combineLatest(this.variables$, this.filterBy$).pipe(
map(([variables, filterBy]) => {
return variables.filter((item) => item.value.indexOf(filterBy) !== -1);
})
);
toggleStateSelectedVariables$ = combineLatest(
this.variables$,
this.toggleFlexibleVariableBy$
).pipe(
map(([availableVariables, clickedVariable]) => {
const clickedVariableValues = clickedVariable.map((item) => item.value);
if (clickedVariable.length) {
// If condition just to save availableVariables iteration for no reason if there is no clicked variables
return availableVariables.map((variable) => {
const checked = clickedVariableValues.indexOf(variable.value) !== -1;
return {
...variable,
checked
};
});
}
return availableVariables;
}),
);
flexibleVariables$ = merge(
this.filteredFlexibleVariables$,
this.toggleStateSelectedVariables$
);
I'd take a different approach. As you're trying to edit a group of values, this makes me think that using a form would be a good idea.
I'm one of the authors of ngx-sub-form and I'll explain the approach I would take using this library to (hopefully?) simplify the workflow and encapsulate the edition of the data then just be warned whenever the form value changes.
Before we start, here's a stackblitz you can play with to see my solution:
https://stackblitz.com/edit/angular-nhnmcm
First, I'd like to point out that the filter should not be tight to the data themselves. It should just show or hide data in the view based on the filter. For that, we can use a pipe:
filter-item.pipe.ts
#Pipe({
name: "filterItem"
})
export class FilterItemPipe implements PipeTransform {
public transform(item: Item, searchFilter: string): Item {
return item.value.includes(searchFilter);
}
}
And before I forget, here's what an "Item" looks like:
export interface Item {
label: string;
value: string;
checked: boolean;
}
Now, within our main component we don't want to be aware of how the data are being displayed nor visualized. The only thing which matters is:
Where do we get our data from?
When do we update them?
app.component.ts
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
public items$: Observable<Item[]> = this.itemsService.items$;
public search: string = "";
constructor(private itemsService: ItemsService) {}
public itemsUpdated(items) {
this.itemsService.updateItems(items);
}
}
Then, from the view of the main component, we should have 2 components:
One to tell us what's the value of the filter
One to display the list of items and tell us when it's been updated
app.component.html
<app-items-filter (search)="search = $event"></app-items-filter>
<app-items-form
[items]="items$ | async"
(itemsUpdated)="itemsUpdated($event)"
[searchFilter]="search"
></app-items-form>
So far, it doesn't look too bad, does it?
But how should we build the app-items-form which seems to be holding all the magic?
items-form.component.ts
interface ItemsForm {
items: Item[];
}
#Component({
selector: "app-items-form",
templateUrl: "./items-form.component.html",
styleUrls: ["./items-form.component.css"]
})
export class ItemsFormComponent extends NgxAutomaticRootFormComponent<
Item[],
ItemsForm
> {
#DataInput()
#Input("items")
public dataInput: Item | null | undefined;
#Output("itemsUpdated")
public dataOutput: EventEmitter<Item> = new EventEmitter();
#Input()
searchFilter: string;
protected getFormControls(): Controls<ItemsForm> {
return {
items: new FormArray([])
};
}
protected transformToFormGroup(obj: Item[]): ItemsForm {
return {
items: obj
};
}
public getDefaultValues(): Partial<ItemsForm> {
return {
items: []
};
}
protected transformFromFormGroup(formValue: ItemsForm): Item[] | null {
return formValue.items;
}
}
It may look like a lot is going on there if you haven't discovered ngx-sub-form yet. So let me walk you through it.
We use a top level class called NgxAutomaticRootFormComponent. That class will ask us to implement the form which is going to hold our data and will automagically bind the form to our input and give us the updated value as an output every time the form is being updated.
It might be slightly easier but as it's an array we need to wrap the array into an object as ngx-sub-form only wants to deal with a FormGroup internally. But that formGroup can contain a FormArray (which is what we just did there).
Now, let's take a look to the view:
items-form.component.html
<ng-container [formGroup]="formGroup">
<ng-container [formArrayName]="formControlNames.items">
<app-item-ctrl [hidden]="!(itemCtrl.value | filterItem:searchFilter)"
*ngFor="let itemCtrl of formGroupControls.items.controls" [formControl]="itemCtrl">
</app-item-ctrl>
</ng-container>
</ng-container>
Few important things to notice:
- ngx-sub-form gives us access to a formGroup property that we can use directly without having to declare it ourselves
- as we're holding a list of values, we use the formArrayName directive to work with the FormArray
- we use the hidden directive provided natively by Angular to hide the controls which are not included by our filter
- last, we delegate the display/edit of the item to a sub component called app-item-ctrl
This is the "magic" part of ngx-sub-form. It's really easy to break down a form into sub form and also at the top level abstract how it's been displayed/edited.
Final part, let's take a look into the sub component:
item-ctrl.component.ts
#Component({
selector: "app-item-ctrl",
templateUrl: "./item-ctrl.component.html",
styleUrls: ["./item-ctrl.component.css"],
providers: subformComponentProviders(ItemCtrlComponent)
})
export class ItemCtrlComponent extends NgxSubFormComponent<Item> {
protected getFormControls(): Controls<Item> {
return {
label: new FormControl(),
value: new FormControl(),
checked: new FormControl()
};
}
}
Things to notice:
providers: subformComponentProviders(ItemCtrlComponent): behind the scenes, ngx-sub-form is a simple wrapper to deal with a ControlValueAccessor in an easier way and with a lot less boilerplate. Hence, we need to register the DI tokens required by a ControlValueAccessor through that simple function where you just pass the class of the current component
getFormControls this is the method where we have to pass an object that'll be used to create our internal formGroup
And the corresponding view:
item-ctrl.component.html
<div [formGroup]="formGroup">
{{ formGroupValues.label }}
<input type="checkbox" [formControl]="formGroupControls.checked" />
</div>
Conclusion:
When you need to edit data, I'd recommend using a form. Whether you use ngx-sub-form or decide to use forms directly is up to you.
If I'm explaining how to use ngx-sub-form though, it's because we've invested quite a lot of time at work to come up with that library as we do manage a lot of forms and we needed to abstract the complexity somehow. It has greatly simplified our workflow and I believe the solution above is quite verbose (by the number of components used) but really simple too as we haven't had to deal with streams for ex. (Even though I do love streams but I don't think that in that case it's the best approach).
Hope it helps and here's the final stackblitz I've made: https://stackblitz.com/edit/angular-nhnmcm
Wow.. so after a lot of brainstorming and frustrating moments I was able to solve this. I was thinking it in a different way. What I ended up doing was to create a stream of toggled Values (select/unselect checkboxes) and then filter on those toggled values.
So in summary -
Stream 1 - Merged stream of original set of variables & clicked variable. Use scan operator to get the updated stream at any point
Stream 2 - Consume the above stream and apply filter on the above stream.
toggleStateSelectedVariables$ = merge(this.variables$, this.toggleFlexibleVariableBy$).pipe(
throttleTime(100),
scan((availableVariables: NzCheckBoxOptionInterface[], clickedVariableValue: NzCheckBoxOptionInterface) => {
return availableVariables.map((variable) => {
// If its clicked, toggle it. If its selected, make it unselected. If unselected, select it
const checked = variable.value === clickedVariableValue.value ? !variable.checked : variable.checked;
return {
...variable,
checked
};
});
}),
);
flexibleVariables$ = combineLatest(this.toggleStateSelectedVariables$, this.filterBy$).pipe(
map(([variables, filterBy]) => variables.filter((item) => item.value.indexOf(filterBy) !== -1);
));
Implementation available here - https://stackblitz.com/edit/angular-ymft6o
Hope this helps someone. If anyone has any better solutions, please chime in
Happy Learning,
Vatsal

two objects have the same value

Both object is the same when i edit.
show the solution to seprate objects.
i have an object and with v-model i bind it to inputs to update it but when i edit it but don't save changes the object is changed. for not occurring this situation i use a second object(temporary) when editing i use that object but again it changes the first object.
data(){
return{
user:{
id:'',
firstname:'',
lastname: ''
},
tuser:{}
}
},
mounted(){
axios.get('/user')
.then((response) => {
this.user = response.data
})
.catch(error)
},
methods:{
edit(){
this.tuser = this.user
},
update(){
axios.patch(`/user/${this.tuser.id}`,this.tuser)
.then((response)=>{
this.user = this.tuser
})
.error()
}
}
You are mutating a reference value, it doesn't matter which one you change - they both get updated as they share the same address in memory heap.
So Use ES6 spread to make clone when you are trying to have copy of any reference value.
this.user = { ...this.tuser }
or
this.tuser = { ...this.user }
References : spread_es6
It is most likely because you reference one object to another.
As objects are reference not primitive values.
What it means is:
if you make obj1 = obj2, then actually they both point to the same object, if you change one, another change also.
You need to use something like Object.assign()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
methods:{
edit(){
this.tuser = this.user
},
I think you just need to pass a cloned copy of this.user, you can achieve that using lodash/underscore.js methods like clone or cloneDeep. If you don't copy your object they are referencing the same object

How can I construct a can.Map with computed values that behave like normal attributes?

Let's say I have a map:
map = new can.Map({foo: 'bar'})
and I want to bind the value of foo in another map. I could do:
otherMap = new can.Map({fizzle: map.compute('foo')})
but this doesn't behave the way I would expect.
I would expect otherMap.attr('fizzle') to return bar, but instead it returns a function. I have to call otherMap.attr('fizzle')() instead.
I would expect to be able to change the value by calling otherMap.attr('fizzle', 'moo'), but this doesn't change the computed value. Instead, if I want to change the underlying value, I have to call otherMap.attr('fizzle')('moo').
Is there a way to create a map with computed values that behave like normal attributes?
Thanks!
I would recommend using the define plugin which makes it easy to create computed getters and setters without having to explicitly create computes. In your example like this:
var map = new can.Map({
foo: 'bar',
baz: 'bla'
});
var OtherMap = can.Map.extend({
define: {
fizzle: {
get: function() {
return map.attr('foo') + '/' + map.attr('baz');
},
set: function(value) {
map.attr('foo', value);
}
}
} });
var other = new OtherMap();
console.log(other.attr('fizzle'));
other.attr('fizzle', 'something');
console.log(map.attr('foo'));
Demo in this Fiddle.

backbone.js model validation isValid() returns true for invalid attribute

I was trying to check the validity of individual attributes using isValid method. It is returning true for an invalid attribute. My code is as follows:
person = Backbone.Model.extend({
defaults:{
name:"default name",
age:0
},
initialize:function(){
this.on("invalid",function(model,errors){
console.log(JSON.stringify(errors));
});
},
validate:function(attrs){
errors=[];
if(attrs.age<0){
errors.push({attribName:"age",errorMsg:"age should be grater than 0"});
}
return errors.length>0?errors:false;
}
});
var person1 = new person();
person1.set({
age:-5
});
console.log("checking validity of model:"+person1.isValid());
console.log("checking for validity of age attribute:"+person1.isValid('age'));
isValid() works fine if used to check the validity of the model as a whole and returns false. But when I try to check the age attribute i.e isValid('age') it returns true when it should return false.
isValid() is an underscore.js function, right? Doesn't it support passing an attribute to check for its validity? What am I missing here?
Short version
Model.isValid doesn't accept an attribute name as argument and has to be used on the whole model. If you don't, you're on undocumented territory and you will get weird behaviors.
To check individual attributes, you will have to set up your own mechanism.
Long version, why you get a different value
Model.isValid does in fact accept an (undocumented) options hash as its first argument and it internally forwards this hash to Model._validate via
this._validate({}, _.extend(options || {}, { validate: true }))
trying to set a validate attribute to true. But at this point, options is a string and won't be modified by _.extend. _validate looks like
_validate: function(attrs, options) {
if (!options.validate || !this.validate) return true;
// ...
}
checking if it indeed has to validate the model, options.validate is undefined and your isValid call gets back a true value.
isValid is a backbone API : http://backbonejs.org/#Model-isValid
The reason it is returning true is, the parameter accepted by isValid is an options paramter. It has to be an object.
One of the scenario you use options is :
validate: function(attrs, options) {
if(options.someSpecialCheck) {
// Perform some special checks here
} else {
// Perform some regular checks here
}
}
myModel.isValid({someSpecialCheck: true});

Backbone.js: How to call methods on the collection within an object literal

I have the following backbone.js code. I'm using an object literal for organizing my code, which has left me with a question regarding the best way to proceed. The application (in its simplified form below) has a control panel (which can be shown or hidden) which is used to add new categories to a collection. (Question follows)
(function($){
// ============================= NAMESPACE ========================================
var categoryManager = categoryManager || {};
// ============================= APPLICATION =================================================
categoryManager.app = categoryManager.app || {
/* Used to Initialise application*/
init: function(){
//this.addView = new this.addCategoryView({el: $("#add-new-category")})
//this.collection = new this.categoryCollection();
new this.addCategoryView({el: $("#add-new-category")})
new this.categoryCollection();
},
categoryModel: Backbone.Model.extend({
name: null
}),
addCategoryView: Backbone.View.extend({
events: {
"click #add-new-category-button.add" : "showPanel",
"click #add-new-category-button.cancel" : "hidePanel",
"click #new-category-save-category" : "addCategory"
},
showPanel: function() {
$('#add-new-category-button').toggleClass('add').toggleClass('cancel');
$('#add-new-category-panel').slideDown('fast');
},
hidePanel: function() {
$('#add-new-category-button').toggleClass('add').toggleClass('cancel');
$('#add-new-category-panel').stop().slideUp('fast');
},
addCategory: function() {
//categoryManager.app.collection.create({
categoryManager.app.categoryCollection.create({ // My Problem is with this line
name: $('#name').val()
});
}
}),
categoryCollection: Backbone.Collection.extend({
model: this.categoryModel,
initialize: function () {
}
})
}
// ============================= END APPLICATION =============================================
/* init Backbone */
categoryManager.app.init();
})(jQuery);
Now obviously the problem with the above, is that calling the addCategory function tries to call a function on an object which is uninitialized. I've worked round the problem (see commented out code) by calling the function instead on a object which is instantiated within the init function. My question is - is this the right thing to do? I detect a code smell. I feel that the contents of the object literal shouldn't rely on the object being created in order to be valid. the function addCategory in this instance wouldn't work unless the init function had been called on the parent first. Is there another pattern here that I should be using?
How else would I pass the contents of the 'create new category form' to the collection in order to be added (I'm using create because I want to automatically validate/create/persist the model and It seems like the easiest thing to do). I'm a rock bottom novice with backbone (this is my 'hello world')
Thanks
I think the main issue is you are treating categoryCollection as if it's an object. It's not really an object, but a constructor function. So first you need to create an instance, as you have discovered.
Then the addCategoryView needs some way of referencing the instance. It looks like you don't have a model associated with the view. I would suggest creating a model and storing the categoryCollection instance as a property of the model. Something like this (warning, untested code):
var model = new BackBone.Model({
categories: new categoryManager.app.CategoryCollection()
});
var view = new categoryManager.app.AddCategoryView({
el: $("#add-new-category"),
model: model
});
Then you can just use this.model.categories from inside addCategoryView.
As an aside, a common Javascript convention is to capitalize the names of constructors. Calling the constructor CategoryCollection might make the code a little bit clearer.
You need to initialize collection before create a new instance of a model
addCategory: function() {
var collection = categoryManager.app.categoryCollection;
!collection.create && (collection = new collection);
collection.create({
name: $('#name').val()
});
}

Resources