Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at RIA Unleashed (Nov. 2009) with: Tom Jordahl
Ben Nadel at RIA Unleashed (Nov. 2009) with: Tom Jordahl

Creating A ReactJS-Inspired "Props" Object In AngularJS

By Ben Nadel on

As I've been digging into ReactJS, one of the things that I have found attractive is the differentiation between the state of a component and the "props" that are passed-into the component from the calling context. In AngularJS, everything lives on the "scope". But, with the emergent pattern of storing the view-model directly on the Controller (as popularized by John Papa's AngularJS style guide), this actually makes it easy to create a ReactJS-inspired "props" object in our AngularJS component directives.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

NOTE: In this post, I'm specifically talking about "component directives" that use the Isolate Scope to define incoming properties. There are fewer mechanics available to "behavioral directives." And, behavioral directives don't typically have their own scope, which makes differentiation less meaningful.

With component directives that use the isolate scope, the incoming properties are mapped onto the isolate scope based on the directive configuration object. And since we, as a community, have started using the Controller instance as the view-model, instead of the scope, it means that we have a natural separation here between the component's view-model and the incoming properties:

  • View-model properties: Stored directly on the Controller.
  • Incoming properties: Stored directly on the $scope.

NOTE: Technically, the view-model is also stored on the scope using the "controller as" syntax, but that detail fades into the background.

With this natural separation, we can create a "props" object as little more than an alias to the component directive's isolate scope. And, we can also inject the props into the scope itself (recursive reference) so that it can be referenced clearly within the view-template.

var props = $scope.props = $scope;

This is just some syntactic sugar; but, it creates a clear separation in the Controller and in the View. To see this in action, I've created a demo that uses a component directive and passes-in one property and two methods:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Creating A ReactJS-Inspired "Props" Object In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController as vm">
  •  
  • <h1>
  • Creating A ReactJS-Inspired "Props" Object In AngularJS
  • </h1>
  •  
  • <!--
  • From our calling context, we will be passing in a number of values to the
  • component directive. This includes:
  •  
  • * someId (property)
  • * doThis (method)
  • * doThat (method)
  •  
  • Within the component directive, we will denote these on a incoming "props"
  • hash (ala ReactJS) to help differentiate it from the "vm" (view-model) hash
  • that is used by the component internally and by its template.
  • -->
  • <div
  • bn-widget
  • some-id="vm.someID"
  • do-this="vm.doThis()"
  • do-that="vm.doThat()">
  • <!-- Content supplied by component. -->
  • </div>
  •  
  •  
  • <!--
  • This is the component directive template. Note that it makes reference to
  • both a "vm" object (its own view-model) and to a "props" object, which holds
  • the values passed-in by the calling context.
  • -->
  • <script type="text/ng-template" id="widget.htm">
  •  
  • <div class="m-widget">
  •  
  • <p>
  • Passed-in ID: {{ props.someID }}
  • </p>
  •  
  • <p>
  • <a ng-click="vm.handleThis()">Do this!</a>
  • or
  • <a ng-click="vm.handleThat()">Do that!</a>
  • </p>
  •  
  • </div>
  •  
  • </script>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.3.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function AppController( $scope ) {
  •  
  • // Here, we are using the emergent pattern of using the "vm" (ie, the
  • // Controller itself) as the view-model instead of the $scope object.
  • // In this way, we force ourselves to get away from scope-inheritance
  • // and must explicitly pass values around. This also has the added
  • // benefit of making the HTML template more explicit in so much as it
  • // requires us to explicitly scope the reference to the controller
  • // (using the "controller as" syntax).
  • var vm = this;
  •  
  • vm.someID = 4;
  •  
  • // Expose the public methods.
  • vm.doThat = doThat;
  • vm.doThis = doThis;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • function doThat() {
  •  
  • console.log( "Do that (passed-in)!" );
  •  
  • }
  •  
  •  
  • function doThis() {
  •  
  • console.log( "Do this (passed-in)!" );
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I define the demo widget "component directive".
  • angular.module( "Demo" ).directive(
  • "bnWidget",
  • function bnWidgetDirective() {
  •  
  • // Return the directive configuration object. We are expecting three
  • // attributes to be passed-in, which we are mapping to isolate-scope
  • // bindings.
  • // --
  • // NOTE: I am purposefully NOT USING the "bindToController" configuration
  • // option as these are going to be called-out using our "props" hash
  • // reference (to differentiate it from the rest of our view-model).
  • return({
  • controller: "WidgetController",
  • controllerAs: "vm",
  • restrict: "A",
  • scope: {
  • someID: "=someId",
  • doThis: "&doThis",
  • doThat: "&doThat",
  • },
  • templateUrl: "widget.htm"
  • });
  •  
  • }
  • );
  •  
  •  
  • // I am the demo widget controller.
  • angular.module( "Demo" ).controller(
  • "WidgetController",
  • function WidgetController( $scope ) {
  •  
  • // Here, again, we are using the emergent pattern of the controller as
  • // the "vm" or "view-model". When combined with the "controllerAs"
  • // directive configuration, this makes our scoping within the controller
  • // easier (thanks to lexical binding) and makes our template references
  • // more explicit.
  • var vm = this;
  •  
  • // Our component directive is using an isolate-scope. Which means that
  • // the passed-in properties are automatically bound to the scope (note
  • // that I purposefully avoided the bindToController option). And, since
  • // we are no longer depending on $scope for our view-model (see note
  • // above), it means that all of our incoming properties are isolated
  • // within the $scope. As such, we can simply alias the scope as our
  • // ReactJS-inspired "props" object. Also, we can store a reference to
  • // the props back on itself so that it can be explicitly referenced in
  • // the template.
  • // --
  • // NOTE: Because this is still technically on the $scope object, you can
  • // easily watch for changes using $scope.$watch( "props.someValue" ).
  • var props = $scope.props = $scope;
  •  
  • // Expose the public methods.
  • vm.handleThat = handleThat;
  • vm.handleThis = handleThis;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I manage the that-click, passing it off to the passed-in method.
  • function handleThat() {
  •  
  • // Notice that I am using the "props" reference to differentiate
  • // between the local view-model and the values passed-in from the
  • // calling context.
  • if ( confirm( "Are you sure for [" + props.someID + "]?" ) ) {
  •  
  • props.doThat();
  •  
  • }
  •  
  • }
  •  
  •  
  • // I manage the this-click, passing it off to the passed-in method.
  • function handleThis() {
  •  
  • // Notice that I am using the "props" reference to differentiate
  • // between the local view-model and the values passed-in from the
  • // calling context.
  • if ( confirm( "Are you sure for [" + props.someID + "]?" ) ) {
  •  
  • props.doThis();
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, I am referencing the "props" object in both the Controller and in the View. Functionally-speaking, absolutely nothing new is happening here. But, from a readability standpoint, I think the strong separation of origins makes things easier to understand. It also makes things easier to use because isolate-scope methods are invoked using "locals" where as normal view-model methods are not.

AngularJS is still my main love. But, ReactJS definitely has something features and concepts that are worth learning and, I think, worth borrowing. For me, the delineation of "props" is one of those concepts that we can easily take advantage of in AngularJS.




Reader Comments

Again very interesting idea Ben.
ReactJs is super cool(expect the jsx abomination imho) and we can definitely learn a lot from it.

I'm doing something similar, angular via "reactJS" api style.

So state ( vm ) props are strictly on this.state and you are using bindToController to get properties sync for free from angular.

check it out, it's a WIP
https://gist.github.com/Hotell/986d17060ea23df91eb2

Reply to this Comment

@Martin,

Very interesting. I'm very new to the ES6 syntax, so stumbling a bit. But, I don't understand how the template will be able to access the state. If "state" is a private variable, will the view be able to reference "ctrl.state.foo"? Or, am I missing the point of the organization?

Reply to this Comment

@Ben,

yes the view can access the state.
It's defined as private just explicitly disable to set state directly from link function or from some other directive, which requires that component/directive.

All in all that's just typescript so the state is publicly available on the scope.ctrl namespace. Anyway that's the "kind of" downside of using Classical pattern.

I will document the gist further, so it's easy to grok for everybody, even the newcomers to typescript/es6

Reply to this Comment

@Martin,

Sounds good. And, I definitely need to bone-up on the ES6 stuff. Some people are starting to use the syntax at work and I need to be able to help debug their code. So, I gotta get on my game!

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.