How can I observe multiple key paths with ractive? - javascript

I'd like to observe changes to a key path per docs on ractive observe. However, I'd like to be able to observe multiple paths at the same time, eg, given:
var binding = new Ractive({
el: '.here',
data: {
items: [
{
finished: false
},
{
finished: false
}
]
},
template: someTemplate
})
I'd like to be able to do something like:
binding.observe('items.*.finished')
Or similar to be able to watch the finished property of any item in the array.
What's the best way to do this?

Exactly as you described! http://jsfiddle.net/rich_harris/c3yc848z/
Note that the values of any * placeholders are passed to the callback as additional arguments, beyond the (newValue, oldValue, keypath) that you normally get – so in this case there'd be a fourth index argument.

Related

How to replace one object for another in Vuejs? Interface not changing

i am doing an app in Vuejs and i am stuck with a problem in replacing one object for another that comes from an API every 10 seconds. I have this at the moment:
watch: {
myObj() {
for (let [key, tuner] of Object.entries(myObj)) {
---- some code ----
}
}
},
},
created() {
setInterval(this.callMyApi(), 10000);
},
I am watching the object and then use it in a for loop that does some logic. I saw the documentation mention using 'this.$set()' but this only adds a property to the object and i want replace one for another. Thanks for the help.
I would use:
this.myObj = JSON.parse(JSON.stringify(newObj))
to make a deep copy of the object inside this.callMyApi().
To watch for changes on this object do the following: create a new bool variable and use it on the component you want to update:
<componenttoupdate :reactto=objupdated/>
data: {
...
objupdated: false
...
}
and switch the objupdated bool state anytime you do the deep copy on your function, and so the component will update.
This is a cheaper solution than keeping watchers over large objects that may get updated very often as yours.

Vuejs does not update my interface, anyone know why?

I have an app with vuejs. I have an object that is being updated from an API every 10 seconds, it must also change the Html (interface) when it's updated.
I am getting some difficulties in updating the interface although I can see the object changing in the console, therefore I know its changing.
Here is a piece of my code:
watch: {
myObj() {
for (let [key, tuner] of Object.entries(myObj)) {
---- some code ----
}
}
},
},
created() {
setInterval(this.callMyApi(), 10000);
},
I call my API every 10 seconds and then i use the WATCH property to check when myObj changes and the do some logic with it. Can anyone help me?
There are 2 components to this that are extremely important to ensure that watching an object -- especially with nested properties -- works correctly.
First, you must either A) instantiate the property as an object by declaring myObj: {} or B) use Vue.set(this, 'myObj', yourCustomObject) to ensure that it is reactive.
Second, you must use deep: true, if your object contains nested objects/arrays.
watch: {
myObj: {
deep: true, // this is the important part
handler() {
// your code
}
}
}

Vue reactivity issue, need some explanations

I've removed most useless parts of my code, so don't worry if this code doesn't really make sense, it's just to show you what's not working.
First, I create an array from a base array called objects:
objects: [
{
text: "I dont trigger stuff",
},
{
"text": "I dont trigger stuff",
},
{
text:"I trigger stuff",
activated: undefined,
},
],
And the create function
created() {
const newArray = [];
this.objects.forEach(anObj => {
anObj.activated = false;
newArray.push(anObj);
});
this.filteredObjects = newArray;
},
I initialize a property activated to false. In my real code I'm not using a forEach, but a find but the result is the same.
Then, I display a few buttons to trigger an "activation"
<button
v-for="(myObj, index) in filteredObjects"
:key="index"
#click="activateObject(myObj, index)">
{{ myObj.text }}
</button>
And the function being triggered is this one:
activateObject(anObj, anObjIndex) {
this.$set(this.filteredObjects[anObjIndex], 'activated', !anObj.activated)
},
My goal here is just to update the activated property.
To check if reactivity is working, I've got a watcher:
watch: {
filteredObjects: {
handler() {
alert('called')
},
deep: true,
}
},
I've got two questions:
1/ Since all activated properties are set to false for all objects, why is there only one working, the one with the property initially set to undefined?
2/ If I update my activation function to:
activateObject(anObj, anObjIndex) {
anObj.activated = !anObj.activated;
this.$set(this.filteredObjects, anObjIndex, anObj);
},
It works well. Can someone explain me why, and what is the difference?
In both cases, VueJS Devtools shows the updated values when clicking on refresh. It's a reactivity issue.
You can find a fiddle here:
https://jsfiddle.net/dv1jgneb/
From the Docs:
Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive.
This explains why only the third button "trigger stuff".
So, you may either add that attribute in the data(), or as said in the docs, use this.$set:
this.objects.forEach(anObj => {
this.$set(anObj, 'activated', false);
newArray.push(anObj);
});
JS Fiddle
Hope this helps!

The collection nested inside firebase collection's model doesn't have add function

In my application, I am trying to use Firebase to store the real time data based on backbone framework.
The problem goes like this:
I have a sub level model and collection, which are both general backbone model and collection.
var Todo = Backbone.Model.extend({
defaults: {
title: "New Todo",
completed : true
}
});
var Todocollection = Backbone.Collection.extend({
model: Todo,
initialize: function() {
console.log("creating a todo collection...");
},
});
And then there is a high level model, which contains the sublevel collection as an attribute.
var Daymodel = Backbone.Model.extend({
defaults : {
day: 1,
agenda : new Todocollection()
}
});
and then for the higher level collection, I will firebase collection
var DayCollection = Backbone.Firebase.Collection.extend({
model: Daymodel
});
So far I can add data to the higher level collection correctly, which has a day attribute and an agenda attribute (which should be a TodoCollection).
The issue is when I try to add data to the sub-level collections, it can't work well.
this.collection.last()
.get("agenda")
.add({
title: this.input.val(),
completed: false
});
The above code will be inside the View part. And this.collection.last() will get the last model. get("agenda") should be the collection object.
But it can't work. The error shows that this.collection.last(...).get(...).add is not a function.
After debugging I found that this.collection.last().get("agenda") returns a general JS object instead of collection object.
I further debugged that if I use backbone collection as the outer collection DayCollection. Everything can go well.
How to solve such problem?
Why the default collection attribute is not a collection anymore?
When you fetch, or create a new Daymodel which I assume looks like this:
{
day: 1,
agenda : [{
title: "New Todo",
completed : false
}, {
title: "other Todo",
completed : false
}]
}
The default agenda attribute which was a Todocollection at first gets replaced by a raw array of objects. Backbone doesn't know that agenda is a collection and won't automagically populates it.
This is what Backbone does with the defaults at model creation (line 401):
var defaults = _.result(this, 'defaults');
attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
this.set(attrs, options);
_.extend({}, defaults, attrs) puts the defaults first, but then, they're overwritten by the passed attrs.
How to use a collection within a model?
Below are three solutions to accomplish this. Use only one of them, or create your own based on the followings.
Easiest and most efficient way is don't.
Keep the Todocollection out of the Daymodel model and only create the collection when you need it, like in the hypothetical DayView:
var DayView = Backbone.View.extend({
initialize: function() {
// create the collection in the view directly
this.agenda = new Todocollection(this.model.get('agenda'));
},
/* ...snip... */
});
Then, when there are changes you want to persist in the model, you just put the collection models back into the Daymodel:
this.model.set('agenda', this.collection.toJSON());
Put the collection into a property of the model
Instead of an attribute, you could make a function which lazily create the collection and keeps it inside the model as a property, leaving the attributes hash clean.
var Daymodel = Backbone.Model.extend({
defaults: { day: 1, },
getAgenda: function() {
if (!this.agenda) this.agenda = new Todocollection(this.get('agenda'));
return this.agenda;
}
});
Then, the model controls the collection and it can be shared easily with everything that shares the model already, creating only one collection per instance.
When saving the model, you still need to pass the raw models back into the attributes hash.
A collection inside the attributes
You can accomplish what you're already trying to do with small changes.
Never put objects into the defaults
...without using a function returning an object instead.
var Daymodel = Backbone.Model.extend({
defaults: function() {
return {
day: 1,
agenda: new Todocollection()
};
},
});
Otherwise, the agenda collection would be shared between every instances of Daymodel as the collection is created only once when creating the Daymodel class.
This also applies to object literals, arrays, functions (why would you put that in the defaults anyway?!).
Ensure it's always a collection.
var Daymodel = Backbone.Model.extend({
defaults: { day: 1, },
initialize: function(attrs, options) {
var agenda = this.getAgenda();
if (!(agenda instanceof Todocollection)) {
// you probably don't want a 'change' event here, so silent it is.
return this.set('agenda', new Todocollection(agenda), { silent: true });
}
},
/**
* Parse can overwrite attributes, so you must ensure it's a collection
* here as well.
*/
parse: function(response) {
if (_.has(response, 'agenda')) {
response.agenda = new Todocollection(response.agenda);
}
return response;
},
getAgenda: function() {
return this.get('agenda');
},
setAgenda: function(models, options) {
return this.getAgenda().set(models, options);
},
});
Ensure it's serializable.
var Daymodel = Backbone.Model.extend({
/* ...snip... */
toJSON: function(options) {
var attrs = Daymodel.__super__.toJSON.apply(this, arguments),
agenda = attrs.agenda;
if (agenda) {
attrs.agenda = agenda.toJSON(options);
}
return attrs;
},
});
This could easily apply if you put the collection in a model property as explained above.
Avoid accidentally overriding the agenda attribute.
This goes alongside with point 2 and that's where it's getting hard as it's easy to overlook, or someone else (or another lib) could do that down the line.
It's possible to override the save and set function to add checks, but it gets overly complex without much gain in the long run.
What's the cons of collection in models?
I talked about avoiding it inside a model completely, or lazily creating it. That's because it can get really slow if you instantiate a lot of models and slower if each models is nested multiple times (models which have a collection of models, which have other collections of models, etc).
When creating it on demand, you only use the machine resources when you need it and only for what's needed. Any model that's not on screen now for example, won't get their collection created.
Out of the box solutions
Maybe it's too much work to get this working correctly, so a complete solution might help and there are a couple.
Backbone relational
backbone-nested
backbone-nested-models
backbone-deep-model

Can you bind a simple javascript array to your ember.js template?

I'm using ember.js RC1 + ember-data rev 11 (but I also need some plain ajax for configuration like models). I want to loop over a simple objects list and display the records (note -here I create just a basic array)
The content I have bound has the following custom find method defined
App.Foo = DS.Model.extend({
name: DS.attr('string')
}).reopenClass({
records: [],
all: function() {
return this.records;
},
find: function() {
var self = this;
$.getJSON('/api/foo/', function(response) {
response.forEach(function(data) {
//say I want to kill everything in the array here for some strange reason...
self.records = [];
//the template still shows the record ... not an empty list ?
}, this);
});
return this.records;
}
});
My other model uses this directly
App.Related = DS.Model.extend({
listings: function() {
return App.Foo.find();
}.property()
});
Now inside my template
{{#each foo in related.listings}}
{{foo.name}}<br />
{{/each}}
The list loads up with whatever I put in the array by default (say I add a simple object using createRecord like so)
add: function(record) {
this.records.addObject(App.Foo.createRecord(record));
},
and when the template is rendered I see anything listed here... but as I put in the comments above, if I decide to remove records or null out the list that is bound it doesn't seem to reflect this in any way.
Is it possible to bind a simple array as I have and yet remove items from it using something basic such as splice? or even a drastic self.records = []; ?
self.records.splice(i, 1);
Even when I query the client manually after the splice or empty work it returns 0
console.log(App.Foo.all().get('length'));
Initially I see records, but then I see they are gone (yet the html doesn't change)
I understood your question this way, that the following remark is the point your are struggling with:
response.forEach(function(data) {
//say I want to kill everything in the array here for some strange reason...
self.records = [];
//the template still shows the record ... not an empty list ?
}, this);
You are wondering, why your template is showing no empty list? It's because you did not tell Ember when to update the template. You can tell Ember this way:
App.Related = DS.Model.extend({
listings: function() {
return App.Foo.find();
}.property("App.Foo.records.#each")
});
Now Ember knows, whenever something is added or removed from your array, it should update the listings property of your model. And therefore it knows that your view needs rerendering.
One additional remark to the orignal question regarding "simple javascript arrays". When you use Ember, you actually do not instantiate simple js arrays. When you declare:
var a = []; // is the same as -> var a = Ember.A();
Ember does some magic and wraps in an enhanced ember version of an array (Ember.NativeArray), which enables you to use such property dependency declarations mentioned above. This enables Ember to use ArrayObservers on those arrays, although they may feel like a plain JS Array.
You need to use the set method when you modify properties and get when you return them, or else Ember won't be able to do its magic and update the template.
In your case, there is an additional problem, which is that in find(), you return a reference to records before your asynchronous getJSON call replaces it with a new empty array. The calling method will never see the new array of records. You probably want to use clear() instead.
Your model should look something like this:
App.Foo = DS.Model.extend({
name: DS.attr('string')
}).reopenClass({
records: [],
all: function() {
// can't use 'this.get(...)' within a class method
return Ember.get(this, 'records');
},
findAll: function() {
var records = Ember.get(this, 'records');
$.getJSON('/api/foo/', function(response) {
records.clear();
// in this case my json has a 'foos' root
response.foos.forEach(function(json) {
this.add(json);
}, this);
}, this);
// this gets updated asynchronously
return records;
},
add: function(json) {
// in order to access the store within a
// class method, I cached it at App.store
var store = App.get('store');
store.load(App.Foo, json);
var records = Ember.get(this, 'records');
records.addObject(App.Foo.find(json.id));
}
});
Note that the addObject() method respects observers, so the template updates as expected. removeObject() is the corresponding binding-aware method to remove an element.
Here's a working jsfiddle.

Categories