angularjsangular-componentsangularjs-ng-transclude

AngularJS component nesting


I created a component search-context, which works well. It's configurable and it does what it's supposed to do.

<search-context context-name="Groups"
    compare-columns="['displayName']"
    search-manager="$ctrl"
    query-url="/group/search/{{contextId}}"
    icon="fa fa-users"
    on-resolve-item-url="resolveItemUrl(row)"></search-context>

Here it is in action, standalone.

search-context in action, standalone

There are various other search contexts, and I'd like to create a search-manager component such that I can write markup like this:

<search-manager>
    <search-context context-name="Devices"
                    compare-columns="['displayName']"
                    search-manager="$ctrl"
                    query-url="/device/search/{{contextId}}"
                    icon="fa fa-laptop"></search-context>
    <search-context context-name="Groups"
                    compare-columns="['displayName']"
                    search-manager="$ctrl"
                    query-url="/group/search/{{contextId}}"
                    icon="fa fa-users"
                    on-resolve-item-url="resolveGroupEditUrl(row)"></search-context>
</search-manager>

The general plan is for search-context to check whether it has a search-manager and if so suppress its own input/button controls, and the search-manager will supply input controls and supply the search term to the search contexts.

The examples in the AngularJS component documentation demonstrate dynamic child controls using ng-repeat in the control template, but it's not clear how to set things up to handle explicit markup such as I propose. If at all possible I'd prefer not to need to explicitly specify the search-manager="$ctrl" parent reference.

How does one go about this and what are the supporting topics one must research and understand? Just the key concept names would be a big help but an overview and a further-reading list would be awesome.

My first attempt at the template for search-manager looks like this

<div>
    <div class="panel-heading">
        <h3 class="panel-title">Search</h3>
        <div class="input-group">
            <input class="form-control" ng-model="$ctrl.term" />
            <span class="input-group-btn" ng-click="$ctrl.search()">
                <button class="btn btn-default">
                    <i class="fa fa-search"></i>
                </button>
            </span>
        </div>
    </div>
    <div class="panel-body">
        <ng-transclude></ng-transclude>
    </div>

</div>

The code looks like this

function SearchManagerController($scope, $element, $attrs, $http) {
    var ctrl = this;
    ctrl.searchContext = [];
    ctrl.registerSearchContext = function (searchContext) {
        ctrl.searchContext.push(searchContext);
    }
    ctrl.search = function () {
        ctrl.searchContext.forEach(function (searchContext) {
            searchContext.search(ctrl.term);
        });
    };
}
angular.module("app").component("searchManager", {
    templateUrl: "/app/components/search-manager.html",
    controller: SearchManagerController,
    transclude: true,
    bindings: {
        term: "@"
    }
});

The child components are transcluded but they need a reference to the search-manager component, and $ctrl is not in scope.

How do we get a reference to the parent?


Solution

  • To obtain a reference to the parent all you need to do is require the parent in the search-context declaration. The double caret prefix means to search the parents. Single caret starts with the current object which will work but is slightly less efficient. The question mark means don't barf if you can't find it, just return undefined. This is necessary when the component may not always be parented by a search manager.

    angular.module("app").component("searchContext", {
        templateUrl: "/app/components/search-context.html",
        controller: SearchContextController,
        require: {
            searchManagerCtrl: "?^^searchManager"
        },
        bindings: {
            ...
        }
    });
    

    But what if you need the parent to have references to the children?

    In the SearchContextController we implement the $onInit lifecycle event handler.

        ctrl.$onInit = function () {
            if (ctrl.searchManagerCtrl) {
                ctrl.searchManagerCtrl.registerSearchContext(ctrl);
            }
        };
    

    registerSearchContext is a method defined in the parent's controller for this purpose. The implementation essentially pushes each registered control into an array property we define on its scope, and then methods of the parent can enumerate the children.

    For a directive this require trick is expressed slightly differently. You must declare the property searchManagerCtrl in the directive scope, and supply the expression directly as the value of require.

    require: "?^^searchManager",
    

    You must also supply a link function. One of the parameters of a link function is controller and a reference to searchManager will be passed in this parameter, at which point you can assign it to a property of the directive scope. The $onInit lifecycle event is still available for registering with the search manager, but refers to $scope rather than ctrl.