Search code examples
angularjsinterfaceproxydirectivefacade

AngularJS proxy directive


I am trying to create a proxy directive like so:

<x-field x-purpose="choice" x-values="data.countries" ng-model="model.country"/>

Where the field directive forwards this to another directive, causing the following replacement:

<x-choiceField x-values="data.countries" ng-model="model.country"/>

[note:] the ng-model could be replaced by a reference to some new isolated scope.

The "field purpose" directive decides which implementation to use (e.g. drop-down/listbox/autocomplete?) based on how many values there are to choose from, client device size, etc - ultimately resulting in something like this:

<select ng-model="model.country" ng-options="data.countries">

This design is largely out of curiosity rather than for any practical reason, I am interested in how to achieve it rather than whether it is actually a good idea from a performance/simplicity point of view...

After reading [https://stackoverflow.com/a/18895441/1156377], I have something like this:

function proxyDirective($injector, $parse, element) {
    return function (scope, element, attrs) {
        var target = element.camelCase(attrs.name + '-field');
        var model = attrs.ngModel;
        var value = $parse(model);
        var directive = $injector.get(target);
        /* Bind ngModel to new isolated scope "value" property */
        scope.$watch(model, function () {
            ???
        });
        /* Generate new directive element */
        var pElement = angular.element.html('');
        var pAttrs = {
            value: ???
        };
        /* Forward to new directive */
        return directive.compile(element, attrs, null)(scope, element, attrs);
    };
}

function alphaFieldDirective() {
    return {
        replace: 'true',
        template: '<input type="text" ng-value="forwarded value?">'
    };
}

function betaFieldDirective() {
    return {
        replace: 'true',
        template: '<textarea attributes? >{{ value }}</textarea>'
    };
} 

But I'm not sure how to achieve the forwarding or binding. This is my first forage into Angular directives, and it doesn't seem to be a particularly popular way of using them!

The purpose of this is to separate the purpose of a form field from its appearance/implementation, and to provide one simple directive for instantiating fields.


Solution

  • I implemented this via a service which proxies directives:

    Fiddle: http://jsfiddle.net/HB7LU/7779/

    HTML:

    <body ng-app="myApp">
        <h1>Directive proxying</h1>
        <proxy target="bold" text="Bold text"></proxy>
        <h1>Attribute forwarding</h1>
        <proxy target="italic" style="color: red;" text="Red, italic text"></proxy>
    </body>
    

    Javascript:

    angular.module('myApp', [])
        .factory('directiveProxyService', directiveProxyService)
        .directive('proxy', dirProxy)
        .directive('bold', boldDirective)
        .directive('italic', italicDirective)
        ;
    
    function directiveProxyService($compile) {
        return function (target, scope, element, attrs, ignoreAttrs) {
            var forward = angular.element('<' + target + '/>');
            /* Move attributes over */
            _(attrs).chain()
                .omit(ignoreAttrs || [])
                .omit('class', 'id')
                .omit(function (val, key) { return key.charAt(0) === '$'; })
                .each(function (val, key) {
                    element.removeAttr(attrs.$attr[key]);
                    forward.attr(attrs.$attr[key], val);
                });
            $compile(forward)(scope);
            element.append(forward);
            return forward;
        };
    }
    
    function dirProxy(directiveProxyService) {
        return {
            restrict: 'E',
            terminal: true,
            priority: 1000000,
            replace: true,
            template: '<span></span>',
            link: function (scope, element, attrs) {
                directiveProxyService(attrs.target, scope, element, attrs, ['target']);
            }
        };
    }
    
    function boldDirective() {
        return {
            restrict: 'E',
            replace: true,
            template: '<i>{{ text }}</i>',
            scope: { text: '@' }
        };
    }
    
    function italicDirective() {
        return {
            restrict: 'E',
            replace: true,
            template: '<i>{{ text }}</i>',
            scope: { text: '@' }
        };
    }