I have a knockoutJS observable array which I created in the usual way:
var MyGridModel = function(items) {
var self = this;
this.items = ko.observableArray(items);
...
I would like to change the array to a new one based on new information. I need to change the entire array (or if I can, the entire contents to the new array, whichever is easier).
My attempt to do this is done in the following code:
this.setData = function(newData)
{
var grid = ko.observableArray(newData);
self.items([]);
self.items(grid);
}
Unfortunately, I am getting a blank grid when I perform this operation.
Clearly I am missing something. How do I make this change work?
Someone please advise...
To update the contents of an observable array, just call it and pass the new contents to it:
this.setData = function(newData) {
self.items(newData);
}
Try this:
this.setData = function(newData)
{
self.items.removeAll();
for(int i=0; i<newData.length; i++){
self.items.push(newData[i]);
}
}
Note: Each push will update observers.
Related
I am implementing a cache function in a computed observable.
Is there any way to invalidate the cache below if the items collection differs since the last call?
I have seen examples of dirty checking where a serialized version of the observable is used to determine if the collection has changed, but it's too expensive for me, since there may be hundreds of items.
var itemCache;
var manipulatedItems = ko.pureComputed(function(){
var items = someObervable();
if(!itemCache /* || someObervable.hasChangedSinceLastCall */) {
itemCache = heavyWork(items);
}
return itemCache;
});
var heavyWork = function(items){
// do some heavy computing with items
return alteredItems;
};
In my viewmodel:
myViewModel.itemList = ko.pureComputed(function(){
var result = manipulatedItems();
return result;
});
Since computed observables always cache the last value, there's no reason to store it separately. In fact, storing it separately can cause trouble with getting the latest data in your application.
var manipulatedItems = ko.pureComputed(function () {
var items = someObervable();
return heavyWork(items);
});
var heavyWork = function (items) {
// do some heavy computing with items
return alteredItems;
};
Yes, but you need to use .subscribe and keep the relevant moments in vars inside your own closure. There is no "last-modified-moment" property on observables or inside ko utils to be found.
In your repro, you could do something like this:
var lastChange = new Date();
var lastCacheRebuild = null;
var itemCache;
someObervable.subscribe(function(newObsValue) { lastChange = new Date(); });
var manipulatedItems = ko.pureComputed(function(){
var items = someObervable();
if(!itemCache || !lastCacheRebuild || lastChange > lastCacheRebuild) {
lastCacheRebuild = new Date();
itemCache = heavyWork(items);
}
return itemCache;
});
As far as the repro is concerned you could even put the items = someObservable() bit inside the if block.
PS. This is not recursive, i.e. the subscription is only on someObservable itself, not on the observable properties of things inside that observable. You'd have to manually craft that, which is specific to the structure of someObservable.
I have a multiselect dependency where I display cities and their areas respectively.
The problem is that, both for loops in the function getArea only get called upon the second select of the cities option element.
Note: In debugger it works fine. I think its a scope problem and I tried using the foreach function but to no avail.
I have indicated below the line where the issue occurs.
//Global Variables
var allAreas = new Array();
$(document).ready(function() {
getCities();
$("#sel-city").bind("change", getAreas);
});
//get Cities on reload
function getCities() {
$.getJSON("cities.json", function(json) {
var citySelect = $("#sel-city");
for (var i in json) {
$("#sel-city")
.append($('<option>', {value: json[i].id})
.text(json[i].name));
}
});
}
function getAreas() {
var parentID = $(this).val();
console.log(parentID);
if (allAreas.length == 0) {
$.getJSON("areas.json", function(json) {
for (var i in json) {
allAreas.push(json[i]);
}
});
}
$('option', $("#sel-area")).remove();
var areasByParentID = new Array();
//Loop here
for (var i in allAreas) {
if (allAreas[i].city_id == parentID) {
areasByParentID.push(allAreas[i]);
}
}
console.log(areasByParentID);
//Loop here
for (var k in areasByParentID) {
$("#sel-area")
.append($('<option>', {value: areasByParentID[k].id})
.text(areasByParentID[k].name));
}
}
With the information you gave us and without sample data everything looks fine. But you have some minor mistakes which could lead to bad results. I decided to have a look on it, make some corrections and get it workin correctly with some sample data.
What you should take care of?
use for loops instead of for-in for arrays
you should avoid the new Array() operation
you should cache jquery variables to reduce siteload
for the areas to be set correctly on siteload you should just define init functions
What here could cause an error?
var a = [];
a[5] = 5;
for (var x in a) {
// Shows only the explicitly set index of "5", and ignores 0-4
}
The use of the for-in statement is to enumerate over object properties and will even inherited properties. Depending on your data it could give wrong results as shown in my example.
here is a jsfiddle with sample data.
I'm looking for a way to sort an array inside an Angular service, and still retain the correct bindings in the controller.
If I skip the sorting, the bindings work great, but the array isn't ordered as I need it to be.
Whenever I perform the sort using Lodash's _.sortBy or angular's $filter('orderBy') service, one of two things happens:
The array in the service is sorted correctly, but the binding to the controller is severed due to it no longer referencing the same array anymore.
If I attempt to fix this by using Lodash's _.cloneDeep or angular's angular.copy, the browser freezes due to circular references (?).
Service.js
angular.module('exampleapp')
.factory('ClientFeedService', function($filter, $firebase, FIREBASE_URL, FeedItemService) {
return function(clientId) {
var ClientFeedService = this;
var ref = new Firebase(FIREBASE_URL + 'feeds/clients/' + clientId);
var initialDataLoaded = false;
ClientFeedService.feedArray = [];
ClientFeedService.sortItems = function() {
// Sorting logic here
};
/**
* Bind to the initial payload from Firebase
*/
ref.once('value', function() {
// Sort items after initial payload
ClientFeedService.sortItems();
initialDataLoaded = true;
});
/**
* Bind to new items being added to Firebase
*/
ref.on('child_added', function(feedItemSnap) {
console.log('child_added');
ClientFeedService.feedArray.unshift(FeedItemService.find(feedItemSnap.name(), feedItemSnap.val()));
// Sort after new item if initial payload loaded
if (initialDataLoaded) {
ClientFeedService.sortItems();
}
});
ClientFeedService.getFeedItems = function() {
return ClientFeedService.feedArray;
};
return ClientFeedService;
};
});
Controller.js
app.controller('ClientsFeedCtrl', function($scope, $stateParams, ClientFeedService) {
var clientId = $stateParams.clientId;
$scope.clientFeed = new ClientFeedService(clientId).getFeedItems();
});
There are a couple of ways that you can solve this. First, let's look at what is happening.
You are assigning the initial array to $scope.cliendFeed. After this, as data is added, a new Array is being generated and stored in the Service, but you still have a reference to the original Array. So ultimately, what you want to do is find a way to keep $scope.clientFeed in sync with your service.
The simplest solution is probably to use a getter method instead of storing a reference to the array in your scope.
In order to do this, you would have to add something like this:
var service = new ClientFeedService(clientId);
$scope.getClientFeed = function () {
return service.getFeedItems();
};
And make sure your ng-repeat called this function:
<li ng-repeat="item in getClientFeed()">...</li>
Hope that helps!
You can push the new data returned from API to the same array in the controller and then apply the $filter
Here is example
function getData(){
$scope.array.push(returnData);
sortArrayList($scope.orderByField, $scope.reverseSort);
}
function sortArrayList(orderByField, reverseSort){
$scope.array = $filter('orderBy')($scope.array, orderByField, reverseSort);
}
IS there a way to check a dirty flag on the model itself, independent of the view?
I need the angular controller to know what properties have been changed, in order to only save changed variables to server.
I have implemented logic regarding if my entire form is dirty or pristine, but that is not specific enough
I could just slap a name and ng-form attribute on every input, to make it recognizable as a form in the controller, but then I end up with a controller that is strongly coupled with the view.
Another not-so appealing approach is to store the initial values that every input is bound to in a separate object, then compare the current values with the initial values to know if they have changed.
I checked Monitor specific fields for pristine/dirty form state and AngularJS : $pristine for ng-check checked inputs
One option I could think of is
As you get a model/object from service, create a replica of the model within the model and bind this new model to your view.
Add a watch on the new Model and as the model changes, use the replica to compare old and new models as follows
var myModel = {
property1: "Property1",
property2: "Property2",
array1:["1","2","3"]
}
var getModel = function(myModel){
var oldData = {};
for(var prop in myModel){
oldData.prop = myModel[prop];
}
myModel.oldData = oldData;
return myModel;
}
var getPropChanged = function(myModel){
var oldData = myModel.oldData;
for(var prop in myModel){
if(prop !== "oldData"){
if(myModel[prop] !== oldData[prop]){
return{
propChanged: prop,
oldValue:oldData[prop],
newValue:myModel[prop]
}
}
}
}
}
You may find it easiest to store and later compare against the JSON representation of the object, rather than looping through the various properties.
See Detect unsaved data using angularjs.
The class shown below may work well for your purpose, and is easily reused across pages.
At the time you load your models, you remember their original values:
$scope.originalValues = new OriginalValues();
// Set the model and remember it's value
$scope.someobject = ...
var key = 'type-' + $scope.someobject.some_unique_key;
$scope.originalValues.remember(key, $scope.someobject);
Later you can determine if it needs to be saved using:
var key = 'type-' + $scope.someobject.some_unique_key;
if ($scope.originalValues.changed(key, $scope.someobject)) {
// Save someobject
...
}
The key allows you to remember the original values for multiple models. If you only have one ng-model the key can simply be 'model' or any other string.
The assumption is that properties starting with '$' or '_' should be ignored when looking for changes, and that new properties will not be added by the UI.
Here's the class definition:
function OriginalValues() {
var hashtable = [ ]; // name -> json
return {
// Remember an object returned by the API
remember: function(key, object) {
// Create a clone, without system properties.
var newobj = { };
for (var property in object) {
if (object.hasOwnProperty(property) && !property.startsWith('_') && !property.startsWith('$')) {
newobj[property] = object[property];
}
}
hashtable[key] = newobj;
},// remember
// See if this object matches the original
changed: function(key, object) {
if (!object) {
return false; // Object does not exist
}
var original = hashtable[key];
if (!original) {
return true; // New object
}
// Compare against the original
for (var property in original) {
var changed = false;
if (object[property] !== original[property]) {
return true; // Property has changed
}
}
return false;
}// changed
}; // returned object
} // OriginalValues
I'm trying to make the {{#each}} helper to iterate over an object, like in vanilla handlebars. Unfortunately if I use #each on an object, Ember.js version gives me this error:
Assertion failed: The value that #each loops over must be an Array. You passed [object Object]
I wrote this helper in attempt to remedy this:
Ember.Handlebars.helper('every', function (context, options) {
var oArray = [];
for (var k in context) {
oArray.push({
key : k,
value : context[k]
})
}
return Ember.Handlebars.helpers.each(oArray, options);
});
Now, when I attempt to use {{#every}}, I get the following error:
Assertion failed: registerBoundHelper-generated helpers do not support use with Handlebars blocks.
This seems like a basic feature, and I know I'm probably missing something obvious. Can anyone help?
Edit:
Here's a fiddle: http://jsfiddle.net/CbV8X/
Use {{each-in}} helper. You can use it like like {{each}} helper.
Example:
{{#each-in modelWhichIsObject as |key value|}}
`{{key}}`:`{{value}}`
{{/each-in}}
JS Bin demo.
After fiddling with it for a few hours, I came up with this hacky way:
Ember.Handlebars.registerHelper('every', function(context, options) {
var oArray = [], actualData = this.get(context);
for (var k in actualData) {
oArray.push({
key: k,
value: actualData[k]
})
}
this.set(context, oArray);
return Ember.Handlebars.helpers.each.apply(this,
Array.prototype.slice.call(arguments));
});
I don't know what repercussions this.set has, but this seems to work!
Here's a fiddle: http://jsfiddle.net/CbV8X/1/
I've been after similar functionality, and since we're sharing our hacky ways, here's my fiddle for the impatient: http://jsfiddle.net/L6axcob8/1/
This fiddle is based on the one provided by #lxe, with updates by #Kingpin2k, and then myself.
Ember: 1.9.1, Handlebars: 2.0.0, jQuery 2.1.3
Here we are adding a helper called every which can iterate over objects and arrays.
For example this model:
model: function() {
return {
properties: {
foo: 'bar',
zoo: 'zar'
}
};
}
can be iterated with the following handlebars template:
<ul class="properties">
{{#every p in properties}}
<li>{{p.key}} : {{p.value}}</li>
{{/every}}
</ul>
every helper works by creating an array from the objects keys, and then coordinating changes to Ember by way of an ArrayController. Yeah, hacky. This does however, let us add/remove properties to/from an object provided that object supports observation of the [] property.
In my use case I have an Ember.Object derived class which notifies [] when properties are added/removed. I'd recommend looking at Ember.Set for this functionality, although I see that Set been recently deprecated. As this is slightly out of this questions scope I'll leave it as an exercise for the reader. Here's a tip: setUnknownProperty
To be notified of property changes we wrap non-object values in what I've called a DataValueObserver which sets up (currently one way) bindings. These bindings provide a bridge between the values held by our internal ArrayController and the object we are observing.
When dealing with objects; we wrap those in ObjectProxy's so that we can introduce a 'key' member without the need to modify the object itself. Why yes, this does imply that you could use #every recursively. Another exercise for the reader ;-)
I'd recommend having your model be based around Ember.Object to be consistent with the rest of Ember, allowing you to manipulate your model via its get & set handlers. Alternatively, as demonstrated in the fiddle, you can use Em.Get/Em.set to access models, as long as you are consistent in doing so. If you touch your model directly (no get/set), then every won't be notified of your change.
Em.set(model.properties, 'foo', 'asdfsdf');
For completeness here's my every helper:
var DataValueObserver = Ember.Object.extend({
init: function() {
this._super();
// one way binding (for now)
Em.addObserver(this.parent, this.key, this, 'valueChanged');
},
value: function() {
return Em.get(this.parent, this.key);
}.property(),
valueChanged: function() {
this.notifyPropertyChange('value');
}
});
Handlebars.registerHelper("every", function() {
var args = [].slice.call(arguments);
var options = args.pop();
var context = (options.contexts && options.contexts[0]) || this;
Ember.assert("Must be in the form #every foo in bar ", 3 == args.length && args[1] === "in");
options.hash.keyword = args[0];
var property = args[2];
// if we're dealing with an array we can just forward onto the collection helper directly
var p = this.get(property);
if (Ember.Array.detect(p)) {
options.hash.dataSource = p;
return Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
}
// create an array that we will manage with content
var array = Em.ArrayController.create();
options.hash.dataSource = array;
Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
//
var update_array = function(result) {
if (!result) {
array.clear();
return;
}
// check for proxy object
var result = (result.isProxy && result.content) ? result.content : result;
var items = result;
var keys = Ember.keys(items).sort();
// iterate through sorted array, inserting & removing any mismatches
var i = 0;
for ( ; i < keys.length; ++i) {
var key = keys[i];
var value = items[key];
while (true) {
var old_obj = array.objectAt(i);
if (old_obj) {
Ember.assert("Assume that all objects in our array have a key", undefined !== old_obj.key);
var c = key.localeCompare(old_obj.key);
if (0 === c) break; // already exists
if (c < 0) {
array.removeAt(i); // remove as no longer exists
continue;
}
}
// insert
if (typeof value === 'object') {
// wrap object so we can give it a key
value = Ember.ObjectProxy.create({
content: value,
isProxy: true,
key: key
});
array.insertAt(i, value);
} else {
// wrap raw value so we can give it a key and observe when it changes
value = DataValueObserver.create({
parent: result,
key: key,
});
array.insertAt(i, value);
}
break;
}
}
// remove any trailing items
while (array.objectAt(i)) array.removeAt(i);
};
var should_display = function() {
return true;
};
// use bind helper to call update_array if the contents of property changes
var child_properties = ["[]"];
var preserve_context = true;
return Ember.Handlebars.bind.call(context, property, options, preserve_context, should_display, update_array, child_properties);
});
Inspired by:
How can I make Ember.js handlebars #each iterate over objects?
http://mozmonkey.com/2014/03/ember-getting-the-index-in-each-loops/
https://github.com/emberjs/ember.js/issues/4365
https://gist.github.com/strathmeyer/1371586
Here's that fiddle again if you missed it:
http://jsfiddle.net/L6axcob8/1/