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.
This is the kind of thing I am talking about:
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:
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
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:
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?
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:
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:
And find elements like this:
Here is one way to do it:
Then we can use the new ElementRegistry service like so:
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:
Lets alter our code to reflect the asynchronous callback method of interaction:
This allows us to write a directive that might look a little like this:
Checkout how our views look now:
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! :)