I have a bunch of divs and spans that I'm using as buttons and need to toggle between the aria-pressed state of "true" and "false". Of course, it's possible to do this using this aria-pressed={this.state.pressed}, but this would require me to have aria state for each button (there are many).
My alternative solution was to use a ternary and conditionally render different divs. For example,
{ this.state.buttonChange? <div onClick=(this.buttonChange} aria-pressed="true></div> : <div onClick=(this.buttonChange} aria-pressed="false"></div>}
But this would create a lot of duplicated code and maybe be less efficient than just having a bunch of different aria states.
What is the most efficient way of handling this problem in React without using a native html button element?
One way which I think is using ternary operator with assigning div elements to a variable for reusability
let button = <div onClick=(this.buttonChange} aria-pressed={this.state.pressed}></div>;
{ this.state.buttonChange? {button} : {button}}
Or if you are rendering buttons dynamically based on dynamic data then you can push all div elements to an array and render it.
<div onClick=(this.buttonChange} aria-pressed={this.state.buttonChange}></div>
so you only need to set the aria-pressed value based on the state.
Quick question regarding performance in Angular JS.
I have an ng-repeat listing out a load of search results. I use a directive/template to pull out each search item.
Is it better to use an ng-if statement wrapping two directives or to use one directive and have multiple ng-if statements within it, and use expression or statements.
E.G. Ng-if statements wrapping two directives:
<div ng-repeat>
<directive-1 ng-if="this"></directive-1>
<directive-2 ng-if="that"></directive-2>
</div>
OR
<div ng-repeat>
<directive-1></directive-1>
</div>
and within directive-1 you have...
<img src="1" ng-if="this">
<img src="2" ng-if="that">
<span>{{title || title2}}</span>
etc...
What ng-if is? It is directive (1), with isolated scope (2), that removes or append DOM(3).
What does this mean?
Ng-if takes time to initialize. (not much)
Ng-if create new isolated scope. (not much)
Ng-if operates with DOM. (possibly very expensive operation)
So, the main problem with preformance connected with DOM. Best way to speed up ng-if - is to reduce dom manipulation.
Also, try to avoid using ng-if with ng-repeat. Of course you can do this, but do this carefully.
Here are almost 2000 rows in the page. I use AngularJS filter to search some items which contains the typed strings. The problem is that efficiency is bad when typing one character into the input control.
Do you guys have good ideas how to improve this filter?
Here is the search code:
input box:
<input class="searchText ng-cloak" ng-model="searchText.ValueName" placeholder="Search Value" />
in the table's ng-repeat:
<tr ng-repeat="registry in currentSettings | filter:searchText" ....
string filter:searchText is used to filter.
The bottleneck is likely adding and removing elements from the DOM. Avoid this by using css to hide elements. In stead of filtering the ng-repeat, use ng-show:
<li ng-repeat="registry in currentSettings" ng-show="([registry] | filter:searchText).length > 0">
http://php.quicoto.com/use-ng-show-filtering-data-angularjs/
There is also the virtual ng-repeat plugin, it only adds the dom nodes which are going to be rendered for better performance
I would also suggest to use the track by in the repeater. It will prevent unnecessary removal and reinsertion of elements and result in dramatic improvement in speed in some cases. Just make sure you are tracking by some unique property.
I am building an app which features a kind of "playlist". This is represented an ng-repeated custom directive with ng-repeat = "element in playlist"
Because I want to allow a user to re-use the same element twice in the playlist, I tried using the track by $index addition.
Now, what's confusing is: when I came to remove an element from the playlist (I have a function removeElement(index) which essentially contains something like this:
$scope.removeElement = function(index){
$scope.playlist.splice(index, 1);
}
Something weird happened: the element was removed correctly from $scope.playlist, but for some reason the view didn't update properly. The last element appeared to be removed.
Furthermore, I couldn't properly re-order the elements in the array either.
When I removed track by $index this problem disappears, so I assume this is because when you remove an item from the array, if you're only looking at the indices, then it appears you've just deleted the last one.
The behaviour is odd though, because transcluded content is correctly removed -- see this plunker
EDIT: The above link has been modified to show the problem better and also show the answer I settled on.
The question has also been slightly edited, to make it clearer what I was getting at. KayakDave's answer below is still correct, but is more suited to an array of primitives (which my original plunker featured).
TL;DR: How do you put duplicate elements in an ng-repeat without sacrificing the ability to control their position, or remove elements correctly?
One of the big performance advantages of using track by is Angular doesn't touch any DOM element whose tracking expression hasn't changed. This is a huge performance improvement for long ng-repeat lists and one of the reasons for the addition of track by.
That performance optimization is at the root of what you're seeing.
When you use $index in track by you're telling ng-repeat to tie each DOM element it creates to it's position ($index) on the first run of ng-repeat.
So the element with color style red is tagged 0, orange 1, yellow 2 ... and finally indigo 5.
When you delete a color Angular looks at the indexes you told it to track and sees that you longer have an index #5 (since your list is one shorter than before). Therefore it removes the DOM element tagged 5- which has a color style of "indigo". You still have an index #2 so the element with the color yellow stays.
What makes it confusing is that, due to data binding, the text inside the DOM element does get updated. Thus when you delete "yellow" the DOM element with the color yellow gets the text "green".
In short what you're seeing is ng-repeat leaving the DOM element styled with yellow untouched because it's tracking value (2) still exists but data binding has updated the text inside that element.
In order to add multiple entries with the same color you need to add your a unique identifier to each entry that you use for the track by. One approach is to use key-value pairs for each entry where the key is your unique identifier. Like so:
$scope.colorlist = {1:'red', 2:'orange',3:'yellow',4:'green',5:'blue',6:'indigo',7:'yellow'};
Then track by the key as follows:
<color-block ng-repeat="(key, color) in colorlist track by key" color="{{color}}" ng-transclude>
{{color}}
</color-block>
And make sure to use that key for your delete select:
<option value="{{key}}" ng-repeat="(key,color) in colorlist">{{color}}</option>
Now the DOM element with the color styling yellow is tied to the key you specified for the "yellow" array element. So when you delete "yellow" the ng-repeat will remove the correct DOM element and everything works.
You can see it work here: http://plnkr.co/edit/cFaU8WIjliRjPI6LInZ0?p=preview
I'd like to add another answer to this question, because I discovered a simpler solution.
There's an important section of the documentation for ng-repeat which is easy to miss, specifically on the dupes error.
It states:
By default, collections are keyed by reference
After reading this, the solution was obvious - as I wasn't dealing with primitives (yes, the plunker is, but that was an over-simplification) I needed to copy the duplicate object and add its copy to the array. This means everything works as expected when you remove track by $index and just let the default behaviour take over.
Angular makes this especially easy because jqlite has a .copy. method.
Here's what I'm saying demonstrated in a plunker.
I have the following page structure:
<ul class="listOfPosts">
<li class="post WCPost" data-postId="1">
<div class="checkbox"><input type="checkbox" class="wcCheckbox"/></div>
<div class="PostContainer>
<!-- some other layout/content here -->
<ul class="listOfLabels">
<li class="label"> Label 1</li>
<li class="label"> Label 2</li>
</ul>
</div>
</li>
<li class="post WCPost" data-postId="2">...</li>
<li class="post WCPost" data-postId="3">...</li>
<li class="post WCPost" data-postId="4">...</li>
...
</ul>
Here is the overly simplistic View:
var MyView = Backbone.View.extend({
el:$('.listOfPosts'),
initialize: function() {
_.bindAll(this,"postClicked");
},
events: {
"click .wcCheckbox":"postClicked"
},
postClicked: function() {
alert("Got a a click in the backbone!");
}
});
Question: I want to know the data Id of post that was clicked. With simple JQuery I can just do the following:
$('.wcCheckbox').live('click' function() {
alert("Data id of post = "+$(this).parents('.WCPost').data('data-postId'));
}
Now I know that Backbone does event delegation so it needs a DOM reference in the el property. If I give it the .listOfPosts then the event seems to fire perfectly but to get "which" posts were checked I'll have to keep traversing the DOM and pick out the elements that were selected!!! It would be quite expensive in my opinion! How do I do this in Backbone for each individual post?? i.e., How do I bind the view to each individual li.WCPost??
NOTE: I'm not using plain vanilla jQuery since the code that I need to write is best done with Backbone's modular design, but since I'm not a pro with Backbone (yet ;) just a bit confused...
Using something like $(event.target).data('postId') in your postClicked() function is a normal way to do this kind of stuff, as far as I can tell.
Extensive usage of events might seem unusual at first, but it's a good way to improve code organization, if used properly. You really can get all the data you want from the event in most cases, especially if you have jQuery. (Note that the event passed to your postClicked function is a regular jQuery event object, and everything you can find about it could be applied. Backbone.js uses jQuery's delegate() function to bind events.)
* * *
However, you still can bind events by yourself in the initialize() method of your view.
initialize: function() {
// Custom binding code:
this.$('.wcCheckbox').live('click' function() {
alert("Data id of post = "+$(this).parents('.WCPost').data('data-postId'));
}
// No need for this anymore:
// _.bindAll(this,"postClicked");
},
(this.$(<selector>) is a function that automatically scopes jQuery selectors to your view, equivalent to $(<selector>, this.el).)
* * *
Another solution (perhaps the most natural in the end, however requiring a bit of work at first) is to create a PostView class, and use it for individual posts and for binding checkbox click event.
To display the posts, in your post list view you go through Posts collection, create a view for each Post instance, and append it to the .listOfPosts element.
You won't need to solve a problem of accessing post's ID anymore, since you would bind the checkbox click event on a PostView. So, in the handler function would be able to find post's ID easily—either through related model (this.model.get('id')) or through view's element ($(this.el).data('postId')).
* * * Update
Now that I generated my posts' DOM independently of Backbone how do I 'retrofit' a view to each post like I mentioned in the question?
I don't want to refactor a ton of client code just to accommodate Backbone. But how do I bind a view to each li??
If you decided to go with MVC and object-oriented JavaScript, you shouldn't manage DOM elements for your posts directly: you create PostView instances, and they, in turn, create corresponding elements, like in todos.js, and fill them with rendered template. (And if you don't want to create elements automatically, Backbone allows you to bind newly created view to the element manually, by specifying el attribute when subclassing. This way, you can bind views to existing elements, for example, on the initial page load.) Any DOM modification related to particular posts should take place inside the PostView class.
Some advantages of OOP approach over DOM-based:
Views are more suitable for data storage than DOM elements.
Note that with your current approach you're using data attributes extensively—for example, to store post's ID. But you don't need to do that if you're operating views: each PostView already knows about its related model, and so you can easily get the post id via, e.g., view.model.get('postId'), as well as any other post data that you want.
You can also store view-specific data that doesn't belong to Post model: for example, animation and / or display state (expanded, selected, …).
Views are more suitable for defining the behaviour of elements.
If you want to, for example, specify a checkbox click event handler for each post, you place this code into your PostView class. This way, event handler knows about all post data, because the view has access to it. Also, the code is better structured—what deals with particular posts, belongs to PostView, and doesn't get in your way.
Another example is a convention to define a render() function on the view. The template function or template string is stored in a view attribute template, and render() renders that template and fills the view's element with resulting HTML:
render: function() {
$(this.el).html(this.template({
modelData: this.model.toJSON()
}));
},
Manipulating DOM may be slower than manipulating objects.
However, DOM-based approach (which seems like you were using until now) has its own pros, especially for less complicated applications. See also: Object Oriented Javascript vs. Pure jQuery and .data storage
Is there a way to bind views to current and future DOM elements that are NOT generated by backbone?
Does that imply that backbone needs to be use from the very start and will be difficult to 'retrofit' so to speak?
As follows from above, you don't need to bind views to future DOM elements (use views to manipulate them).
Regarding binding to the DOM elements that are already on the page when it's initialized, I don't know the ‘right’ way to do that. My current approach is to clear existing elements and create views from scratch, which would automatically create corresponding DOM elements.
This means browser will have some extra work to do at page initialization. For me, advantages of OOP justify it. From the user's point of view, it's also fine, I guess: after that initial initialization, everything will work faster, since page won't need to be reloaded until user does something like logout.
(BTW, I think I should clarify the point of using MVC approach, at least, for me: creating a client-side application that, essentially, does the most work by itself, using the API provided by the back-end server. Server provides data in JSON for models, and your client-side app does the rest, extensively using AJAX. If you do normal page loads when user interacts with your site, then such MVC and heavy client-side might be overhead for you. Another thing is that you will have to refactor some code, which may not be an option depending on your requirements and resources.)
Note that events pass reference to themselves and their point of origin, it's the easiest way to access the origination of the event, in my opinion.
Try it like this, and see if this is what you need (and less convoluted):
var MyView = Backbone.View.extend({
el:$('.listOfPosts'),
initialize: function() {
_.bindAll(this,"postClicked");
},
events: {
"click .wcCheckbox":"postClicked"
},
postClicked: function(e) {
alert("Here is my event origin: "+e.target);
}
});
There is a rich amount of data that can be had from the event, as can be seen here: http://www.quirksmode.org/js/events_properties.html
once you get your head wrapped around javascript eventing, you might look into PubSub/Observer pattern for more-decoupled UI components, here is a good introduction:
http://blog.bobcravens.com/2011/01/loosely-coupled-javascript-using-pubsub/
Nothing prevents you from nesting views. That is, the outer view represents the UL. The UL view would render a bunch of inner LI views. You can nest views as deeply as makes sense.
I've been doing things this way very successfully.