Angular-styleguide: Missing styleguide for component

Created on 1 Sep 2016  Â·  25Comments  Â·  Source: johnpapa/angular-styleguide

Instead of using directives we can use component on those places where we dont need to DOM manipulate. But I cant find any styleguide for this in angularjs 1.5.

Angular 1 enhancement help wanted

Most helpful comment

Personally I've been using the following style:

(function(){
  'use strict';

  angular
    .module('app')
    .component('exampleComponent', exampleComponent());

  function exampleComponent() {
    var component = {
      bindings: {
        /* bindings - avoiding "=" */
      },
      controller: ExampleComponentController,
      controllerAs: 'vm',
      templateUrl: 'path/to/example-component.html'
    };

    return component;
  }

  ExampleComponentController.$inject = ['exampleService'];

  /* @ngInject */
  function ExampleComponentController(exampleService) {
    var vm = this;

    /* generic controller code */
  }
})();

All 25 comments

What suggestions does the community have for this?

I absolutely want a component section

I like it this way:

(function() {
    'use strict';

    angular
        .module('app.widget.activeplaces')
        .component('activePlaces', {
            bindings: {
                data: '<'
            },
            templateUrl: 'components/active-places/active-places.widget.html',
            controller: ActivePlacesController,
            controllerAs: 'vm'
        });

    function ActivePlacesController(activityService) {
        var vm = this;

        vm.activities = [];
        vm.doFunnyStuff = doFunnyStuff;

        function doFunnyStuff() {
            vm.activities.push('yolo');
        }
    }

}());

Alternatively move component registration to the bottom:

(function() {
    'use strict';

    var activePlaces = {
        bindings: {
            data: '<'
        },
        templateUrl: 'components/active-places/active-places.widget.html',
        controller: ActivePlacesController,
        controllerAs: 'vm'
    };

    function ActivePlacesController(activityService) {
        var vm = this;

        vm.activities = [];
        vm.doFunnyStuff = doFunnyStuff;

        function doFunnyStuff() {
            vm.activities.push('yolo');
        }
    }

    angular
        .module('app.widget.activeplaces')
        .component('activePlaces', activePlaces);

}());

The problem is that I would prefer using the new $ctrl reference instead of vm which makes the vm-pattern inconsistent. So either switch completely to $ctrl or simply add controllerAs: 'vm' to the component settings object to be consistent with the rest of the style guide :)

I like the second way better. I'd also consider putting methods on a prototype to make it easier to migrate to ES6 classes later. I also agree it's time to switch to $ctrl as it's the default and not having to declare controllerAs reduces the boilerplate needed.

I prefer the first way fwiw :grin:
(without vm of cource :stuck_out_tongue:)

First method is my presonal fav. I'd add to both controllers:

// other vm vars/assigns here
vm.$onInit = activate;

// functions
function activate() {
  // initialization magic here
}

of course we can change the name of activate function to init or onInit etc.

This is a bit more opinionated and ES2016 but for my components I do it like that :

(function() {
  'use strict';
  /*
<active-places data="">
</active-places>
   */
  const componentConfig = {
    bindings: { data: '<' },
    templateUrl: 'components/active-places/active-places.widget.html',
    controller: ActivePlacesController,
  };

  /* @ngInject */
  function ActivePlacesController(activityService) {
    Object.assign(this, {
      activities: [],
      doFunnyStuff, 
      $onInit,
    })

    function $onInit() {
      activityService.list()
        .then(response => { this.activities = response })
        .catch(console.error)
    }
    function doFunnyStuff() {
      this.activities.push('yolo');
    }
  }
  angular
    .module('app.widget.activeplaces')
    .component('activePlaces', componentConfig);

}());

I like to have a copy-pastable snippet at the top of my file, that make me gain a lot of time. And it is more explicit and useful when developing in a project than the module and component declaration AMHO.
I also like the Object.assign (ES2015 could use angular.extend) call to bind all the methods and props to the component scope and also shave the file of all those little vm..
I use ES6 shorthand notation for objects when I can.
I also dropped the vm capture variable for the this use. Since ES2015, it is way more usable and since NG1.5+ I always find a way not to use $watch. Bonus effect, I learn to use it instead of running away from it.

Here is a revision using ES6 if you are interested.

https://github.com/rwwagner90/angular-styleguide-es6

This is also opinionated but I like to keep it simple. No vm, just $ctrl. No division of what to be exposed to the view. Thoughts?

    (function(){
        'use strict';

        angular
            .module('app.shared')
            .component('buttonList', {
                bindings: {
                    data: '<',
                },
                template: `<span ng-repeat="button in $ctrl.buttons">
                                <button class="btn btn-default" ng-click="$ctrl.removeButton(button)">{{ ::button.name }}</button>
                            </span>`,
                controller: MyDataController
            });

        function MyDataController(dataService) {

            this.$onInit = function () {
                this.buttons = [];
                this.loadData();
            };

            this.removeButton = function (button) {
                var index = this.buttons.indexOf(button);
                this.buttons.splice(index, 1);
            };

        }

    })();

I helped push the team for a .component feature for A1 ... and I am thrilled they did this.

I also voted very publicly for vm.* to continue in the A1 version of .component. But the community voted on the issue to go with $ctrl. In the spirit of the style guide, consistency is most important. As such, I think $ctrl is the best option here and it is in line with the defaults mentioned in your notes above.

Other things to consider are

  1. how to define the configuration
  2. how to set properties in the component (this, vm, $ctrl)
    2b. if using this (see # 2 above) how to handle new closures (e.g. self) ... yuk
  3. Naming of the function (Component or Controller)
  4. how to handle callbacks in bindings
  5. naming of bindings

These are just some examples I grabbed out of the many ways I have seen and used .component thus far.

Let's keep the chat going on these and any other relevant ones.

I am keen on the 1st format that declares the component at the top. The reason is that the 2nd version introduces a new variable activePlaces, the name of which would need to be debated and which adds more complexity to reading the file.

I would also be keen to put the controller methods on the prototype. Unlike services, you can get multiple instances of controllers, especially if used in components, and it would seem more favourable not to be creating new copies of every method per instance.

Since components require a controller as, the default of $ctrl is my preferred option, you tend not to get context-free function calls so often of the controller methods.

I have always been calling the function [ComponentName]Controller since it is a controller for a component. I would recommend this, despite the fact that when we move to A2 these names would change - it would be a simple refactoring.

It is important in components to make use of the lifecycle hooks, rather than the controller constructor:

  • constructor - only simple initialisation of properties, such as attaching injected services to properties. No real work should be done here, since going forward none of the bindings will be assigned here.
  • $onInit - at this point the bindings and any required controllers have been assigned. This is where any real initialization work should be done.
  • $onChanges - this is called whenever any of the input (<) and interpolation (@) bindings change. It is important to realise that mutation of input objects does not trigger the interpolation. But this is a good place to put code that reacts to changes to the inputs, which saves on creating watches. I would suggest that if you ar going for a "one-way data flow" approach then this is a good place to make a copy of the input objects as they change so that you don't inadvertently mutate an object that is passed.
  • $onDestroy - the only reason for this method is to release or close long running resources that have been acquired or opened during the life of the component. Note that this hook does not get called if you manually destroy the component's isolate scope, nor if you manually remove the components element from the DOM. It only gets destroyed if the containing scope, the scope in which this component lives, is destroyed.

For output bindings I have been following the approach of having a handler method buttonClick(button) that actually triggers the output onButtonClick({$event: button}). And that the payload for the output is always put on the $event property. This means that on the outside you are able to do <button-list on-button-click="$ctrl.doStuff($event)"> which is similar to what you see in Angular 2.

So it all tends to look like this:

    (function(){
        'use strict';

        angular
            .module('app.shared')
            .component('buttonList', {
                bindings: {
                    buttons: '<',
                    onButtonClick: '&'
                },
                template: `<span ng-repeat="button in $ctrl.buttons">
                                <button class="btn btn-default" ng-click="$ctrl.buttonClick(button)">{{ ::button.name }}</button>
                            </span>`,
                controller: ButtonListController
            });

        function ButtonListController(dataService) {
            this.dataService = dataService;
        }

        ButtonListController.prototype = {
            $onInit: function () {
                this.dataService.loadData().then(function(data) {
                  this.data = data;
                }.bind(this));
            },
            $onChange: function(changes) {
                if (changes.buttons) {
                    this.buttons = angular.copy(this.buttons);
                }
            },
            buttonClick: function (button) {
              this.onButtonClick({ $event: { button });
            }
        };
    })();

@petebacondarwin I like what you have with a few changes.

I prefer not to embed the { } for the configuration for the component and instead to use a variable. I find this easier to read and debug. The name is irrelevant to me ...

var componentConfig = { ... };
angular
            .module('app.shared')
            .component('buttonList', componentConfig);

My larger concern is about the use of this and how functions can access the properties of the controller. The controller will have state ... yours has $ctrl.buttons in the template and I assume we'd have a this.buttons in the controller. When we create functions (prototype or otherwise) we have closures and some of those have their own this reference. This happens, for example, when we use $timeout(). How are you seeing these being resolved?

I had this problem in my example with the promise then handler. I chose to use .bind

yeah, but that is not fun :)

that's another reason we used vm in A1 controllers

Well you can always go with $ctrl in that case

On 14 Sep 2016 19:05, "John Papa" [email protected] wrote:

yeah, but that is not fun :)

that's another reason we used vm in A1 controllers

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/johnpapa/angular-styleguide/issues/766#issuecomment-247103228,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAA9J5DE4qIUEOEuPEIPLKobfDKT_Bbjks5qqDeEgaJpZM4Jyo-5
.

Right now I'm doing it like this (going with $ctrl):

( function() {
  'use strict';

  var component = {
    bindings: {
      data: '='
    },
    controller: controller,
    templateUrl: '/breadcrumbs.html'
  };

  angular.module( 'app' ).component( 'breadcrumbs', component );


  function controller( $log, $state ) {
    var logger = $log.getLogger( 'breadcrumbs' );
    var $ctrl = this;

    logger.log( $ctrl );

    $ctrl.goToState = goToState;


    function goToState( breadcrumb ) {
      logger.log( 'goToState()', breadcrumb );
      $state.go( breadcrumb.state, breadcrumb.data );
    }
  }
} )();

What do you guys think?

I guess one could be inspired from Todd Motto styleguide ?
https://github.com/toddmotto/angular-styleguide

Yes but that's ES2015. What happens with people still stuck with pre-ES2015?

Personally I've been using the following style:

(function(){
  'use strict';

  angular
    .module('app')
    .component('exampleComponent', exampleComponent());

  function exampleComponent() {
    var component = {
      bindings: {
        /* bindings - avoiding "=" */
      },
      controller: ExampleComponentController,
      controllerAs: 'vm',
      templateUrl: 'path/to/example-component.html'
    };

    return component;
  }

  ExampleComponentController.$inject = ['exampleService'];

  /* @ngInject */
  function ExampleComponentController(exampleService) {
    var vm = this;

    /* generic controller code */
  }
})();

What about providing unique prefix for component as in directive? Is it considered necessary?

@petebacondarwin I like your example as it emphasizes one way data bindings and the need to copy them in the $onChange lifecycle hook to truly achieve one-way data (i.e. state that a child may not change in a way that propagates to parent). A few questions:

  1. Why did you call this.dataService.loadData() inside of the $onInit hook? As far as I understand it, since dataService is declared as a dependency as a controller parameter, angular's dependency injection mechanisms will ensure such (singleton) service is available when the controller object is constructed. Would it make sense to make use of the require part of the component/directive API to showcase the $onInit guaranty instead?
  2. Why would you add to the prototype of ButtonListController when you can just as easily add these methods inside the constructor function itself without any loss in behavior, especially considering that ButtonListController will not be used as part of some prototypical chain of inheritance. Is there something you're trying to emphasize here?

@texonidas :

  1. Do you need to explicitly attach "$injectables" to a constructor function that references them as parameters?
  2. Why introduce exampleComponent() when you can just pass the component configuration (object literal) directly as a parameter to the component method? Among other things, you're introducing a variable named component, only to return the same variable and to method by the same name.

@jbmilgrom

  1. It is much easier to control your unit tests if you do not put "real" work in the constructor. If your constructor does work such as loading then it is impossible to prevent this from happening (or delaying it) inside your tests. WIth the added complication of the DI system doing the instantiation you lose even more control over the timing.

  2. Given that this component could be used numerous times in a single app it is more memory efficient to put the methods, which will be shared between instances, on the prototype.

@petebacondarwin thanks for the response, makes sense.

By declaring methods in the scope of the constructor function that reference variables also declared in the scope of the constructor function, you can gain a kinda private state (through a closure). For controllers that may be doing a little too much, this can be helpful for, among other things, distinguishing between state that could potentially be mutated directly in the template and state that undoubtedly cannot. That appears to not be possible in your setup. Instead, do you bind all state to this and delegate to services if privacy is desired?

@jbmilgrom

  1. The $inject is there for minification safety. It's not absolutely required, and if this was to go into the styleguide, I would remove it, as it is laid out explicitly further on in the section on minification.

  2. I went with the same syntax the styleguide recommends for services:

/* recommended */
function dataService() {
    var someValue = '';
    var service = {
        save: save,
        someValue: someValue,
        validate: validate
    };
    return service;

    ////////////

    function save() {
        /* */
    };

    function validate() {
        /* */
    };
}

in that you explicitly name the variable, then return it.

I am using an aproach similar to texonidas's, but decided to keep the default $ctrl in the html (not using controllerAs).

file name: example.component.js

    .module('app.moduleName')
    .component('exampleComponent', componentConfig());

function componentConfig () {
    return{
        bindings: {
            /*bindings*/
        },
        templateUrl: 'path/to/example.html',
        controller: exampleController
    };    
};

exampleController.$inject = ['exampleService'];

function exampleController (exampleService){
    var ctrl = this;
    ctrl.someFunction = someFunction;
    ctrl.$onInit = function(){
        ctrl.someVar = 'initial values';
   }
   .
   .
   .
};

Should the component be registered with the module?

My teams interpretation of the v1 style guide is that all controllers should be registered with the module and referenced by the name.

    .module('app.moduleName')
    .controller('exampleController', exampleController)
    .component('exampleComponent', componentConfig());


function componentConfig () {
    return{
        bindings: {
            /*bindings*/
        },
        templateUrl: 'path/to/example.html',
        controller: 'exampleController'
    };    
};
Was this page helpful?
0 / 5 - 0 ratings

Related issues

sgbeal picture sgbeal  Â·  7Comments

jusefb picture jusefb  Â·  9Comments

samithaf picture samithaf  Â·  12Comments

nonopolarity picture nonopolarity  Â·  5Comments

andreshg112 picture andreshg112  Â·  3Comments