Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Jeff Coughlin
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Jeff Coughlin@jeffcoughlin )

Decoupling Component Directives From Layout In AngularJS

By Ben Nadel on

As I've been learning about ReactJS and thinking more in terms of "component directives" rather than just "views," one of the biggest hurdles for me is learning to separate component directives from the layout in which they reside. When all you have is "views," it's very easy to create a hybrid situation in which the content is also responsible, to some degree, for layout. This makes switching over to components feel "bloated" and "heavy." But, the more I think about it, and the more I look toward the future of Web Components and the "shadow DOM," the more it feels right to decouple component directives from any sense of layout in AngularJS.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To see what I mean, let's look at a small example that employs an older approach to view rendering. In this demo, I have a collection of chat-messages that I want to render. Since we are not dealing with any component directives yet, the idea of the chat history and the chat message are rather commingled. In fact, if you look at the following code, you'll see that the message itself handles both the rendering of the message content as well as the line item itself, which means that it has to worry about things like inter-item spacing.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Decoupling Component Directives From Layout In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./inline.css"></link>
  • </head>
  • <body ng-controller="AppController as vm">
  •  
  • <h1>
  • Decoupling Component Directives From Layout In AngularJS
  • </h1>
  •  
  • <h2>
  • Coupled Version
  • </h2>
  •  
  • <div class="chat">
  • <div class="history">
  •  
  • <!--
  • In this version, the concept of the history line-item and the chat
  • message are one-in-the-same. This means that the chat message is
  • responsible not just for its own content but also for other things like
  • line-item spacing.
  •  
  • The upside to this is that there is minimal markup.
  • -->
  • <div
  • ng-repeat="message in vm.messages track by message.id"
  • class="message"
  • ng-class="{ other: ( message.name != 'Ben' ) }">
  •  
  • <img ng-src="./{{ message.name.toLowerCase() }}.jpg" />
  •  
  • <div class="header">
  • {{ message.name }}
  • </div>
  •  
  • <div class="content">
  • {{ message.message }}
  • </div>
  •  
  • </div>
  •  
  • </div>
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • angular.module( "Demo", [] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function( $scope ) {
  •  
  • var vm = this;
  •  
  • // I am the messages to render in the chat history.
  • vm.messages = [
  • {
  • id: 1,
  • name: "Ben",
  • message: "Hey, how's it going?"
  • },
  • {
  • id: 2,
  • name: "Kim",
  • message: "Good. What's going on?"
  • },
  • {
  • id: 3,
  • name: "Ben",
  • message: "Not too much. I was thinking about going to a movie. Any interest?"
  • },
  • {
  • id: 4,
  • name: "Kim",
  • message: "Heck's to the yeah. Which one?"
  • },
  • {
  • id: 5,
  • name: "Ben",
  • message: "How about Everest?"
  • },
  • {
  • id: 6,
  • name: "Kim",
  • message: "Let's do it!"
  • }
  • ];
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

When we run this code, you can see the markup is rather minimal; but, the Div that represents that chat history item is the same Div that represents the message itself:


 
 
 

 
 Decoupling layout responsibilities from component directive responsibilities - the old way. 
 
 
 

The double-duty of the above Div also includes hidden complexities like sub-classing. Imagine that we wanted to take this idea of a "message" and use it in a different history context. Now, whenever we override the CSS properties, we have to worry about affecting both the message content as well as the layout. This can get pretty hairy in the CSS.

When we move toward "component directives," in AngularJS, it forces us to separate the component from the layout because the component directive has its own isolated template. As such, it is necessarily decoupled, taking on more of a "shadow DOM" structure. Emotionally, this is very difficult because it requires more markup in the document, which can feel dirty and sub-optimal. It's also technically more challenging because it forces you to think about separating responsibilities. Does this CSS belong to the layout or to the component? How do I sub-class this component? What values should be passed-in? Should a particular calculation happen inside the component or, inside the calling context?

That said, let's take the above demo and move the "chat message" into its own component directive. This time, we'll still have the chat history and the history line-item. But, instead of letting that Div element work double-duty, we'll include an entirely separate element (our component directive) inside of the history line item:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Decoupling Component Directives From Layout In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./component.css"></link>
  • </head>
  • <body ng-controller="AppController as vm">
  •  
  • <h1>
  • Decoupling Component Directives From Layout In AngularJS
  • </h1>
  •  
  • <h2>
  • Decoupled Version
  • </h2>
  •  
  • <div class="chat">
  • <div class="history">
  •  
  • <!--
  • In this version, the concept of the chat history line-item is completely
  • separate from the concept of the chat message. The line-item is all about
  • layout whereas the chat-message is all about the message. Now, the chat-
  • message doesn't have to care about spacing and contextual layout.
  •  
  • The downside to this is that there is more markup. But, I'm starting to
  • embrace the idea that this is just an "emotional" downside.
  • -->
  • <div
  • ng-repeat="message in vm.messages track by message.id"
  • class="history-item">
  •  
  • <chat-message
  • id="message.id"
  • name="message.name"
  • message="message.message"
  • other="( message.name != 'Ben' )">
  • </chat-message>
  •  
  • </div>
  •  
  • </div>
  • </div>
  •  
  • <p>
  • <strong>Caution</strong>: I am using template strings (ie, the back-tick) to
  • inline the component view. If you are using an older browser, this will not work.
  • </p>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • angular.module( "Demo", [] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function( $scope ) {
  •  
  • var vm = this;
  •  
  • // I am the messages to render in the chat history.
  • vm.messages = [
  • {
  • id: 1,
  • name: "Ben",
  • message: "Hey, how's it going?"
  • },
  • {
  • id: 2,
  • name: "Kim",
  • message: "Good. What's going on?"
  • },
  • {
  • id: 3,
  • name: "Ben",
  • message: "Not too much. I was thinking about going to a movie. Any interest?"
  • },
  • {
  • id: 4,
  • name: "Kim",
  • message: "Heck's to the yeah. Which one?"
  • },
  • {
  • id: 5,
  • name: "Ben",
  • message: "How about Everest?"
  • },
  • {
  • id: 6,
  • name: "Kim",
  • message: "Let's do it!"
  • }
  • ];
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I manage that Chat Message component directive.
  • angular.module( "Demo" ).directive(
  • "chatMessage",
  • function chatMessageDirective() {
  •  
  • // Return the directive configuration object.
  • return({
  • controller: Controller,
  • controllerAs: "vm",
  • restrict: "E",
  • scope: {
  • id: "=",
  • name: "=",
  • message: "=",
  • other: "="
  • },
  • template:
  • `
  • <div class="content" ng-class="{ other: props.other }">
  •  
  • <img ng-src="./{{ props.name.toLowerCase() }}.jpg" />
  •  
  • <div class="header">
  • {{ props.name }}
  • </div>
  •  
  • <div class="content">
  • {{ props.message }}
  • </div>
  •  
  • </div>
  • `
  • });
  •  
  •  
  • // I control the chat module.
  • function Controller( $scope ) {
  •  
  • var vm = this;
  • var props = $scope.props = $scope;
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

While this is emotionally tough, the markup actually feels easier to read and to reason about. And, is makes the responsibilities a bit more clear. For example, looking at this markup, it's easy to see that the concept of "other" - which is essentially a sub-classing of the component directive - needs to be passed-in, making it the responsibility of the calling context, not the message itself.


 
 
 

 
 Decoupling layout responsibilities from component directive responsibilities - the component directive way. 
 
 
 

Seeing that extra element (or two extra elements, depending on how you think about it) definitely takes some getting used to. But, I think it's moving in the right direction. And, in fact, it makes me think of Jon Snook's book, Scalable And Modular Architecture For CSS (SMACSS). I read that book 4 years ago, long before I really got into AngularJS and years before I started thinking in terms of "components." But, in that book, Snook talks about the merits of separating the concerns of layout from the concerns of modules. Probably time for me to do a re-read of the book.




Reader Comments

@Eduardo,

I wouldn't want to change "vm" to "props" because then I lose the separation between the two storage mechanisms. Although, if you were to use "bindToController", then they all go into the same place anyway. But, that's one of the reasons I don't use the bindToController settings. This way:

props = things passed into the component.
vm = things in the component "state."

As far as the odd "$scope.props = $scope", that's just done for two reasons:

1. So that "props.xyz" can be used in the View.
2. So that "props.xyz" can be used in $scope.$watch() bindings.

Reply to this Comment

@Ben,

Another great post on AngularJS directives!

I can see the chat-message directive uses $scope.prop to reference view values in directive template. My question is:

- Why not just uses "id, name, message, other" properties in your directive template instead of setting up "$scope.prop = $scope" in directive controller since chat-message uses two-way binding to pass "id, name, message, other" into the directive.

Really enjoy reading your AngularJS related posts, they're very very helpful && insightful!

Thanks

Reply to this Comment

@Jay,

Thank you good sir. As far as the variable references, I think you're asking why I don't just use:

{{ name }}

... instead of the more verbose:

{{ props.name }}

You are correct in that the "name" (and other properties) are available directly on the $scope since they are part of the two-way data binding, and therefore can be referenced without any scoping.

The only reason that I use the "props" prefix is to create very explicit separation between the passed-in values vs. the ones that the Controller has "control" over. So, ultimately, there is no "technical" reason to do this - it's just for personal preference and (IMO) readability.

Reply to this Comment

I've only recently started some component directives myself and find it much easier to reason about code as a result.

Having said that I hate using $scope if I don't have to and would definitely recommend using the bindToController property as others have already said.

Reply to this Comment

@Richard,

To play devil's advocate for a second, though, beyond setting up the initial "props = $scope", you don't really have to reference scope at all. Except for $watch() type things; and, even then, the fact that props is part of $scope actually makes life easier. For example, watching a scope-based prop:

scope.$watch( "props.foo", handler );

... where as, if you had bindToController, you'd have to:

scope.$watch( function getValue() { return( controller.foo ); }, handler );

... which is definitely more wordy.

But, ultimately, I think it just comes down to personal preference. I, personally, like having the "props" hold the incoming data and have the controller hold the internal state.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.