I'm trying to implement some kind of undo feature in my project using KnockoutJS. For this purpose, I'm using this extender:
ko.extenders.trackChange = function (target, track) {
if (track) {
target.isDirty = ko.observable(false);
target.originalValue = target();
target.subscribe(function (newValue) {
// Push to "states" array a JS representation of the viewModel
// so I can get a stack of changes
states.push(ko.toJS(track.myViewModel));
target.isDirty(newValue != target.originalValue);
target.originalValue = newValue;
});
}
return target;
};
Then I apply the extender to an object into the viewModel:
this.myViewModel = {
label: ko.observable("Label").extend({ trackChange: this });
}
And when I want to undo an action, I do this:
ko.applyBindings(ko.mapping.fromJS(states[statesPointer]));
This is ok in order to get the old values, but the extend function in the observable is lost so new changes are not saved in the "states" stack.
Suggestions?
Thanks in advance,
Elian.
I think you should not create new model instance performing undo op, but just update observable properties of existing model:
// you do:
// ko.applyBindings(ko.mapping.fromJS(states[statesPointer]));
// I would do:
ko.mapping.fromJS(states[statesPointer], myViewModel);
Looks like your myViewModel has own context, so you have to modify my code to get correct ref to the model.
Related
I have a demo application where all the list items are observables. To achieve this I have used the mapping plugin on the array from which the list is created.
this.allItems = ko.mapping.fromJS([
{name:"Annabelle"},
{name:"Vertie"},
{name:"Charles"},
{name:"John"}
]);
My question is - how to properly implement this kind of behavior in Knockout JS: whenever a property of an observable(any item of the list) changes a sort function is executed:
this.sortList = function() {
this.allItems(this.allItems().sort(function(a, b) { return a.name() > b.name();}).slice(0, 2));
};
First, you need to declare a view model class. Then, declare allItems as an observable array, but display to the user a different member, called say sortedItems, which should be a computed field. This computed field will use allItems internally, so KO will notice when allItems gets updated and will re-evaluate sortedItems too.
function AppViewModel() {
var self = this;
this.allItems = ko.observableArray([
{name:"Annabelle"},
{name:"Vertie"},
{name:"Charles"},
{name:"John"}
]);
this.sortedItems = ko.computed(function() {
return self.allItems().sort(sortFunction);
}, this);
}
I'm currently developing my first Backbone single page app project and I'm facing an issue.
Basically I have a menu (html select input element) implemented as a View. Its value is used to control pretty much every other data requests since it specifies which kind of data to show in the other Views.
Right now I handle the DOM event and trigger a global event so that every model can catch it and keep track internally of the new value. That's because that value is then needed when requesting new data. But this doesn't look like a good solution because A) I end up writing the same function (event handler) in every model and B) I get several models with the same variable.
var Metrics = Backbone.Collection.extend({
url: "dummy-metrics.json",
model: MetricsItem,
initialize: function () {
this.metric = undefined;
},
setMetric: function (metric) {
this.metric = metric;
globalEvents.trigger("metric:change", this.get(metric));
}
});
var GlobalComplexity = Backbone.Collection.extend({
url: function () {
var url = "http://asd/global.json?metric=" + this.metric;
return url;
}, //"dummy-global.json",
model: GlobalComplexyItem,
initialize: function () {
this.metric = undefined;
this.listenTo(globalEvents, "metric:change", this.updateMetric);
},
updateMetric: function (metric) {
this.metric = metric.get("id");
this.fetch({ reset: true });
}
});
All my other Collections are structured like GlobalComplexity.
What's the cleanest way to solve this problem?
Thank you very much.
Define a global parametersManager. Export an instance (singleton) then require it when you need it.
On "globalupdate" you update the parametersManager then trigger "update" for all your model/collections so they'll look what are the current parameters in the parametersManager.
OK, following the suggestion from PW Kad I'm splitting this part of the question off from where it started on question ID 17973991.
I have a viewmodel that utilises a datacontext built around breeze and it fetches the data I want and populates observable arrays. I have a requirement to use data already retrieved by Breeze to populate another (observable) array to use in a treeview.
As the existing data does not have the correct fieldnames, I need to be able to create a new array with correct fieldnames that the dynatree/fancytree plugin can use.
My first attempt: (subsequently shown to not work so don't do this!)
So in my viewmodel I added the following at the top of the .js file:
var treeMaterials = ko.observableArray();
var treeMaterial = function (data) {
var self = this;
self.name = ko.observable(data.name);
self.id = ko.observable(data.id);
self.children = ko.observableArray();
$.each(data.children, function (index, item) {
self.children.push(new Person(item));
});
};
I then added an "asTreeMaterials" method to my module:
var asTreeMaterials = function (treeMatsObservable, matsObservable) {
treeMatsObservable([]); //clear out array as we're rebuilding it in here
var tmpArray = treeMatsObservable(); //create local temp array to avoid ko notifications on each push
$.each(matsObservable, function (index, mat) {
tmpArray.push(new treeMaterial({
id: mat.id,
name: mat.materialName,
children: []
}));
});
treeMatsObservable(tmpArray);
};
(borrowing heavily from John Papa's coding there, thanks John!)
Note: there will be more code going into the "children" bit once I have the basics working
And finally changing the "activate" method to use the new method:
var activate = function () {
// go get local data, if we have it
return datacontext.getMaterialPartials(materials),
asTreeMaterials(treeMaterials, materials);
};
....
and then returning the new array from the module:
var vm = {
activate: activate,
materials: materials,
treeMaterials: treeMaterials,
title: 'My test app page 1',
refresh: refresh
};
means that I don't hit the server again for the treeview version of the data.
Edit 2.
Following the guidance from PW Kad on the other question (will be added to this question shortly) I have modified the "asTreeMaterials" method as follows:
var asTreeMaterials = function () {
treeMaterials([]); //clear out array as we're rebuilding it in here
var matArray = materials().slice();
var tmpArray = [];
$.each(matArray, function (index, mat) {
tmpArray.push(new treeMaterial({
id: mat.id,
name: mat.materialName,
children: []
}));
});
treeMaterials(tmpArray);
};
The reason (I think) I have to create a separate new array is that the existing "materials" observable that I slice does not contain the correct properties. Dynatree/fancytree requires (among other things) an "ID" and a "name". I have the ID, but I have "materialName" in the materials observable hence the "$.each" on the array created by the slicing of the materials observable to push the "materialname" property into the "name" property in my new array (tmpArray). I'm new to all this, I may be miles off the mark here!
Do I actually need an observable array...? I don't think I do if I understand what observable arrays are for... my materials are pretty much set in stone and will change very, very rarely. I presume I can simply leave "treeMaterials" as a standard javascribt object array and return that in the viewmodel instead of making it an observableArray?
Either way, currently the values for materialname and ID are not passed into the relevant properties in the tmpArray I'm making. Instead I'm getting the functions from the materials observable so I think I need to approach this with an "unwrap" of some sort to get at the actual values?
You are not populating the treeMaterials because you don't have any data in materials when you are sending it to asTreeMaterials. I am making some assumptions here but basically it looks like this is what you are trying to do -
At the top of your view model, I assume you have two observableArrays
var treeMaterials = ko.observableArray();
var materials = ko.observableArray();
For your activate method, you need to go get some data, and then when your datacontext returns a promise, go make a tree out of it of some object type -
var activate = function () {
return datacontext.getMaterialPartials(materials).then(
makeMyTree);
};
You don't need to pass treeMaterials or materials because they are within the scope of the view model already, and you are just trying to make a tree of objects out of your materials.
var makeMyTree = function () {
treeMaterials([]);
ko.utils.arrayForEach(materials(), function (mat) {
treeMaterials.push(new treeMaterial(mat));
});
};
This is going to make an observableArray of objects with observable properties, meaning if you are passing them or trying to get their value you would need to use something like treeMaterials()[0].name().
In case your dynatree doesn't take observables, or isn't playing well with them
I am not sure how your dynatree or w/e works with observables, so here is a standard array of non-observable objects instead of an observable array -
var treeMaterials = [];
var makeMyTree = function () {
treeMaterials[];
ko.utils.arrayForEach(materials(), function (mat) {
treeMaterials.push(new treeMaterial(mat));
});
};
var treeMaterial = function (data) {
var self = this;
self.name = data.name;
self.id = data.id;
self.children = [];
$.each(data.children, function (index, item) {
self.children.push(new Person(item));
});
};
Sure this is a very easy question to answer but is there an easy way to determine if any property of a knockout view model has changed?
Use extenders:
ko.extenders.trackChange = function (target, track) {
if (track) {
target.isDirty = ko.observable(false);
target.originalValue = target();
target.setOriginalValue = function(startingValue) {
target.originalValue = startingValue;
};
target.subscribe(function (newValue) {
// use != not !== so numbers will equate naturally
target.isDirty(newValue != target.originalValue);
});
}
return target;
};
Then:
self.MyProperty= ko.observable("Property Value").extend({ trackChange: true });
Now you can inspect like this:
self.MyProperty.isDirty()
You can also write some generic viewModel traversing to see if anything's changed:
self.isDirty = ko.computed(function () {
for (key in self) {
if (self.hasOwnProperty(key) && ko.isObservable(self[key]) && typeof self[key].isDirty === 'function' && self[key].isDirty()) {
return true;
}
}
});
... and then just check at the viewModel level
self.isDirty()
You can subscribe to the properties that you want to monitor:
myViewModel.personName.subscribe(function(newValue) {
alert("The person's new name is " + newValue);
});
This will alert when personName changes.
Ok, so you want to know when anything changes in your model...
var viewModel = … // define your viewModel
var changeLog = new Array();
function catchChanges(property, value){
changeLog.push({property: property, value: value});
viewModel.isDirty = true;
}
function initialiseViewModel()
{
// loop through all the properties in the model
for (var property in viewModel) {
if (viewModel.hasOwnProperty(property)) {
// if they're observable
if(viewModel[property].subscribe){
// subscribe to changes
viewModel[property].subscribe(function(value) {
catchChanges(property, value);
});
}
}
}
viewModel.isDirty = false;
}
function resetViewModel() {
changeLog = new Array();
viewModel.isDirty = false;
}
(haven't tested it - but you should get the idea)
Consider using Knockout-Validation plug-in
It implements the following:
yourProperty.isModified() - Checks if the user modified the value.
yourProperty.originalValue - So you can check if the value really changed.
Along with other validation stuff which comes in handy!
Cheers
You might use the plugin below for this:
https://github.com/ZiadJ/knockoutjs-reactor
The code for example will allow you to keep track of all changes within any viewModel:
ko.watch(someViewModel, { depth: -1 }, function(parents, child) {
alert('New value is: ' + child());
});
PS: As of now this will not work with subscribables nested within an array but a new version that supports it is on the way.
Update: The sample code was upgraded to work with v1.2b which adds support for array items and subscribable-in-subscribable properties.
I had the same problem, i needed to observe any change on the viewModel, in order to send the data back to the server,
If anyone still intersted, i did some research and this is the best solution iv'e managed to assemble:
function GlobalObserver(viewModel, callback) {
var self = this;
viewModel.allChangesObserver = ko.computed(function() {
self.viewModelRaw = ko.mapping.toJS(viewModel);
});
viewModel.allChangesObserver.subscribe(function() {
callback(self.viewModelRaw);
});
self.dispose = function() {
if (viewModel.allChangesObserver)
viewModel.allChangesObserver.dispose();
delete viewModel.allChangesObserver;
};
};
in order to use this 'global observer':
function updateEntireViewModel() {
var rawViewModel = Ajax_GetItemEntity(); //fetch the json object..
//enter validation code here, to ensure entity is correct.
if (koGlobalObserver)
koGlobalObserver.dispose(); //If already observing the older ViewModel, stop doing that!
var viewModel = ko.mapping.fromJS(rawViewModel);
koGlobalObserver = new GlobalObserver(viewModel, Ajax_Submit);
ko.applyBindings(viewModel [ ,optional dom element]);
}
Note that the callback given (in this case 'Ajax_Submit') will be fired on ANY change that occurs on the view model, so i think it's really recommended to make some sort of delay mechanism to send the entity only when the user finished to edit the properties:
var _entitiesUpdateTimers = {};
function Ajax_Submit(entity) {
var key = entity.ID; //or whatever uniquely related to the current view model..
if (typeof _entitiesUpdateTimers[key] !== 'undefined')
clearTimeout(_entitiesUpdateTimers[key]);
_entitiesUpdateTimers[key] =
setTimeout(function() { SendEntityFunction(entity); }, 500);
}
I'm new to JavaScript and the knockout framework, (only yestarday i started to work with this wonderfull framework), so don't get mad at me if i did something wrong.. (-:
Hope this helps!
I've adapted #Brett Green code and extended it so that we can have AcceptChanges, marking the model as not dirty plus having a nicer way of marking models as trackables. Here is the code:
var viewModel = {
name: ko.observable()
};
ko.track(viewModel);
http://jsfiddle.net/david_freire/3HZEu/2/
I did this by taking a snapshot of the view model when the page loads, and then later comparing that snapshot to the current view model. I didn't care what properties changed, only if any changed.
Take a snapshot:
var originalViewModel = JSON.stringify(ko.toJS(viewModel));
Compare later:
if(originalViewModel != JSON.stringify(ko.toJS(viewModel))){
// Something has changed, but we don't know what
}
Consider a view model as follows
function myViewModel(){
var that = this;
that.Name = ko.observable();
that.OldState = ko.observable();
that.NewState = ko.observable();
that.dirtyCalcultions - ko.computed(function(){
// Code to execute when state of an observable changes.
});
}
After you Bind your Data you can store the state using ko.toJS(myViewModel) function.
myViewModel.Name("test");
myViewModel.OldState(ko.toJS(myViewModel));
You can declare a variable inside your view model as a computed observable like
that.dirtyCalculations = ko.computed(function () {});
This computed function will be entered when there is change to any of the other observables inside the view model.
Then you can compare the two view model states as:
that.dirtyCalculations = ko.computed(function () {
that.NewState(that);
//Compare old state to new state
if(that.OldState().Name == that.NewState().Name()){
// View model states are same.
}
else{
// View model states are different.
}
});
**Note: This computed observable function is also executed the first time when the view model is initialized. **
Hope this helps !
Cheers!!
I like Brett Green's solution. As someone pointed out, the isDirty comparison doesn't work with Date objects. I solved it by extending the subscribe method like this:
observable.subscribe(function (newValue) {
observable.isDirty(newValue != observable.originalValue);
if (newValue instanceof Date) {
observable.isDirty(newValue.getTime() != observable.originalValue.getTime());
}
});
I'm looking for the best way to initialize a knockout observable array from some server data (ViewBag), and I want the array contents to be of a javascript type I have defined. Without the requirement of the JS type I could just use:
materialVarieties: ko.observableArray(#Html.Raw(Json.Encode(ViewBag.Materials)))
but I also have a material JS type that I want to use so I can have some extra ViewModel specific properties and functions i.e.:
var material = function(id, name) {
this.id = id;
this.name = name;
this.selected = ko.observable(false);
this.select = function()
{
jQuery.each(processViewModel.materials(), function(index, item)
{
item.selected(false);
});
this.selected(true);
}
}
And then the required initialization becomes:
materialVarieties: ko.observableArray([new material(1, "Apricot"), .....
Currently I build up a string from the ViewBag data and then render that as the initializer like this:
#{ var items = string.Join(",",
((IEnumerable<MaterialVariety>) ViewBag.Materials)
.Select(m => string.Format("new material({0}, {1})",
Json.Encode(m.Id), Json.Encode(m.Name)))); }
var processViewModel = {
material: ko.observableArray([#Html.Raw(items)])
But I'm wondering if there is a cleaner way than the string.Join bit. I could wrap it up in a Helper. What do you do?
I would typically serialize the array first, then map it when putting it in the view model. Would be like:
var originalVarieties = #Html.Raw(Json.Encode(ViewBag.Materials))
var processViewModel = {
materialVarieties: ko.observableArray(ko.utils.arrayMap(originalVarieties, function(variety) {
return new material(variety.id, variety.name);
}))
}
Requires a minor amount of additional processing on the client-side, but seems cleaner than building strings.