I got a component hierarchy that looks a bit like this:
#Component({
...
})
export class A {
constructor(private _serviceThatDeliversData: Service){}
getData(): Array<DataItem> {
return this._serviceThatDeliversData.getData();
}
}
html:
<b-selector [data]="getData()"></b-selector>
Child component:
#Component({
selector: 'b-selector'
...
})
export class B {
#Input() data: Array<DataItem>;
}
html:
<ul>
<li *ngFor="let item of data">
<c-selector [item]="item"></c-selector>
</li>
</ul>
So I got a parent component 'A' that receives data from a service. The serviceThatDeliversData creates the DataItem list every time it receives data from a websocket. This list is then passed down to 'B' where every list entry is used as a base for sub components ('C', I omitted the C component here cause it basically just presents the input data 'item').
My problem now is the following:
Since the list changes every time the service gets an update, the whole list of C components + the B component is newly created. I assume because the DataItem list changes completely Angular notices them as new entries (?) and the list as a new one itself (?)
However in reality it might very well be that only one item was added or removed or just a field has changed in one of the items. So only one C component would needed to be removed/added/updated.
Since that is the case any animations happening in the view in any of the C components stop and start again once the list is recreated. That plus some other side effects I would like to avoid.
So my question is, is there a way to let Angular only update the C components which relate to a DataItem that actually changed internally AND the ones that were added or removed to/from the list, but leave the ones that have not changed untouched?
I have searched for a while and found that one could use changeDetection: ChangeDetectionStrategy.OnPush, however I have not found a working example (with a changing array and changing array elements (internally)) that would help me with my problem.
I assume one could store a copy of the list in 'B', check the changes manually and then react to them - which could also be done with an animation to signal the user which entry was removed or added - however that would require a way to stop Angular from updating (recreating) 'B' and the 'C' components at all. I take it using the same array (clearing it and repopulating it with the new entries) might work.
But I'd still like to know if this could be possible via raw Angular mechanisms.
The changedetection strategy OnPush only picks up changes to values decorated with the Input-decorator and only if the reference of the value changes.
For example myArray[1] = 'a'; only mutates the array and no new reference is created, therefore angular with OnPush strategy wouldn't pick up the change. You have to clone the array and make the change to create a new array reference.
Your problem with angular recreating the elements inside an ngForOf directive is descriped in another answer of me. Read the note at the end of the answer.
Related
I've read in another stack overflow post that angular.js tries to be smart when the collection used in ng-repeat changes so that the DOM is not rebuilt completely (see Does ng-repeat retain DOM elements or create all new ones when the collection changes?). However I'm experiencing a problem: When I remove one item from my collection, all items following this one are rebuilt in the DOM. This is problematic for me because I am listing for the jQuery remove event and this event gets fired multiple times, although only one item got removed. Listening for scope.$on('remove') could solve my problem - this gets only triggered once - but here is my problem, that I want to access jQuery data before removing and when scope.$on('remove') is triggered, data got already removed.
What can I do to solve this issue? See this link for a demo: http://plnkr.co/edit/T1ZicU20JjWYk1J5ch7Z?p=preview.
element.bind('remove', function() {
alert("Element remove handler called.");
});
When I remove the third element, remove gets already triggered once, when I remove the second one, it gets triggered twice, when removing the third one, it gets triggered thrice.
Just add the track by to solve this issue :
ng-repeat="w in widgets track by $index"
Explanation:
When you add track by you basically tell angular to generate a single DOM element per data object in the given collection. This could be useful when paging and filtering, or any case where objects are added or removed from ng-repeat list.
Reference: http://www.bennadel.com/blog/2556-using-track-by-with-ngrepeat-in-angularjs-1-2.htm
Updated: In short, my problem seems to be that the DOM stops updating whenever I am calling a function which switches from one state to another in the application.
More interestingly though, this actually only happens the second time this function is called from one particular element.
For specific details, I'm developing an single page app in AngularJS 1.2.28 and one I've stripped my html template, display-widgets.html to contain only 3 elements as follows:
<p data-ng-click="widgetCtrl.onWidgetSelected(widgetCtrl.model.widgetData[0].widget)">widget 1</p>
<p data-ng-click="widgetCtrl.onWidgetSelected(widgetCtrl.model.widgetData[0].widget)">also widget 1</p>
<p data-ng-click="widgetCtrl.onWidgetSelected(widgetCtrl.model.widgetData[1].widget)">widget 2</p>
<p>{{widgetCtrl.interstitialMode}}</p>
The controller looks something like as follows:
widgetShop.widgetCtrl.prototype.showWidgetHome = function (widget) {
this.state.go('widgetHome', {
widgetId: widget.widgetID
});
};
widgetShop.widgetCtrl.prototype.setupInterstitial = function () {
this.interstitialMode = true;
};
widgetShop.widgetCtrl.prototype.onWidgetSelected = function(widget) {
this.setupInterstitial();
this.showWidgetHome(widget);
};
The intended functionality is that after the widget is selected, onWidgetSelected() is called passing the selected widget as a parameter where the animation variable is updated (with the intention to add class with ng-class in the DOM) with setupInterstitial() before another function is called to switch the state. Some interaction will occur on this state, (e.g. add widget to basket) and once complete, the state is switched back to the display-widgets state.
I've stripped the html down here to be just p tags for testing but I've found if you select the same widget(p tag) again, the element is NOT updated in the page as before, and more interesting still is that no DOM updates occur at all. Console logs and alerts show me that angular has updated the correct variables in the controller but the state transition seems to prevent any DOM updates occurring. Selecting the widget 2 will update the DOM as expected, but as with the first widget, this will not occur more than once. Selecting 'also widget 1' after selecting widget 1 will not update the DOM as though it were the second time it were selected, but selecting this before widget 1 will have the same affect as selecting widget 1 first.
Does anybody know if this is a specific issue with browsers not performing animation again because it is a single page app? Or is there likely to be an issue elsewhere in my code? I haven't been able to find anything via google or stack overflow with people having a similar issue.
Sounds like once you've added the class, it is never removed again.
Once the class is added, the animation starts. When you add it again nothing changes because it already had that class and all the styling was already applied.
I'm following the "denormalized" data pattern that #Anant described nicely on the Firebase blog. To query and render child objects he suggests listening for child_added events on the parent to get the child ids, then querying those children individually with .once('value', fnc) to render them. From the blog post (he uses a posted Link as the parent and Comments as children -- think Reddit or Hacker News):
var commentsRef =
new Firebase("https://awesome.firebaseio-demo.com/comments");
var linkRef =
new Firebase("https://awesome.firebaseio-demo.com/links");
var linkCommentsRef = linkRef.child(LINK_ID);
linkCommentsRef.on("child_added", function(snap) {
commentsRef.child(snap.name()).once("value", function() {
// Render the comment on the link page.
));
});
[sic]
I'm trying to reconcile this with my AngularJS view, since adding the result of .once into a $scope array and using ngRepeat will leave you with a static list (the children won't update in realtime if they are changed or removed by another client).
Put another way, I'd like to have something like an angularFireCollection of child objects that will add, remove and update dynamically.
I think what you're looking for is a combination of AngularFire and FirebaseIndex. I haven't checked that combining these still works, but Kato reports it has in the past.
Ignoring that for a second, though, I don't see anything wrong with your proposed plan:
push the result of .once into an array, add it to the $scope and use ngRepeat
In this case, Kato's FirebaseIndex will probably still be useful, so definitely check that out.
I have an array of items bound to <li> elements in a <ul> with AngularJS. I want to be able to click "remove item" next to each of them and have the item removed.
This answer on StackOverflow allows us to do exactly that, but because the name of the array which the elements are being deleted from is hardcoded it is not usable across lists.
You can see an example here on JSfiddle set up, if you try clicking "remove" next to a Game, then the student is removed, not the game.
Passing this back from the button gives me access to the Angular $scope at that point, but I don't know how to cleanly remove that item from the parent array.
I could have the button defined with ng-click="remove('games',this)" and have the function look like this:
$scope.remove = function (arrayName, scope) {
scope.$parent[arrayName].splice(scope.$index,1);
}
(Like this JSFiddle) but naming the parent array while I'm inside it seems like a very good way to break functionality when I edit my code in a year.
Any ideas?
I did not get why you were trying to pass this .. You almost never need to deal with this in angular. ( And I think that is one of its strengths! ).
Here is a fiddle that solves the problem in a slightly different way.
http://jsfiddle.net/WJ226/5/
The controller is now simplified to
function VariousThingsCtrl($scope) {
$scope.students = students;
$scope.games = games;
$scope.remove = function (arrayName,$index) {
$scope[arrayName].splice($index,1);
}
}
Instead of passing the whole scope, why not just pass the $index ? Since you are already in the scope where the arrays are located, it should be pretty easy from then.
I'm working on a sample ToDo list project in Backbone and I'd like to understand how the framework would prefer me to organize its views and models in the nested list scenario.
To clarify what I mean by that, my single-page Backbone app should display lists of ToDo lists. From the backend standpoint, there's a List resource and an Item (a single entry in a todo list) resource. Something along the lines of:
Monday chores
Pick up the mail
Do the laundry
Pick up drycleaning
Grocery list
Celery
Beef
You get the idea...
Since mine is a Rails 3.2 app, I'm vaguely following the Railscasts Backbone.js tutorial, so that's where I'm getting the current design from. I would love to know if I'm wildly off the Backbone-prescribed pattern, or if I'm on the right track!
I thus far have:
ListsIndex View //index of all lists
\-- ListsCollection
\-- ListView / Model //individual list
\-- ItemsIndex View //index of items in one list
\-- ItemsCollection
\-- Item View / Model //individual todo item
The flow would be:
On router initialize, fetch() collection of lists on /lists backend route. On the 'reset' event for the collection part of ListsIndex, execute render() on each of the items in the collection, appending to the list index view template.
In the initialize method of each Item View (is this where you'd wire-up the second level fetch?) fetch() the items from the /lists/:id/items backend route into an ItemsCollection specific to that view.
In the same method, instantiate an ItemsIndex object and pass the collection into it. Once again, in ItemsIndex, have a 'reset' event handler for when the collection is populated, at which point it should render each fetched model from the item collection and append them to its own view.
I'm essentially taking the design of the List and mirroring it down one level to its items. The difference is that I no longer have a router to rely on. I therefore use the initialize method of ListView to a similar effect.
Yay / nay? Super wrong? Thanks!
TL:DR; 1) I would bootstrap your initial data instead of a fetch() reset(). 2) You can do a fetch in the initialize of a View as you need it. Or you could load the data at the start. Just remember that if you fetch in the init, the async nature won't have the data ready at render. Not a problem if you have a listener waiting for that sync/add/etc. 3) I don't know what you mean by itemIndex object but you can create objects and add to them collections as you need them. Or you can just bake the in at the start if you know all your lists are going to have a collection eventually. You can reset if you want (fetch automatically does this unless you give it option {add:true}) or just add them in one by one as they come in although reset(), remove prior views, render all views seems to be the common way people do things with a complete fetch().
I think it looks pretty good. The nice thing about Backbone is that you can do it many different ways. For example, your number 2 says to wire up a second fetch() from the view. You could do that if you want to lazy load. Or you could just grab all the data at app start before anything is done. It's really up to you. This is how I might do it.
This is how I might make an app like this (just my preference, I don't know that it's any better or worse or if its the same as you described.)
First I would create a model called ListModel. It would have an id and a name attr. This way, you can create many separate lists, each with their own id that you can fetch individually.
Each ListModel has an ItemsCollection inside of it. This collection has a url based on the ListModel it is a part of. Thus, the collection url for ListModel-1 would be something like /list/1
Finally you have ItemModel which is a resource id and text.
ListCollection
ListModel // Monday Chores
ItemCollection
ItemModel // Mail
ItemModel // Laundry
ItemModel // Drycleaning
ListModel // Grocery
ItemCollection
ItemModel // Celery
ItemModel // Beef
So in this little display you'll notice I didn't put anything to do with views in yet. I don't know if it's more of a conceptual thing but this is what the data hierarchy looks like and your views can be, should be totally independent of it. I wasn't exactly sure how you were including the views up above but I thought this might make it clearer.
As for defining these structures, I think two things.
First, I'd make sure my ListModel is defined in my collection. That way I can use the collection add(hash) to instantiate new models as I produce / add them.
Second, I would define the ListModel so that when one is created, it automatically creates an ItemCollection as a property of that ListModel object (not as an attribute).
So ideally, your ListModels would be like this:
ListModel.ItemCollection
Before the app initializes, I would bootstrap the data in and not fetch(). (This kind of addresses point 1 you make) Ideally, when your Backbone application starts it should have all the necessary data it needs from the get go. I would pass in the head some data like this:
var lists = [listModel-1-hash, listModel-2-hash];
Now when the app fires up, you can instantly create these two lists.
var myLists = new ListCollection();
_.each(lists, function(hash) {
myLists.add(hash); // Assumes you have defined your model in the ListCollection
}
Now your List Collection has all the list models it needs.
Here is where views come in. You can pass in anything to any view. But I might break views down into three things.
AppView, ListModelView, ItemModelView and that's it.
Imagine a structure like this:
<body> // AppView
<ul class="List"> // ListModelView
<li class="Item"></li> // ItemModelView
</ul>
<ul class="List"> // ListModelView
</ul>
</body>
When your start your app and create an AppView, inside AppView you'd generate each ListModelView and append it to the body. Our lists are empty. Maybe when you click on the it lazy loads the items. This is how you'd hook it up.
// In ListModelView
events: {'click':'fetchItems'}
fetchItems: function() {
this.model.itemCollection.fetch(); // Assumes you passed in the ListModel into view
}
So since I bootstrapped the data to begin with, this fetch() call would be your "second" fetch. (I'm addressing point 2 you made.) You can fetch it in your initialize. Just remember that it is an asynchronous function so if you need them at render time, it won't work. But, what you can do is add event listeners to this view that are listening for add events to your itemCollections.
this.model.itemCollection.on('add', this.addItemView, this);
addItemView() will generate new instances of the itemViews and append them.
As for point 3, you can instantiate a collection at that point you need it and throw it into your ListModel. Or you can do what I did and make sure all your models always have an ItemCollection. This depends on your preferences and goals. You probably didn't need all this but I felt like illustrating it out for some reason. I dunno, maybe it helps.