Search code examples
javascriptangularjsangularjs-directiveangularjs-scopeangularjs-ng-transclude

directive with bindToController can't get data from child directives


I'm tring to write a directive that builds an object from its child directive's input and pushes it to an array provided as a parameter. Something like:

<aggregate-input on="SiteContent.data.objList">
    <p aggregate-question>Question text</p>
    <input aggregate-answer ng-model="answer" type="text" />
</aggregate-input>
<aggregate-input on="SiteContent.data.objList">
    <p aggregate-question>Question text 2</p>
    <input aggregate-answer ng-model="answer" type="text" />
</aggregate-input>

I'm looking to collect the data like:

SiteContent.data.objList === [
    {question: 'Quesion text', answer: 'user input'},
    {question: 'Quesion text 2', answer: 'user input 2'},
];

Here's the plunker with the code.

  • update 1: @jrsala included scope and bindToController syntax changes

I'm having trouble figuring out the way these directives should communicate. I'm expecting the input object defined in the link will be isolated in the scope of each directive and pushed to the on object provided. The result is that the input object is shared among all instances, and only one object gets ever pushed to the array.

I'm guessing the transcluded scope rules are confusing me, but I really don't see where. Any ideas? Thanks!


Solution

  • First issue: your aggregate-input directive specifies an isolate scope with no bound properties but you still use an on attribute on the element with the directive on it:

    <aggregate-input on="SiteContent.data.objList">
    

    but in your JS,

    {
        restrict: 'E',
        scope: {},
        controller: aggregateInputController,
        controllerAs: 'Aggregate',
        bindToController: { on: '=' },
        /* */
    }
    

    whereas what you need is

    {
        restrict: 'E',
        scope: { on: '=' },
        controller: aggregateInputController,
        controllerAs: 'Aggregate',
        bindToController: true // BOOLEAN REQUIRED HERE, NO OBJECT
    }
    

    As per the spec's paragraph on bindToController,

    When an isolate scope is used for a component (see above), and controllerAs is used, bindToController: true will allow a component to have its properties bound to the controller, rather than to scope. When the controller is instantiated, the initial values of the isolate scope bindings are already available.

    Then you do not need to assign the on property to your controller, it's done for you by Angular (also I did not understand why you did this.on = this.on || [], the this.on || part looks unnecessary to me).

    I suppose you can apply that to the rest of the code and that should be a start. I'm going to look for more issues.

    edit: Several more issues that I found:

    • If the scope of siteContent is isolated then the SiteContent controller is not accessible when the directive is compiled and Angular silently fails (like always...) when evaluating SiteContent.data.objList to pass it to the child directives. I fixed that by removing scope: {} from its definition.

    • It was necessary to move the functionality of aggregateInputLink over to aggregateInputController because, as usual, the child controllers execute before the link function, and since the aggregateQuestion directive makes the call InputCtrl.changeValue('question', elem.text()); in its controller, the scope.input assigned to in the parent directive post-link function did not exist yet.

    function aggregateInputController($scope) {
        $scope.input = {};
        this.on.push($scope.input);
    
        this.changeValue = function changeValue(field, value) {
            $scope.input[field] = value;
        };
    }
    

    As a reminder: controllers are executed in a pre-order fashion and link functions in a post-order fashion during the traversal of the directive tree.

    • Finally, after that, the SiteContent controller's data did not get rendered property since the collection used to iterate over in ng-repeat was erroneously SiteContent.objList instead of SiteContent.data.objList.

    Link to final plunker