Knockout View Model Reset -- When Observables Require Defaults - javascript

Let us say that I have the following simplified and generic view model, which uses KnockoutJS and the Knockout Validation library to create/manipulate and validate observables, respectively.
// view declared
// KnockoutJS loaded
// KnockoutValidation loaded
var ChildNodePropertiesVM = function(properties) {
var self = this;
/* data within properties object are assigned to
observables within VM */
};
var ChildNode = function() {
var self = this;
var options = // data from ajax request sent to web service
// list of POJOs
this.availableParentNodes = ko.observableArray();
// a specific POJO from the list above, selected from an HTML select element
// Knockout Validation ensures that a value is present (required)
this.associatedParentNode = ko.observable().extend({
required: {
params: true,
message: "Please choose a parent"
}
});
// a view model, declared above, constructed with value from options
this.childProperties = new ChildNodePropertiesVM(options.childProps);
/* Many more model-members follow, and may be any of the above types
(observable, observableArray, view model, etc) */
};
// apply bindings to view here (in this case, ko.applyBindingsWithValidation)
What I am interested in is, if I want to create a "reset" procedure to clear a majority of the members of a view model (of which there may be many), and keep a minority of the fields as they were, what is the most maintainable way in which I could do so? To be more specific, consider the availableParentNodes object to be one of the observables that I would like to keep, while associatedParentNode must be cleared (as in, self.associatedParentNode(undefined)), and half of the (undeclared) model-members of childProperties must be set to null, as part of the reset process.
SO and search engine queries either suggest that each observable/variable be cleared manually, or that a new view model (in this case, the ChildNode view model) simply be created to replace the old one. Since I have specific fields that I would like to keep, the latter is not an option, and since there are so many observables in these models, the former is unmaintainable.

An option would be to create a generic reset function, with a whitelist parameter for the observables you would like to keep. Here's an example (polyfill indexOf if you need to support outdated browsers):
var reset = function ( obj, whitelist ) {
for ( var prop in obj ) {
if ( obj.hasOwnProperty( prop ) && ko.isObservable( obj[ prop ] ) && whitelist.indexOf( prop ) === -1 ) {
obj[ prop ]( undefined );
}
}
};
Then you can curry this function with your whitelist on your individual models.
SomeModel.prototype.reset = function () {
reset( this, [ 'something', 'somethingElse' ] );
};
The above would set every observable except the ones named something and somethingElse in your model to undefined.
Here's a JSFiddle example.

this could be done in a simple way by creating a function
self.Reset = function(){
self.someobservable(null)
self.someobservbleArray([])
}
And now simply call it
self.Reset()
The values will be reset.

Related

Javascript - Mithril nested components

I am building a dashboard using mithril components. The dashboard consist of generic widget components which nest more specific components such as twitter widgets, sales widgets etc. The generic widget is agnostic of the nested component.
Now I have a requirement to compliment the generic widget from the nested component. Eg, the generic widget has a toolbar with common operations with delete & refresh, I want to add inner component specific operations such as change date range in case of sale widget, change account in case of twitter widget.
Straight forward approach to this is decompose the inner component is to 2 subcompoents a toolbar and the content, eg sales_toolbar_component & sales_content_component. Both these subcomponnets needs the same data loaded via ajax, hence it will lead to duplication. An approach is to follow Hybrid architecture (http://mithril.js.org/components.html#hybrid-architecture) where a top level component will do the ajax calls and pass data at constructor of each sub component. However in this case the parent component is generic is unaware of child data requirements
Another approach is for one sub-component to do the ajax call and notify the sibling via observer pattern, this seems quite a bit of internal coupling/dependency
Another consideration was to create a component that can have multiple views & for the parent to render each view as required using the same controller instance.Something like:
//create a component whose controller and view functions receive some arguments
var component = m.component(MyComponent, {name: "world"}, "this is a test")
var ctrl = new component.controller() // logs "world", "this is a test"
m.component(MyComponent.toolbar(ctrl));
m.component(MyComponent.content(ctrl));
None of these seems complete, is there a reference pattern I can consider?
The memoization pattern might suit you. Memoization involves wrapping a function - in this case the AJAX request - such that the first call with any given input triggers the underlying function, but its return value is stored in an internal cache; subsequent calls with the same input retrieve the cached value without touching the underlying function. Here's a simple implementation1 and a memoized AJAX requesting function that wraps Mithril's m.request:
function memoize( fn ){
var cache = {}
return function memoized( input ){
if( !( input in cache ) )
cache[ input ] = fn( input )
return cache[ input ]
}
}
var get = memoize( function( url ){
return m.request( { method : 'GET', url : url } )
} )
This way whichever components executes first will make the request; the next component (which will execute immediately afterwards) will retrieve the same value.
Regarding the idea of having a multi-view component, this isn't really any different in practical terms from your original proposal. Consider the following code:
var Wrapper = {
controller : function(){
// AJAX or whatnot
},
view : function( ctrl ){
return m( '.Wrapper',
m( Toolbar, ctrl )
m( Content, ctrl )
)
}
}
m( Wrapper, { name : 'world' }, 'This is a test' )
Here, I used the reference Wrapper instead of MyComponent, and the Toolbar and Content are just components as originally proposed. The wrapper isn't generic, but neither was MyComponent. I find that trying to reduce individual units of Mithril code to components where possible - even if you'd rather have less components in an ideal world - generally makes code much easier to maintain, because whereas you may end up with many context-specific modules instead of a few highly configurable generic modules, all of these context-specific modules are generic in the way they're called, which is much more useful in terms of code predictability.
Having said that, it would be possible to refine your idea of a pattern for passing one controller to multiple views. Again, I would reduce this pattern to component form, so that we can deal with the complications internally but expose an interface that's consistent across Mithril:
var MultiView = {
controller : function( controller ){
return new controller()
},
view : function( ctrl ){
var views = [].slice.call( arguments, 2 )
return m( 'div',
view.map( function( view ){
return view( ctrl )
} )
)
}
}
m( MultiView, function controller(){ /* AJAX or whatnot */ }, Toolbar.view, Content.view )
1 This memoization function will work for any function which accepts a single string argument, which is perfect for AJAX requests. If you ever want a more comprehensive memoization solution, check out funes.

EmberJS dynamic observers at run-time?

Given the following models:
(note: these are simplified for illustration purposes)
App.CustomerOrder = DS.Model.extend({
deliveries: DS.hasMany('delivery'),
total: DS.attr('number')
});
App.Delivery = DS.Model.extend({
orderlines: DS.hasMany('orderline')
});
App.OrderLine = DS.Model.extend({
productid: DS.attr('string'),
qtyordered: DS.attr('number')
});
When the app first loads I'm querying an API that sends me information about which dependencies should trigger an update. So for example it'll send me something like:
CustomerOrder: ["deliveries", "deliveries.orderlines", "deliveries.orderlines.qtyordered"...]
..means, if deliveries are added/deleted from a customerorder or if lines are added/deleted from a delivery attached to a customer order or if the qtyordered on an orderline on a delivery attached to a customer order, then what the API expects is for me to serialize CustomerOrder (along with the entire chain of relationships) and sent to an 'update' service (i.e. server/customerorder/updates type thing) that will run various routines and fill-in pieces of data and send the entire chain of objects back.
For illustration purposes I've put a simple example on here of an ordertotal (I realize this is easily calculated client-side but there's a bunch of other stuff that would be duplication of code from the server). So, if the qtyordered on an orderline changes, I need to send the customerorder instance to the server, where it will update my total field.
One of the challenges is that I can't hard code that dependency list by setting up observer functions with .observes() type stuff, it has to be done dynamically after that dependency data is loaded (presumably using addObserver). The other is that observers wont dig multiple layers deep like that.
I've tried using a mix-in to the models that overrides the init function and does exactly that.
clientchangeset: DS.attr('raw'),
init: function() {
this._super.apply(this, arguments);
var className = this.auth.camelizedModelString(this.constructor.toString());
var watchlist = this.auth.dependencies[className] || null;
var self = this;
watchlist.forEach(function(watch) {
if(watch.hasOwnProperty('attributeName') && watch.hasOwnProperty('collectionFlag')) {
// {attributeName: attributeName, collectionFlag: collectionFlag}
if(watch['collectionFlag']) {
console.log(className+'.addObserver('+watch['attributeName']+'.#each.clientchangeset)');
self.addObserver(watch['attributeName']+'.#each.clientchangeset', null, 'updateChangelist');
} else {
console.log(className+'.addObserver('+watch['attributeName']+')');
self.addObserver(watch['attributeName'], null, 'updateChangelist');
}
}
});
},
This appears to work, but only one layer deep. For completeness, heres the updateChangelist function:
updateChangelist: function(src, field, value) { //jshint ignore:line
if(this.get('pauseUpdates')) {
return;
}
var className = this.auth.camelizedModelString(this.constructor.toString());
var oldclientchangeset = this.get('clientchangeset') || [];
console.log('Before: '+className+'.[clientchangeset]= '+oldclientchangeset);
oldclientchangeset.pushObject(field);
this.set('clientchangeset', oldclientchangeset);
console.log('After: '+className+'.[clientchangeset]= '+oldclientchangeset);
}
So in general the way I got this to work was to create the observers as indicated, but the handlers simply update a property called '_needsUpdate' on each level of the relationships whenever they are triggered. '_needsUpdate' is just a date so when triggered I do:
this.set('_needsUpdate', +new Date());
Then when setting up observers at each level for that level's children, I just set up a single observer to look at child.#each._needsUpdate.

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.

In Backbone, can I add model behavior to a collection?

If I have a collection of objects, but also want to store some higher-level information about those objects, is it appropriate to add some model behavior to the collection?
In my situation, I'm looking for a collection of application paths to have a bool field called 'curPath'. If it's changed, the collection should set a flag that indicates the current page. This way outside observers only have to observe one field, not every model in the path collection.
Here's what that might look like:
var PathModel = Backbone.Model.extend({})
var PathCollection = Backbone.Collection.extend({
initialize: function(){ this.model = PathModel }
})
// I want to be able to set observable properties on the collection, so...
var PathManager = _.extend(Backbone.Model, PathCollection)
// Then maybe I can do something like this?
PathManager.each(function(pathModel){
pathModel.on('change:curPath', function(m, value, o){
// I mean for 'this'.set to point to the instance of the PathManager
if (value === true){ this.set('curPath', pathModel.get('id')) }
}, this)
}, this)
Is it appropriate to add observable behavior to a collection (collection+model > model), or do I need to add a wrapping model to the whole thing (model > collection > model), or is there some other solution?
As some of the methods on Model and Collection have the same names that will cause conflicts and possible problems with updating to future versions of Backbone could occur I would advise against this pattern. Rather then doing that create a PathManager class and have PathCollection be initialized and set it as paths property on this model. If from what you are saying
var PathManager = Backbone.Model.extend({
initialize: function() {
this.paths = new PathCollection();
}
});
var pathManager = new PathManager();
pathManager.paths.add({ /* new path */ });
pathManager.on('change', doSomething);
pathManager.paths.on('add', doSomethingElse);
I'd do it something like this

How to initialize a Knockout viewmodel when initial viewmodel load is empty

I am using Knockout to implement a course list selection tool. I am using the approach below to populate the data (MVC3/Razor), so that when the viewmodel is initially populated, I have no issues working with each KO array (i.e. CourseList, ScheduleList). However, when the initial load from the server returns zero rows, meaning that the viewmodel 'ScheduleList' property is empty, then it's not possible to call any methods such as .push() or .removeAll(). Presumably this means that the observable array was never created since there was nothing to fill it with. When the model is filled, the ScheduleList property is populated with a List. What is the best way to instantiate it when the MVC action returns it as empty? There is a jsFiddle that seems to address it, but when I try to use the 'create' option, it renders my entire model blank. I am not sure what the syntax is of the 'create' option. The jsFiddle is here: http://jsfiddle.net/rniemeyer/WQGVC/
// Get the data from the server
var DataFromServer = #Html.Raw(Json.Encode(Model));
// Data property in viewmodel
var data = {
"CourseList": DataFromServer.CourseList ,
"ScheduleList": DataFromServer.ScheduleList
};
$(function() {
// Populate Data property
viewModel.Data = ko.mapping.fromJS(data);
// ko.applyBindings(viewModel, mappingOptions);
ko.applyBindings(viewModel);
});
When the initial page load does not populate ScheduleList, then the following code throws an error. If the initial page load contained data, then you could call .removeAll() and .push() etc.
var oneA= 'abc';
// push not working
this.Data.ScheduleList.push( oneA );
Set up your mapping parameters to make it so on creation, you give it a certain structure. Then it will do the updates for you.
What is most likely happening is that your DataFromServer doesn't actually contain a ScheduleList property at all. So when it is mapped, a corresponding property is never made. The mapper will only map existing properties to observables.
You need to set in your create options for the view model to add empty arrays when either array is not set. That way, your view model will end up with corresponding observable arrays in place.
By ensuring that CourseList or ScheduleList is an array, the mapped view model will map them as observableArray objects so your code will work as you expected.
var DataFromServer = {
'CourseList': [1,2,3]
//, 'ScheduleList': []
};
var dataMappingOptions = {
'create': function (options) {
var data = options.data;
data.CourseList = data.CourseList || [];
data.ScheduleList = data.ScheduleList || [];
return ko.mapping.fromJS(data);
}
};
viewModel.Data = ko.mapping.fromJS(DataFromServer, dataMappingOptions);
var data = {
CourseList: DataFromServer.CourseList || ko.observableArray([]) ,
ScheduleList: DataFromServer.ScheduleList || ko.observableArray([])
};

Categories