How to properly bind object in 'bindings' using $compile in angular.js 1?

691 views Asked by At

I want to dynamically compile component for inserting this to specific DOM element (DOM also dynamically created by 3rd party library). So, I use $compile, $scope.

https://jsbin.com/gutekat/edit?html,js,console,output

// ListController $postLink life cycle hook

function $postLink() {
  ...

  $timeout(function () {
    ctrl.items.splice(0, 1);
    $log.debug('First item of array is removed');
    $log.debug(ctrl.items);
  }, 2000);
}

but below $onChanges life cycle hook in ListItemController isn't executed.

// ListItemController $onChanges life cycle hook

function $onChanges(changes) {
  if (!changes.item.isFirstChange()) {
    $log.debug(changes);  // Not executed
  }
}

I guess that angular.merge to pass item before ListItemController controller instance initialization is a major cause.

var itemScope = $scope.$new(true, $scope);
itemScope = angular.merge(itemScope, {
  $ctrl: {
    item: item
  }
});
1

There are 1 answers

0
Anthony C On

I modified you code a bit to demonstrate what is going on w/ the one way binding.

angular.module('app', [
    'list.component',
    'list-item.component'
]);

/**
 * list.component
 */
angular
    .module('list.component', [])
    .component('list', {
        controller: ListController,
        template: '<div id="list"></div>'
    });

ListController.$inject = ['$compile', '$document', '$log', '$scope', '$timeout'];
function ListController($compile, $document, $log, $scope, $timeout) {
    var ctrl = this;

    ctrl.$onInit = $onInit;
    ctrl.$postLink = $postLink;

    function $onInit() {
        ctrl.items = [
            {
                id: 0,
                value: 'a'
            },
            {
                id: 1,
                value: 'b'
            },
            {
                id: 2,
                value: 'c'
            }
        ];
    }

    function $postLink() {
        var index = 0;
        // Not entirely sure what you need to do this. This can easily be done in the template.
        /** ie:
         * template: '<div id="list" ng-repeat="item in $ctrl.items"><list-item item="item"></list-item></div>'
         **/

        var iElements = ctrl.items.map(function (item) {
            var template = '<list-item item="$ctrl.items[' + (index) + ']"></list-item>';
            index++;
            // you don't want to create an isolate scope here for the 1 way binding of the item.
            return $compile(template)($scope.$new(false));
        });

        var listDOM = $document[0].getElementById('list');
        var jqListDOM = angular.element(listDOM);

        iElements.forEach(function (iElement) {
            jqListDOM.append(iElement);
        });

        $timeout(function () {
            // this will trigger $onChanges since this is a reference change
            ctrl.items[0] = { id: 3, value: 'ss' };
            // this however, will not trigger the $onChanges, if you need to use deep comparison, consider to use $watch
            ctrl.items[1].value = 's';
            ctrl.items[2].value = 's';
        }, 2000);
    }
}

/**
 * list-item.component
 */
angular
    .module('list-item.component', [])
    .component('listItem', {
        bindings: {
            item: '<'
        },
        controller: ListItemController,
        template: '<div class="listItem">{{ $ctrl.item.value }}</div>'
    });

ListItemController.$inject = ['$log'];
function ListItemController($log) {
    var ctrl = this;

    ctrl.$onChanges = $onChanges;

    function $onChanges(changes) {
        if (!changes.item.isFirstChange()) {
            $log.debug(changes);    // Not executed      
        }
    }
}