Manipulating DOM in angularJS : best practice?

后端 未结 1 1814
别那么骄傲
别那么骄傲 2020-12-29 15:47

We are building a big web application using AngularJS. We use custom directive a lot for different cases. When it comes to do DOM manipulation, binding event, e

相关标签:
1条回答
  • 2020-12-29 16:19

    I think the "don't manipulate the DOM from controllers" mantra is back from the days, when directives mainly/only used linking functions (or directive controllers where just a way to intercommunicate with other directives).

    The currently suggested best practice is to use "components" (which can be realized via directives), where basically all the directive logic leaves in the controller. (Note for example that in Angular 2 there is no linking functions and each component/directive is basically a class/controller (plus some metadata).)

    In that context, I believe it is perfectly fine to manipulate the DOM in a directive's template from within the directive's controller.


    The idea is to keep your templates/HTML declarative. Compare the following snippets:

    <!--
      `SomeController` reaches out in the DOM and
      makes changes to `myComponent`'s template --- BAD
    -->
    <div ng-controller="SomeController">
      ...
      <my-component></my-component>
      ...
    </div>
    

    vs

    <div ng-controller="SomeController">
      ...
      <!--
        `myComponent`'s controller makes changes to
        `myComponent`'s template --- OK
      -->
      <my-component></my-component>
      ...
    </div>
    

    In the first (bad) example, myComponent will have different behavior/appearance depending on where in the DOM it appears (e.g. is it under SomeController ?). What's more important, it is very hard to find out what other (unrelated) part might be changing myComponent's behavior/appearance.

    In the second (good) example, myComponent's behavior and appearance will be consistent across the app and it is very easy to find out what it will be: I just have to look in the directive's definition (one place).


    There are a couple of caveats though:

    1. You don't want to mix your DOM manipulation code with other logic. (It would make your code less maintainable and harder to test).

    2. Often, you want to manipulate the DOM in the post-linking phase, when all children are in place (compiled + linked). Running the DOM manipulation code during controller instantiation would mean that the template content has not been processed yet.

    3. Usually, you don't want to run the DOM manipulation when your controller is not instantiated in the context of a directive, because that would mean you always need a compiled template in order to test your controller. This is undesirable, because it makes unit tests slower, even if you only want to test other parts of the controller logic that are not DOM/HTML related.

    So, what can we do ?

    • Isolate your DOM manipulation code in a dedicated function. This function will be called when appropriate (see below), but all DOM interaction will be in one place, which makes it easier to review.

    • Expose that function as a controller method and call it from your directive's linking function (instead of during controller initialization). This ensures that the DOM will be in the desired state (if that is necessary) and also decouples "stand-alone" controller instantiation from DOM manipulation.

    What we gain:

    • If your controller is instantiated as part of directive's compiling/linking, the method will be called and the DOM will be manipulated, as expected.

    • In unit-tests, if you don't need the DOM manipulation logic, you can instantiate the controller directly and test it's business logic (independently of any DOM or compiling).

    • You have more control over when the DOM manipulation happens (in unit tests). E.g. you can instantiate the controller directly, but still pass in an $element, make any assertions you might want to make, then manually call the DOM-manipulating method and assert that the element is transformed properly. It is also easier to pass in a mocked $element and stuff like adding event listeners, without having to set up a real DOM.

    The downside of this approach (exposing method and calling it from the linking function), is the extra boilerplate. If you are using Angular 1.5.x, you can spare the boilerplate by using the directive controller lifecycle hooks (e.g. $onInit or $postLink), without a need to have a linking function, just to get hold of the controller and call a method on it. (Bonus feature: Using the 1.5.x component syntax with lifecycle hooks, would make it easier to migrate to Angular 2.)

    Examples:

    Before v1.5.x

    .directive('myButton', function myButtonDirective() {
      // DDO
      return {
        template: '<button ng-click="$ctrl.onClick()></button>',
        scope: {}
        bindToController: {
          label: '@'
        }
        controllerAs: '$ctrl',
        controller: function MyButtonController($element) {
          // Variables - Private
          var self = this;
    
          // Functions - Public
          self._setupElement = _setupElement;
          self.onClick = onClick;
    
          // Functions - Definitions
          function _setupElement() {
            $element.text(self.label);
          }
    
          function onClick() {
            alert('*click*');
          }
        },
        link: function myButtonPostLink(scope, elem, attrs, ctrl) {
          ctrl._setupElement();
        }
      };
    })
    

    After v1.5.x

    .component('myButton', {
      template: '<button ng-click="$ctrl.onClick()></button>',
      bindings: {
        label: '@'
      }
      controller: function MyButtonController($element) {
        // Variables - Private
        var self = this;
    
        // Functions - Public
        self.$postLink = $postLink;
        self.onClick = onClick;
    
        // Functions - Definitions
        function $postLink() {
          $element.text(self.label);
        }
    
        function onClick() {
          alert('*click*');
        }
      }
    })
    
    0 讨论(0)
提交回复
热议问题