Angular: Update one-time binded data - javascript

Suppose you have in your directive controller an array like
this.items = [
{active: true, title: 'bar', ...},
{active: false, title: 'foo', ...},
...
];
And in the template you visualise it
<ol>
<li ng-repeat="item in ::items" ng-class="...">{{item.title}}</li>
</ol>
Note the :: which makes sure that it does this only once, because the directive injects some DOM elements too after ng-repeat is ready.
DEMO
However, at some point in time the app receives an update
this.items = updatedItems;
The update is identical to the original data except for some of the properties of each item (active might change from true to false for example). Now, the update will not do anything because of the ::. Now I can iterate through every item
updatedItems.forEach((item, index) => {
$scope.items[index].active = item.active;
$scope.items[index].title = item.title;
...
});
So the question is what would be the best approach to this problem ?

How about having a copy of the dataset and data binding that, instead of the actual dataset, changes to which you want the template to ignore, all be it for a particular condition.
Then when you would want the data to be updated you can just update the necessary changes to the copied data and the changes will be reflected. I understand, that does add the overhead of having a copy of the data but a simple solution can often save from a lot of head scratching later, especially if the source is being looked at from a different member of the team.

Related

AngularJS: trigger event after nested ng-repeat with filter is complete

I have a very big JSON in my controller fetched from a MongoDB database. Here's its structure (haven't added irrelevant keys):
$scope.keyword; //ignore for now explained below
$scope.data = [
{"key" : [
{"nested_key": //some value},
{"nested_key": //some value} //so on
]
},
{"key" : [
{"nested_key": //some value},
{"nested_key": //some value}
]
},
{"key" : [
{"nested_key": //some value},
{"nested_key": //some value}
]
}
]
Here's my HTML template displaying the JSON:
<div ng-repeat="outer in data">
<div ng-repeat="inner in outer.key | keywordFilter: keyword">
//print inner values
</div>
</div>
Here, 'keyword' holds values which will be used to filter the inner ng-repeat loop. Here's my filter:
app.filter('keywordFilter', function() {
return function(collection, keyword) {
//Iteration over the entire collection. If keyword exists add the item to output
return output;
}
})
Now, as expected, the filter runs whenever I modify '$scope.keyword'. It is updated when I click on a button as follows:
HTML:
<input type="text" ng-model="temp" />
<button ng-click="updateKeyword()">
Controller:
$scope.updateKeyword = function() {
$scope.keyword = $scope.temp;
}
The Problem:
Due to the sheer size of data, the filtering process is taking around 10-15 seconds (sometimes the browser hangs!) and THEN the new filtered data returned from the filter is displayed on the screen.
The Requirement:
What I want to do is that during these 10-15 seconds I want to show a loader and after the calculations are done, the news is displayed. How do I achieve this?
What I tried:
I figured that once I click on the filter button, I would need to wait for the ng-repeat loop to finish and thus I tried triggering an event on the finish of ng-repeat by referring this thread.
But what I found out is that the event is triggered ONLY when the data is displayed for the first time and not when the filter button is clicked and the data is filtered by the keyword filter.
You could maybe simply invalidate you action when angular is in digest:
in your controller:
function isInDigest(){
$scope.$$phase ? true : false;
}
$scope.updateKeyword = function() {
if(isInDigest()){ return; }
$scope.keyword = $scope.temp;
}
and in your template
<button ng-class="{'inactive': isInDigest()}" ng-click="updateKeyword()">
Just use angular's $timeout, which runs the function it receives as parameter after angular has manipulated and the browser has rendered the DOM.
For more info, I recommend this and this stackoverflow answer discussing some timings in connection with $timeout and $evalAsync, with some very authoritative references.
But I concur with estus' comment: this might not be the architecture you're looking for, use javascript in the controller or a directive to leave less work for ng-repeat, or use lazy loading mechanisms if your data is so large.

Knockout component updating observable not being subscribed to by parent view model?

I've written a component called Upload which allows users to upload files and then report back with a JSON object with these files. In this particular instance, the Upload component has a parameter which comes from a parent view model:
<upload params="dropzoneId: 'uploadFilesDropzone', postLocation: '/create/upload', uploadedFiles: uploadedFiles"></upload>
The one of importance is called uploadedFiles. The parameter binding here means I can reference params.uploadedFiles on my component and .push() new objects onto it as they get uploaded. The data being passed, also called uploadedFiles, is an observableArray on my parent view model:
var UploadViewModel = function () {
// Files ready to be submitted to the queue.
self.uploadedFiles = ko.observableArray([]);
};
I can indeed confirm that on my component, params.uploadedFiles is an observableArray, as it has a push method. After altering this value on the component, I can console.log() it to see that it has actually changed:
params.uploadedFiles.push(object);
console.log(params.uploadedFiles().length); // was 0, now returns 1
The problem is that this change does not seem to be reflected on my parent viewmodel. self.uploadedFiles() does not change and still reports a length of 0.
No matter if I add a self.uploadedFiles.subscribe(function(newValue) {}); subscription in my parent viewmodel.
No matter if I also add a params.uploadedFiles.valueHasMutated() method onto my component after the change.
How can I get the changes from my array on my component to be reflected in the array on my parent view model?
Why do you create a new observable array when the source already is one? You can't expect a new object to have the same reference as another one: simply pass it to your component viewModel as this.uploads = params.uploads. In the below trimmed-down version of your example, you'll see upon clicking the Add button that both arrays (well the same array referenced in different contexts) stay in sync.
ko.components.register('upload', {
viewModel: function(params) {
this.uploads = params.uploads;
this.addUpload = function() { this.uploads.push('item'); }.bind(this);
},
template: [
'<div><button type="button" data-bind="click: addUpload">Add upload</button>',
'<span data-bind="text: uploads().length + \' - \' + $root.uploads().length"></span></div>'].join('')
});
var app = {
uploads: ko.observableArray([])
};
ko.applyBindings(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: {name: 'upload', params: {uploads: uploads}}"></div>
It is only in case your source array is not observable that things get a little more complicated and you need to have a manual subscription to update the source, eg. you would insert the following in the viewModel:
this.uploads.subscribe(function(newValue) { params.uploads = newValue; });
Additionally the output in the text binding would not be updated for the source because it is not observable. If for some reason that I cannot conceive of you would want to have 2 different observableArrays (1 source & 1 component), you should still be able to do with the line above, but replace the function code with params.uploads(newValue)
The problem may be related to this bug (to be confirmed): https://github.com/knockout/knockout/issues/1863
Edit 1: So this was not a bug. You have to unwrap the raw param to access the original observable. In your case, it would be:
params.$raw.uploadedFiles() //this would give you access to the original observableArray and from there, you can "push", "remove", etc.
The problem is that when you pass a param to a component, it gets wrapped in a computed observable and when you unwrap it, you don't have the original observableArray.
Reference: http://knockoutjs.com/documentation/component-custom-elements.html#advanced-accessing-raw-parameters
While Binding Property that involves Parent --> Child Relation
Use Binding in this way
If You want to bind data to Child Property
data-bind='BindingName : ParentViewmodel.ChildViewModel.ObservableProperty'
Here it seems you want to subscibe to a function when any data is pushed in Array for that you can write subscribe on Length of Observable array which can help you capture event that you want.
This should solve your problem.

AngularJS: ng-repeat with dynamic list, without rebuilding entire DOM tree?

I'm using ng-repeat on a table row with data from a JSON array retrieved from a server. My goal is to have the list update automatically whenever an item is added, removed, or modified on the server, without affecting the unmodified items. In the final implementation, these table rows will also contain bidirectionally bound <input> and <select> elements to send updates back to the server. Some of the available options in the <select> elements will also be generated using ng-repeat directives from another list that may also change.
So far, every time a new array comes from the server (currently polled every two seconds), the entire ng-repeat list is deleted and regenerated. This is problematic because it interferes with text selection, destroys input fields even if they are currently being edited by the user, and probably runs a lot more slowly than necessary.
I've written other web apps that do what I want using jQuery and DOM manipulation, but the code ends up being really hairy and development is time consuming. I'm hoping to use AngularJS and data binding to accomplish this in a fraction of the code and time.
So here's the question: is it possible to update the backing array this way, but only modify the DOM elements corresponding to the items/properties that actually changed?
Here's a minimal test case that simulates periodic polling using a hard-coded array in a timer (see it live at http://jsfiddle.net/DWrmP/). Notice that the text selection is cleared every 500ms due to the elements being deleted and recreated.
HTML
<body ng-app="myApp">
<table ng-controller="MyController">
<tr ng-repeat="item in items | orderBy:'id'">
<td>{{item.id}}</td>
<td>{{item.data}}</td>
</tr>
</table>
</body>
JavaScript
angular.module('myApp', []).controller(
'MyController', [
'$scope', '$timeout',
function($scope, $timeout) {
$scope.items = [
{ id: 0, data: 'Zero' }
];
function setData() {
$scope.items = [
{ id: 1, data: 'One' },
{ id: 2, data: 'Two' },
{ id: 5, data: 'Five' },
{ id: 4, data: 'Four' },
{ id: 3, data: 'Three' }
];
$timeout(setData, 500);
}
$timeout(setData, 500);
}
]
);
For those finding this from google, the page below describes a feature in AngularJS 1.2 that helps with this problem:
http://www.bennadel.com/blog/2556-Using-Track-By-With-ngRepeat-In-AngularJS-1-2.htm
Edit to add: The most important sentences from the linked post, in case the link ever dies:
With the new "track by" syntax, I can now tell AngularJS which object property (or property path) should be used to associate a JavaScript object with a DOM node. This means that I can swap out JavaScript objects without destroying DOM nodes so long as the "track by" association still works.
I belive this article will explain how ngRepeat works
http://www.bennadel.com/blog/2443-Rendering-DOM-Elements-With-ngRepeat-In-AngularJS.htm
So if you are keeping objects in collection - then yes ( i e $hashKey persist )
Otherwise - no
I'm planning to build the following solution myself eventually though it's still in my product backlog.
The problem with ng-repeat is it will remove items from the DOM when it needs to so for a table it would mean it will resize and such, but if the data is dynamic, it may flicker because the data changed around and the table size is shifting. Particularly during paging because the whole page may not have loaded yet.
To get around this flickering, the table must not change its number of rows. Instead have an ng-repeat of "displayed" data and just change it as needed without adding or removing items from the array.

Missing view.context or templateContext in an Ember event handler

I'm trying to push the object that populated a view into an array, but the reference is somehow getting lost. I've got an Ember view, with a defined eventManager:
FrontLine.NewProductButton = Em.View.extend({
tagName: 'button',
classNames: ['addtl_product',],
templateName: 'product-button',
eventManager: Ember.Object.create({
click: function(event, view) {
FrontLine.ProductsController.toggleProductToCustomer(event, view);
}
})
})
That view renders a bunch of buttons that are rendered with properties that come from objects in the ProductsController using the #each helper. That part works great. And when I click on any of those buttons, the click event is firing and doing whatever I ask, including successfully calling the handler function (toggleProductToCustomer) I've designated from my ProductsController:
FrontLine.ProductsController = Em.ArrayController.create({
content: [],
newProduct: function(productLiteral) {
this.pushObject(productLiteral);
},
toggleProductToCustomer: function(event, view){
FrontLine.CustomersController.currentCustomer.productSetAdditional.pushObject(view.context);
}
});
I'm trying to use that function to push the object whose properties populated that view into an array. Another place in my app (a simple search field), that works perfectly well, using pushObject(view.context). Here, however, all that gets pushed into the array is undefined. I tried using view.templateContext instead, but that doesn't work any better. When I try console.log-ing the button's view object from inside those functions, I get what I'd expect:
<(subclass of FrontLine.NewProductButton):ember623>
But either view.context or view.templateContext return undefined. How do I access the object I'm after, so I can add it to my array?
The simple answer is that it was one letter's difference:
view.content
or:
view.get('content')
provides the source object in that particular situation, rather than view.context.
(My only real challenge with Ember so far is that accessors for objects and properties vary so much from situation to situation, and there's no real documentation for that. Sometimes the object is at view.context, sometimes it's at view.content, sometimes _parentView.content, etc., etc. It would be awesome if there were a chart with the umpteen different syntaxes for accessing the same data, depending on which particular aperture you're reaching through to get it. I'm still discovering them...)

How to clear/remove observable bindings in Knockout.js?

I'm building functionality onto a webpage which the user can perform multiple times. Through the user's action, an object/model is created and applied to HTML using ko.applyBindings().
The data-bound HTML is created through jQuery templates.
So far so good.
When I repeat this step by creating a second object/model and call ko.applyBindings() I encounter two problems:
The markup shows the previous object/model as well as the new object/model.
A javascript error occurs relating to one of the properties in the object/model, although it's still rendered in the markup.
To get around this problem, after the first pass I call jQuery's .empty() to remove the templated HTML which contains all the data-bind attributes, so that it's no longer in the DOM. When the user starts the process for the second pass the data-bound HTML is re-added to the DOM.
But like I said, when the HTML is re-added to the DOM and re-bound to the new object/model, it still includes data from the the first object/model, and I still get the JS error which doesn't occur during the first pass.
The conclusion appears to be that Knockout is holding on to these bound properties, even though the markup is removed from the DOM.
So what I'm looking for is a means of removing these bound properties from Knockout; telling knockout that there is no longer an observable model. Is there a way to do this?
EDIT
The basic process is that the user uploads a file; the server then responds with a JSON object, the data-bound HTML is added to the DOM, then the JSON object model is bound to this HTML using
mn.AccountCreationModel = new AccountViewModel(jsonData.Account);
ko.applyBindings(mn.AccountCreationModel);
Once the user has made some selections on the model, the same object is posted back to the server, the data-bound HTML is removed from then DOM, and I then have the following JS
mn.AccountCreationModel = null;
When the user wishes to do this once more, all these steps are repeated.
I'm afraid the code is too 'involved' to do a jsFiddle demo.
Have you tried calling knockout's clean node method on your DOM element to dispose of the in memory bound objects?
var element = $('#elementId')[0];
ko.cleanNode(element);
Then applying the knockout bindings again on just that element with your new view models would update your view binding.
For a project I'm working on, I wrote a simple ko.unapplyBindings function that accepts a jQuery node and the remove boolean. It first unbinds all jQuery events as ko.cleanNode method doesn't take care of that. I've tested for memory leaks, and it appears to work just fine.
ko.unapplyBindings = function ($node, remove) {
// unbind events
$node.find("*").each(function () {
$(this).unbind();
});
// Remove KO subscriptions and references
if (remove) {
ko.removeNode($node[0]);
} else {
ko.cleanNode($node[0]);
}
};
You could try using the with binding that knockout offers:
http://knockoutjs.com/documentation/with-binding.html
The idea is to use apply bindings once, and whenever your data changes, just update your model.
Lets say you have a top level view model storeViewModel, your cart represented by cartViewModel,
and a list of items in that cart - say cartItemsViewModel.
You would bind the top level model - the storeViewModel to the whole page. Then, you could separate the parts of your page that are responsible for cart or cart items.
Lets assume that the cartItemsViewModel has the following structure:
var actualCartItemsModel = { CartItems: [
{ ItemName: "FirstItem", Price: 12 },
{ ItemName: "SecondItem", Price: 10 }
] }
The cartItemsViewModel can be empty at the beginning.
The steps would look like this:
Define bindings in html. Separate the cartItemsViewModel binding.
<div data-bind="with: cartItemsViewModel">
<div data-bind="foreach: CartItems">
<span data-bind="text: ItemName"></span>
<span data-bind="text: Price"></span>
</div>
</div>
The store model comes from your server (or is created in any other way).
var storeViewModel = ko.mapping.fromJS(modelFromServer)
Define empty models on your top level view model. Then a structure of that model can be updated with
actual data.
storeViewModel.cartItemsViewModel = ko.observable();
storeViewModel.cartViewModel = ko.observable();
Bind the top level view model.
ko.applyBindings(storeViewModel);
When the cartItemsViewModel object is available then assign it to the previously defined placeholder.
storeViewModel.cartItemsViewModel(actualCartItemsModel);
If you would like to clear the cart items:
storeViewModel.cartItemsViewModel(null);
Knockout will take care of html - i.e. it will appear when model is not empty and the contents of div (the one with the "with binding") will disappear.
I have to call ko.applyBinding each time search button click, and filtered data is return from server, and in this case following work for me without using ko.cleanNode.
I experienced, if we replace foreach with template then it should work fine in case of collections/observableArray.
You may find this scenario useful.
<ul data-bind="template: { name: 'template', foreach: Events }"></ul>
<script id="template" type="text/html">
<li><span data-bind="text: Name"></span></li>
</script>
Instead of using KO's internal functions and dealing with JQuery's blanket event handler removal, a much better idea is using with or template bindings. When you do this, ko re-creates that part of DOM and so it automatically gets cleaned. This is also recommended way, see here: https://stackoverflow.com/a/15069509/207661.
I think it might be better to keep the binding the entire time, and simply update the data associated with it. I ran into this issue, and found that just calling using the .resetAll() method on the array in which I was keeping my data was the most effective way to do this.
Basically you can start with some global var which contains data to be rendered via the ViewModel:
var myLiveData = ko.observableArray();
It took me a while to realize I couldn't just make myLiveData a normal array -- the ko.oberservableArray part was important.
Then you can go ahead and do whatever you want to myLiveData. For instance, make a $.getJSON call:
$.getJSON("http://foo.bar/data.json?callback=?", function(data) {
myLiveData.removeAll();
/* parse the JSON data however you want, get it into myLiveData, as below */
myLiveData.push(data[0].foo);
myLiveData.push(data[4].bar);
});
Once you've done this, you can go ahead and apply bindings using your ViewModel as usual:
function MyViewModel() {
var self = this;
self.myData = myLiveData;
};
ko.applyBindings(new MyViewModel());
Then in the HTML just use myData as you normally would.
This way, you can just muck with myLiveData from whichever function. For instance, if you want to update every few seconds, just wrap that $.getJSON line in a function and call setInterval on it. You'll never need to remove the binding as long as you remember to keep the myLiveData.removeAll(); line in.
Unless your data is really huge, user's won't even be able to notice the time in between resetting the array and then adding the most-current data back in.
I had a memory leak problem recently and ko.cleanNode(element); wouldn't do it for me -ko.removeNode(element); did. Javascript + Knockout.js memory leak - How to make sure object is being destroyed?
Have you thought about this:
try {
ko.applyBindings(PersonListViewModel);
}
catch (err) {
console.log(err.message);
}
I came up with this because in Knockout, i found this code
var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
if (!sourceBindings) {
if (alreadyBound) {
throw Error("You cannot apply bindings multiple times to the same element.");
}
ko.utils.domData.set(node, boundElementDomDataKey, true);
}
So to me its not really an issue that its already bound, its that the error was not caught and dealt with...
I have found that if the view model contains many div bindings the best way to clear the ko.applyBindings(new someModelView); is to use: ko.cleanNode($("body")[0]); This allows you to call a new ko.applyBindings(new someModelView2); dynamically without the worry of the previous view model still being binded.
<div id="books">
<ul data-bind="foreach: booksImReading">
<li data-bind="text: name"></li>
</ul>
</div>
var bookModel = {
booksImReading: [
{ name: "Effective Akka" },
{ name: "Node.js the Right Way" }]
};
ko.applyBindings(bookModel, el);
var bookModel2 = {
booksImReading: [
{ name: "SQL Performance Explained" },
{ name: "Code Connected" }]
};
ko.cleanNode(books);
ko.applyBindings(bookModel2, books);

Categories

Resources