Sometimes, There Is Unavoidable Coupling To The DOM In AngularJS
In the last few posts, I've touched upon the "Angular Way" which includes a strong separation of concerns between the Controllers, which know about the view-model, and the Directives, which know how to "glue" the DOM (Document Object Model) to the Controllers. But sometimes, I think there is coupling that is simply unavoidable. This coupling may not be direct, in so much as that there is no direct reference to the DOM from within the Controller; but, rather, an indirect coupling by way of a workflow that is there for no other reason than to accomodate browser behaviors and / or animations.
To explore this edge-case of coupling, I put together a little demo in which there is a grid of dots that is evenly distributed over a contained area. As new dots are added to the grid, the Controller has to update the location of the dots to keep them evenly distributed. Now, when a user clicks on the grid, I want a new dot to be created at the click location and then animated into place (by way of CSS transition properties).
At first, the separation of concerns seems easy - the link() function of the directive is the only thing that can know about the X/Y coordinate-local offsets; as such, it has to be the one that tells the controller where to add the dot. The controller, on the other hand, manages the collection of dots; so, it has to be the aspect of the code that knows how to calculate the location of each evenly-distributed dot. No problem.
But, things get tricky when the dot is initially added to the grid. While the link() function tells the Controller where to add the dot (at the initial click-location), the Controller immediately overrides the given X/Y values with evenly-distributed coordinates. As such, we need something to put a repaint between the initial rendering of the dot and the recalculation of the layout:
- Add dot to grid at click-coordinate.
- Repaint browser (forces new dot to render).
- Recalculate grid layout (transitions click-location to grid-location).
That said, the very fact that a repaint is required is a condition of the browser, not necessarily of the view-model. As such, it would make sense to have the link() function implement the repaint. But, if the link() function needs to insert a forced-repaint, it would then also have to coordinate the recalculation of the grid layout, which feels very much like an overstepping of bounds; if the Controller manages the grid, it should be the one that knows when to recalculate the layout based on changes in the view-model.
On the flip-side, however, if the Controller coordinates both the adding of the dot and the recalculation of the layout, it would then need to implement the forced-repaint, which again, is a concern of the DOM. So it seems, no matter who does what, by fact of coordination, there has to be some degree of coupling in both directions.
In the following code, I chose to move the coupling into the Controller by way of an internally-consumed $timeout(). This way, the Controller is coupled to the DOM through a "leaky workflow." But, I felt that it was more important to keep the internal workflow - adding dots and recalculating distribution - together within the Controller.
And, when we run this code, we are able to get new dots zoom in from the click location thanks to the embedded $timeout() call:
Now, you could argue that this coupling is OK since this is a single component designed to create a single, cohesive user experience (UX). Which, in this case, I think is probably true. But, being too comfortable with that mentality can quickly lead to a slippery slope of overly-tight coupling. So, while I do think that sometimes there is just unavoidable coupling of the Controller to the DOM, it should still be thought of as an edge-case.
Want to use code from this post? Check out the license.