For most of the last year I have been working with a large legacy Rails / AngularJS software application where we have HAML views that are enhanced with AngularJS directives.
Now Angular can certainly be effectively used that way but occasionally, it becomes clear, Angular is ultimately designed to work more as a monolithic application framework, as opposed to, a 'page enhancer' and there are certain problems that do crop up from time to time to remind you of this, as you bend it's usecase. One such issue prominent specifically with the Angular+Rails system is what I like to call 'php-fication' or the tendency to want to program your entire application in it's templating language.
How much logic can we stuff into HAML?
One frustrating problem is the tendency for both Angular and Rails to use views as dumping grounds for huge amounts of view logic that doesn't have a home anywhere else. In Rails it is as a result of a lack of decent view first design patterns within it's architecture. On the client-side, in our case, it is as a result of not handling templating within Angular so the declarative logic stays stuck on a single HAML view.
The end result are views with a fair amount of Ruby, Angular and DOM Javascript spaghetti all together, in the case of the app I have been working with, alongside a healthy dollop of SMACSS driven CSS adding it's own layer of confusion.
This is the kind of thing I am talking about:
https://gist.github.com/ryardley/3c547990887690276b20
Decoupling Rails from Angular from CSS
How did we get to this point? Well it all probably started with code that looks a little like this:
https://gist.github.com/ryardley/c54e78b078cb14f4cc8b
This doesn't look too evil and wouldn't be if it was an excerpt from a template within an Angular component, but if you examine it from the perspective of Angular served through a Rails HAML view there are several issues with this piece of code:
- It inadvertently piggybacks on it's parent's scope, blurring the lines between where it's function start and ends with that of it's parent.
- It assumes there are magic handlers defined (presumably by OverlayController) that will be there for it's button components to call that will do something.
- It exposes it's internal state externally by declaring behaviour bound to the external isOverlayShowing variable.
Better: Encapsulate in a Directive
https://gist.github.com/ryardley/31a05e5919df460a72ca
This is better but how do we apply our behaviour to our controls that launch the overlay and display based on the overlay's state?
It would probably be done with code in the directive's link function that looks a little like this:
https://gist.github.com/ryardley/c0d64262eaf04ae1a4ab
The immediate problem you can see here is that the class names that are designed to style the specific instance of the CSS component are being used to define behaviour which removes flexibility from our system. Most CSS folks would say there you should use IDs for attaching behaviour and classes for styling but in our case as we are trying to keep components reusable this is not really applicable as we may end up using ID's in multiple places on the page.
More Directives perhaps?
So classes couple style to functionality and ID's wont work when we have multiple components on a page, that leaves us with perhaps using more directives to handle functionality?
https://gist.github.com/ryardley/92414b6841dc0a53ce33
This seems acceptable until you actually start creating all these tiny directives just to handle your behaviour of what should really be the functionality of a single directive:
https://gist.github.com/ryardley/80212f6b87a67e679657
The Registry Pattern
A real solution to this problem would be to create a registry that registers elements with the given parent scope and finds those elements from that scope. This should work so we can define elements like this:
https://gist.github.com/ryardley/dc2a4c3f94764526210f
And find elements like this:
https://gist.github.com/ryardley/2e8eecf5ffe31027b29d
Here is one way to do it:
https://gist.github.com/ryardley/176fa71b07ea18110b73
Then we can use the new ElementRegistry service like so:
https://gist.github.com/ryardley/f88980b03e5496bd84fe
This works in some cases but reacts poorly to race conditions where we need the elements before the elements have been rendered and compiled. What about if we add some asynchronous callback magic so we can use the finder like this:
https://gist.github.com/ryardley/ecddc0152a5c8187eb9b
Lets alter our code to reflect the asynchronous callback method of interaction:
https://gist.github.com/ryardley/ed0245ca1f89bd20e49d
This allows us to write a directive that might look a little like this:
https://gist.github.com/ryardley/479cc146e980b662c6b9
Checkout how our views look now:
https://gist.github.com/ryardley/d80addcfef045199e58d
Looking at this view, we now don't need to care about the functionality that Angular is adding, although it is clear to see where those behavioural additions are. All we see from the perspective of the view are exactly all we should see: our styles, which are important in a view and the general structure of the HTML DOM which we can now manipulate safely without worrying about how that affects our behaviour we have received from Angular! :)