rivets.js newbie here. I want to bind to an item that will be changing dynamically (store.ActiveItem). I tried the following approach, but although store.ActiveItem is set, store.ActiveItem.(any property) is always undefined. Is there a standard way to bind deeper than one level?
<div id="editItemDialog" data-modal="store.ActiveItem < .ActiveItem">
<a data-on-click="store:ClearActiveItem" href="#">close - works</a>
<div>
<div>
<label>name:</label><input data-value="store.ActiveItem.Name < .ActiveItem"/>
</div>
<div>
<label>price:</label><input data-value="store.ActiveItem.Price < .ActiveItem"/>
</div>
<div>
<label>description:</label><textarea data-value="store.ActiveItem.Description < .ActiveItem"></textarea>
</div>
</div>
</div>
How the binding works is largely determined by the Rivets adapter you are using, although your model could also do the heavy lifting.
Option 1: Smart Model
If you're using Backbone.js, you could take a look at backbone-deep-model, which supports path syntax for nested attributes (Eg. store.get('ActiveItem.Price')), though it is still under development. If that doesn't quite meet your needs, there are other nested model-type options on the Backbone plugins and extensions wiki.
Option 2: Smart Adapter
If that doesn't work for you, you can extend your Rivets adapter to handle path syntax. I've put together a simple example on how to do this at http://jsfiddle.net/zKHYz/2/ with the following naive adapter:
rivets.configure({
adapter: {
subscribe: function(obj, keypath, callback) { /* Subscribe here */ },
unsubscribe: function(obj, keypath, callback) { /* Unsubscribe here */ },
read: function(obj, keypath) {
var index = keypath.indexOf('.');
if (index > -1) {
var pathA = keypath.slice(0, index);
var pathB = keypath.slice(index + 1);
return obj[pathA][pathB];
} else {
return obj[keypath];
}
},
publish: function(obj, keypath, value) {
var index = keypath.indexOf('.');
if (index > -1) {
var pathA = keypath.slice(0, index);
var pathB = keypath.slice(index + 1);
return obj[pathA][pathB] = value;
} else {
return obj[keypath] = value;
}
}
}
});
Option 3: Dirty Hacks
As of version 0.3.2, Rivets supports iteration binding. By creating a Rivets formatter that returns an array, you can "iterate" over your property. Take a look at http://jsfiddle.net/mhsXG/3/ for a working example of this:
rivets.formatters.toArray = function(value) {
return [value];
};
<div data-each-item="store.ActiveItem | toArray < store.ActiveItem"">
<label>name:</label><input data-value="item.Name < store.ActiveItem"/>
...
</div>
I'm not sure if the computed property syntax is required here; you will have to test this with your model to see what works.
Option 4: Don't bind deeper than one level (Recommended)
The need to bind deeper than one level may be an indication that your design can be improved.
In your example, you have a list of Items in an ItemCollection for a Store. You go about assigning a single Item to the Store's ActiveItem property, setting up events everywhere to try link things together, and then need to be able to bind to the properties of the ActiveItem under the Store, yet have things update whenever the ActiveItem itself changes, etc..
A better way of doing this is by using a view-per-model approach. In your example, you're trying to handle the Store Model, the ItemCollection and the Item Model with a single view. Instead, you could have a parent Store view, a subview for the ItemCollection, and then generate Item views as necessary below that. This way, the views are easier to build and debug, less tightly coupled to your overall Model design, and are more readily reusable throughout your application. In this example, it also simplifies your Model design, as you no longer need the ActiveItem property on the Store to try maintain state; you simply bind the Item View to the selected Item Model, and everything is released with the Item View.
If you're using Backbone.js, take a look at Backbone.View as a starting point; there are many examples online, although I'll be the first to admit that things can get somewhat complex, especially when you have nested views. I have heard good things about Backbone.LayoutManager and how it reduces this complexity, but have not yet had the chance to use it myself.
I've modified your most recent example to use generated Item views at http://jsfiddle.net/EAvXT/8/, and done away with the ActiveItem property accordingly. While I haven't split the Store view from the ItemCollection view, note that I pass their Models into Rivets separately to avoid needing to bind to store.Items.models. Again, it is a fairly naive example, and does not handle the full View lifecycle, such as unbinding Rivets when the View is removed.
Related
I'm testing Ionic 2 and Angular 2, and I've got a doubt about accessing to parent view's properties.
Per example, I've got a test app in which my view is a list of items, and when I click one item, I enter to their details. Pretty straightforward, huh? Well, that details view has got functions that edit the element, and then apply the changes.
For this, I use three different ways:
One is to pass the object reference and just edit it, which edits it back in the list (I guess this is pretty optimal)
Before the typical navCtrl.pop(), pass a parameter via navParam to the function "ionViewDidEnter()", which executes just when you come back to a view, and filter it there, so you can perform the task you desire. Problem: it doesn't work (probably it's a bug).
Here comes the krakken: when removing the element, this won't work, since I have to remove it from the list, per example, with the typical list.splice(index, 1);
I found two different methods of performing this: you can either pass the new view a reference of the list, or you can access it from the NavController, just as I do here:
remove(){
let list = this.navCtrl._views[0].instance.list;
for(var i=0;i<list.length;i++){
if(list[i].id === this.contact.id){
list.splice(i,1);
}
}
this.navCtrl.pop();
}
Here I have another example of this weird technique, reusing the edit view for creating a new element:
editContact(obj){
if(this.onEdit){
this.onEdit = false;
this.editBtnTxt = "Edit contact";
if(this.onCreate){
this.navCtrl._views[0].instance.list.push(this.contact);
this.navCtrl.pop();
}
}else{
this.editBtnTxt = 'Apply changes';
this.onEdit = true;
}
}
Although this works pretty nicely and isn't throwing any errors, I guess I'm just being somewhat lucky, because: how do you know the index of the view you want to access, if you're not in a simple test project like this with two views, per example? I guess there can be a lot of errors with this way of doing things.
But as it works, and it seems to be more optimal than passing tons of parameters, or using localStorage as a "global" variable, I'm sticking with this by the moment.
What I would like to know, is... which way is the most optimal of accessing parent view properties?
You should try to avoid accessing the parent view.
Use #Output()s in the child and (someEvent) bindings in the parent and notify the parent about the actions it should take on the model.
If they are not direct parent child (like when the child is added by the router) use shared services with observables instead.
For most JS frameworks and libraries, the value they bring are often in the form of a new structure as to how to build an application (Backbone, React), or new ideas that effectively power-up the language (Angular), or simply the methods they offer are well tested, fast, and really convenient (jQuery).
Usually the ideas and methods they provide are pretty straightforward usage of JavaScript, but with a very clever team behind it that find interesting ways to do things which you can think through and get a solid guess as to how the guts work.
However, I've been unable to think through the ability to two-way bind JS models to view components. What is the secret sauce at the heart of this feature that makes this work? Changing an internal variable from a user input is simple, but what about the reverse? How would you be able to "know" when a JS variable has changed in order to update the display instantly? Surely it can't be polling, so what then?
Whenever a block of your JS runs that angular triggered it will run a digest cycle when the block finishes executing. This basically checks all the values that might of changed and would require updates to the view.
If angular didn't trigger the code then it won't know that something might of changed so your bindings can get out of sync. For example if you run something like this
setTimeout(function() {$scope.myValue = '123'});
Angular won't know that myValue changed and it actually won't update the view. That's why Angular has it's own services for doing everything. e.g. $timeout or $http.
If you have some callback function that Angular doesn't know about then you can manually tell it to check for changes by calling $scope.$apply()
there are several ways to do it. Object.observe is great, but lacks good support. You can poll for values as well, keeping a 2nd copy of the object around to compare. You can also write your own explicit set/get methods to update the model like backbone does.
One neat method i use a lot is using getters/setters to keep the model synced to the dom:
//a demo "model" of data:
model = {
name: "Fred"
};
function change(k,v){alert([k,v]);} // a stand-in change monitor for demo
// iterate model and replace values with getter/setter combos:
Object.keys(model).forEach(function(key) {
var val = model[key];
delete model[key];
Object.defineProperty(model, key, {
get: function() {
return val;
},
set: function(v) {
val = v;
change(key, val);
} //call change upon setting
});
change(key, val); //update view "onload"
}); // alerts "Fred";
//update model (fires change() with "name" and "sally" arguments:
model.name="sally"; // alerts "sally";
the change function is quite simple and for your case should just find elements bound to keys.
the advantage here is that you don't need special custom CRUD methods, you can just modify the object properties via assignment like it's 1999. It also doesn't poll, and works correctly all the way back to IE9 and any other ES5 environments. It's the simplest way to bind JS>DOM (afaik) without custom methods.
It does have some limits: nested objects are tricky to get/set upon, you can't do the whole object at once, you can only "watch" primitives. Arrays are a problem too: you can't really replace expando properties with getters/setters without side-effects. But, upon a relatively flat collection of JSON-safe data, get/set works a charm and needs no complex libs to get operational.
checkout a complete example using this method: http://pagedemos.com/xg3szbguqnwu/4
I can speak to how it's done in Backbone, which has a relatively low-level perspective on data-binding.
It's a combination of 1. the library having control over attribute setter methods 2. invoking callback functions when attributes change (e.g. by dispatching events) in order to update the UI.
The essential pseudocode is this:
class Model:
method set(name, value):
if value != this.attributes[name]
this.triggerEvent('change', name, value)
this.attributes[name] = value
m = new Model()
someInputWidget.onEvent('userChangedInput', function(value) {
m.set(someInputWidget.name, value)
})
m.onEvent('change', function(name, value) {
getInputWidgetByName(name).setValue(value)
})
Backbone does not do any data binding to the UI, but you can refer to Backbone's annotated source for the actual event-dispatching implementation.
I have a model which I load and store using $resource. The model is an aggregate and has nested collections inside, which are binded to an html view using ng-repeat.
Model:
{
someRootField: "blabla",
sectionCollection: [
{
name: "section1"
....
},
{
name: "section2",
....
}
]
}
html:
<div ng-repeat="section in myModel.sectionCollection">
...
</div>
controller:
MyModelResource = $resource(config.api4resource + 'models/:id', {id:'#_id'});
$scope.myModel = MyModelResource.get({id: xxxx});
The problem: when I use $save on this model, it causes a reload/redraw of some portions of the screen (seems not the root fields, but the collection related ones), if some binded elements within the sections are inputs, focus is lost too. I did some debugging and here is what I think is happening.
When I save the model, the results from the POST command mirror the body of the request, and myModel is being repopulated with it. Simple fields in the root of the model are pretty much the same, so the watch() mechanism doesn't detect a change there, however the the objects in the sectionCollection array are different, as they are compared not by their contents but by an equality of the references and fail, this causes the ui controls associated with the collection to be completely reloaded/redrawn.
There is this code in $watchCollectionWatch() in angular:
} else if (isArrayLike(newValue)) {
if (oldValue !== internalArray) {
// we are transitioning from something which was not an array into array.
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}
newLength = newValue.length;
if (oldLength !== newLength) {
// if lengths do not match we need to trigger change notification
changeDetected++;
oldValue.length = oldLength = newLength;
}
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
if (oldValue[i] !== newValue[i]) {
changeDetected++;
oldValue[i] = newValue[i];
}
}
}
in my case, I've definitely seen the oldValue[i] = newValue[i] comparison fail, the objects were different. One of the reason is oldValue contained variables prefixed with $ that were referring back to the scopes that were previously created for each item.
The question is, how can I prevent a reflow? Or how can I do it differently to avoid it. Keeping myself two copies of the model, one for $resource and another for binding to view and synchronizing between them manually does not seem right.
Thanks!
You can use $http service to avoid model updates that $save cause:
$scope.save2 = -> $http.get 'blah_new.json'
I used get in example but you can use whatever you need from this list of shortcut methods. And here is a simple example plunk.
Also it's simple to save elemen's focus after rerendering:
$scope.save = ->
active = document.activeElement.getAttribute 'id'
$scope.user1.$save ->
document.getElementById(active).focus()
For UI purposes, when I load the array my viewModel is based on I add a new property to each object based on some other properties:
item.forEach(function (party) {
if (party.AcknowledgementDate() === null) {
party.Agreed = ko.observable(false);
}
else {
party.Agreed = ko.observable(true);
}
vm.Parties.push(party);
});
"Parties" is defined as ko.observableArray when the page starts.
The items in this array are edited in a separate UI window. When those changes are saved and the window closed, I call this function to update those values:
function updateAgreed() {
vm.Parties().forEach(function (i) {
if (i.AcknowledgementDate() === null) {
i.Agreed(false);
}
else {
i.Agreed(true);
}
});
}
This all works fine, and makes me very happy. The problem arrives when users create a new party item. We're using Breeze too, so we go off to the data service which requests entity framework create a new object of the appropriate type, then add an observable:
var lp = manager.createEntity('Party_dto'. { [an array of initial values] });
lp.Agreed = ko.observable('');
return lp;
Thanks to Breeze, this adds itself to the Parties observableArray because it's related to the same parent object. I can then call updateAgreed again to populate the Agreed observable with the appropriate value.
Logically, this work as expected - you can step through it and watch the Agreed observable of the new item be added and populated with the expected values. The problem comes in the UI - it doesn't update as having changed. Yet running the same code against an already-loaded object does cause the UI to update.
I'm stumped by this. I can't replicate it in Fiddle because we create objects in Breeze and not on the fly - and making a mock version without Breeze works perfectly. Why do my observables update on already loaded objects, but the same observable not update on a new object?
There are a few things that I see that need to be addressed. One, since you are using Breeze, take advantage of the model constructors and initializers. Wherever you are defining properties for your models, add the following code -
metadataStore.registerEntityTypeCtor(
'Party', null, partyinitializer);
function partyinitializer(party) {
party.Agreed = ko.observable(false);
}
Now all of your party entities have an agreed property that you can access. Next, make sure you aren't setting the Party's parent navigation property in the createEntity method, as that will break your binding.
var lp = manager.createEntity('Party'. { [an array of initial values] });
lp.parentParty(something); // Set the parent here
return lp;
This will make sure that before the party is bound back to the parent and shown in the view, all of the properties will be set. Then when you set the navigation property, it will show up in your view all happy-like.
If a viewmodel is already defined, either manually or automatically with mapping plugin, is there any problem to "extend" (add properties/functions) the viewmodel later in the code by using prototype?
I'm aware of the create callback of mapping plugin, I just want to know if there are any implications of using prototype instead? The reason I'm asking is because I'm generating large parts of the viewmodels from server side code, but sometimes need to extend the viewmodel later than at initial generation.
I don't think there is any problem with this, I work with a large deep view model graph instantiated via the mapping plugin from correspondingly structured JSON and I use prototypes to define an "AbstractViewModel" with useful properties and toJSON "overrides" among other things.
There is no problem with this. Just make sure that the view responds appropriately when there's no data in that particular field in the viewModel.
There seems to be a couple ways of going about this.
For one, you can take a single object view model and utils.extend them via prototype:
ko.utils.extend(ViewModelClass.prototype, {
newPrototype1: function () { ... },
newPrototype2: function () { ... }
}
Or, you can add an extender to knockout and invoke it via the observable object itself:
(http://knockoutjs.com/documentation/extenders.html)
ko.extenders.numeric = function(target, precision) {
...details in link above...
}
...
self.NumericProperty = ko.observable(data).extend({ numeric: 0 });
...
Or create a function that is available to all instances of an observable, observableArray, computed...
(http://knockoutjs.com/documentations/fn.html)
ko.observable.fn.filterByProperty = function(propName, matchValue) {
return ko.computed(function() {
...details in link above...
}, this);
}
I tend to use combinations of these. I like extending prototypes of View Models in a separate file from the VMs and Mappings, so I have a structure like
ViewModel(s).js
ViewModel(s).Mappings.js
ViewModel(s).Prototypes.js
As you'll see, the timespan between these 'answers' is rather large, so some things have changed yet some things remain the same.