Absolute Grid (ReactJS) Knock-Off In AngularJS
Right now, it's "cool" to hate on AngularJS. This is unfortunate because AngularJS is an extremely powerful framework. Sure, it has some problems in the same way that no framework is perfect. But, once you dig into AngularJS and understand how it works, there are very few problems that can't be solved. Recently, one of my co-workers, Jonathan Rowny, published a ReactJS component called "Absolute Grid." To demonstrate my love for AngularJS (and to work on my AngularJS skills), I wanted to see if I could [mostly] re-create the component in AngularJS and achieve the same kind of performance that the ReactJS community likes to talk about.
Now, to be fair, I haven't recreated all aspects of the Absolute Grid component because I didn't want to put more than a weekend into it - eventually, there's a point of diminishing returns for pleasure in an exploration like this. But, there was definitely a sufficient amount of complexity here - I really had to stretch the way I think about AngularJS and about how I organize components.
The absolute grid creates a layout that is automatically sized to fit into its container. And, it updates that layout as the items in the collection are changed or zoomed. However, the grid doesn't "own" that functionality, it simply consumes it. The root controller, that owns the list, also owns the filtering and the zooming. This is an important separation of concerns because the grid can't know how those features (filtering and zooming) fit into the greater application. It only knows that it has to consume and respond to those changes.
Ultimately, the grid is just a repeater that stamps-out clones of the grid item content. Since the repeater needs to create data that is available to the content (such as the item reference), we can't use an isolate scope. Which also means that we can't use the fancy (and expensive) two-way data binding. This turns out to be a good constraint because isolate scope adds additional watchers (which we don't need) and forces us to think about treating the grid as a pure consumer.
The grid is too complex to really describe in detail, so I'll leave it up to you to explore the code below. Something like half of the code is dedicated to just the sortable nature of the grid.
Google Chrome definitely handles the performance better than Firefox. But, the bottleneck seems to be the animations (I'm using CSS3 transitions), not the AngularJS parts. I truly believe in the AngularJS framework for both its power and its relative simplicity (in terms of reasoning). I hope that people give it a fair chance and don't haphazardly jump from one framework to another.
Want to use code from this post? Check out the license.
As one my favorite developers often says... I can dig it! Nice work.
It was a pretty cool journey. Thanks for the inspiration! Definitely one of - if not the most - complex things that I have build in AngularJS. Thinking about how to check for dirty values on the filtering was tough since the grid is not the owner of filtering, but rather a consumer. So, I had to figure out how to efficiently monitor the collection of items to see when / if their visibility changed. For me, the most interesting part of this whole demo is the watchDynamicValues() function which is what powers the main $watch() method. Half of me is always worried that that's it represents so much work; but, then I remember that this is basically what AngularJS and ReactJS are doing - diffing collections of data.
It's interesting that you could get Angular to perform almost as well as React in some browsers. Did the size of the lists make a difference? What happens when the list is really big?
Also, it'd be interesting if you or another Angular fan could split your demo out into a reusable module. I know it's not really what you were after in your experiment, but it's kind of hard to compare the code between a one-time demo and a modular, reusable component. I can't imagine people with a lot of time invested in Angular would want to switch to React (yet), but a comparison of the code for things like this would probably be worth looking at for people who are researching Angular vs. React for a new project.
There's no doubt that as the size of the list grows and / or the context in which it is used increases in complexity, performance is going to degrade. How much will depend very heavily on context and browser. For example, I've seen React code that doesn't perform that well in Firefox ... oh Firefox, I wish I could quit you.
As the list / context gets bigger, the limitation is always the amount of data that needs to be checked. The more data-bindings, the more data may have changed based on each user interaction. You can combat this by using things like the one-time bindings ( :: before the expression):
Which will go a long way. But, if you are dealing with cached data, or event-driven data, that might not be an option. If not, there's all kind of other stuff you can do to help reduce the number of bindings.
But, each context is different; so, you probably only need to worry about performance when it becomes a manifested problem.
As far as reusability, I tried to make it module. The directive is basically:
grid-zoom=" zoom expression "
grid-items=" items expression "
item-index=" ng-repeat item "
item-identifier=" track-by id "
item-width=" base width "
item-height=" base height "
item-visibility=" visibility expression "
item-move=" on-move expression ">
<!-- YOU PUT ANYTHING YOU WANT IN HERE. -->
That said, I've never published an actual module, so I am not what the best-practices are for that. Like, how to incorporate the CSS and the jQuery requirement, etc..
Interesting article. I, myself, have recently delved into the react.js world as a part of an experiment I became involved with at work. I really like what I have seen of it so far. I've done some exploring Angular.js through your blog and looking into some of the things you write about on your blog, but my experience with it is limited. Have you ever worked with/used Plunker? (plnkr.co)? I used it with my project, and I felt it was a pretty cool tool for developers. The project/experiment I was working with tied in to gitHub through Plunker. It was a pretty cool experience, because in the past, some of my lack of experimentation in the development world was simply wanting to keep my "work code" and my "play code/personal code" separate, and having a playground/place to do it. Using Plunker was like having a playground for me coding. GitHub may have something similar, and I've used GitHub some before, but when I followed this tutorial that Pluralsight sent me at work, they were using Plunker, and you didn't have to sign up or anything, and I found it to be quite a nice tool for developers. Thought I'd share. :-) Anyway, nice Angular work.
Great work! I wish I was skilled enough to code this up over a weekend. angular-isotope and angular-gridster do similar things too btw, although not exactly the same.
Very cool, have you considered adding support for pagination using this https://github.com/michaelbromley/angularUtils/tree/master/src/directives/pagination , which would help load times plus still able to filter results , it replaces ng-repeat with dir-paginate .
Ben, I took this grid and turned it into a nice little reusable directive. Really amazing work. Using it in a couple of projects.
My question is about the "compile" function. I don't love it. I tend to prefer using the "template" attribute to pass a function that uses that either grabs the innerHtml of the element or uses a pre-cached template from Angulars template cache and turn them both into a string. Then I use underscores template replacement functionality to pass attributes into these templates to set it up exactly as I like prior to returning it to the directive.
I know this is not the "angular way", but it sure seems easy and very flexible. Any ideas why I should not do it this way?
I'm definitely a noob at using Angular and this is demo program is way more complicated than any other demos I've ever tried to analyze. I don't see how the sample data: (sampleItems) gets populated before it gets passed into the controller. Any chance Ben or someone else could explain how this is happening to me?