Question

We have a contact form we use in many applications. There are many default values, validation rules, structure, etc, that are repeated. We're working on a set of directives in order to make the view more semantic and less verbose.

There are a few targets we're shooting for.

  1. Defining the contact form model once in a parent directive like this: <div my-form model='formModel'>. Associated children directives would be able to get the base model from the model attribute.

  2. Supply the default configuration (size, validation rules, placeholders, classes, etc) for each input, but allow the possibility for attributes to be overwritten if necessary. Thus, we are creating child directives using the my-form directive's controller for communication. We also want these child directives to bind to the application controller's model formModel.

I'm having some trouble with implementing this.

  • formModel is exposed through the parent directive's controller, but I'm having to manually $compile the child directive using scope.$parent in the link function. This seems smelly to me, but if I try to use the child directive's scope the compiled HTML contains the correct attribute (it's visible in the source), but it isn't bound to the controller and it doesn't appear on any scope when inspected with Batarang. I'm guessing I'm adding the attribute too late, but not sure how to add the attribute earlier.

  • Although I could just use ng-model on each of the child directives, this is exactly what I'm trying to avoid. I want the resulting view to be very clean, and having to specify the model names on every field is repetitive and error-prone. How else can I solve this?

Here is a jsfiddle that has a working but "smelly" setup of what I'm trying to accomplish.

angular.module('myApp', []).controller('myCtrl', function ($scope) {
    $scope.formModel = {
        name: 'foo',
        email: 'foo@foobar.net'
    };
})
    .directive('myForm', function () {
    return {
        replace: true,
        transclude: true,
        scope: true,
        template: '<div ng-form novalidate><div ng-transclude></div></div>',
        controller: function ($scope, $element, $attrs) {
            $scope.model = $attrs.myModel;
            this.getModel = function () {
                return $scope.model;
            };
        }
    };
})
    .directive('myFormName', function ($compile) {
    return {
        require: '^myForm',
        replace: true,
        link: function (scope, element, attrs, parentCtrl) {

            var modelName = [parentCtrl.getModel(),attrs.id].join('.'),
                template = '<input ng-model="' + modelName + '">';

            element.replaceWith($compile(template)(scope.$parent));
        }
    };
});
Was it helpful?

Solution

There is a much simpler solution.

Working Fiddle Here

Parent Form Directive

First, establish an isolated scope for the parent form directive and import the my-model attribute with 2-way binding. This can be done by specifying scope: { model:'=myModel'}. There really is no need to specify prototypical scope inheritance because your directives make no use of it.

Your isolated scope now has the 'model' binding imported, and we can use this fact to compile and link child directives against the parent scope. For this to work, we are going to expose a compile function from the parent directive, that the child directives can call.

.directive('myForm', function ($compile) {
return {
    replace: true,
    transclude: true,
    scope: { model:'=myModel'},
    template: '<div ng-form novalidate><div ng-transclude></div></div>',
    controller: function ($scope, $element, $attrs) {
        this.compile = function (element) {
            $compile(element)($scope);
        };
    }
}; 

Child Field Directive

Now its time to setup your child directive. In the directive definition, use require:'^myForm' to specify that it must always reside within the parent form directive. In your compile function, add the ng-model="model.{id attribute}". There is no need to figure out the name of the model, because we already know what 'model' will resolve to in the parent scope. Finally, in your link function, just call the parent controller's compile function that you setup earlier.

.directive('myFormName', function () {
return {
    require: '^myForm',
    scope: false,
    compile: function (element, attrs) {
        element.attr('ng-model', 'model.' + attrs.id);
        return function(scope, element, attrs, parentCtrl) {
            parentCtrl.compile(element);

        };
      }
   };
});

This solution is minimal with very little DOM manipulation. Also it preserves the original intent of compiling and linking input form fields against the parent scope, with as little intrusion as possible.

OTHER TIPS

It turns out this question has been asked before (and clarified) here, but never answered.

The question was also asked on the AngularJS mailing list, where the question WAS answered, although the solution results in some smelly code.

Following is Daniel Tabuenca's response from the AngularJS mailing list changed a bit to solve this question.

.directive('foo', function($compile) {

  return {
    restrict: 'A',
    priority: 9999,
    terminal: true, //Pause Compilation to give us the opportunity to add our directives
    link: function postLink (scope, el, attr, parentCtrl) {
        // parentCtrl.getModel() returns the base model name in the parent
        var model = [parentCtrl.getModel(), attr.id].join('.');
        attr.$set('ngModel', model);
        // Resume the compilation phase after setting ngModel
        $compile(el, null /* transclude function */, 9999 /* maxPriority */)(scope);
    }
  };
});

Explanation:

First, the myForm controller is instantiated. This happens before any pre-linking, which makes it possible to expose myForm's variables to the myFormName directive.

Next, myFormName is set to the highest priority (9999) and the property of terminal is set true. The devdocs say:

If set to true then the current priority will be the last set of directives which will execute (any directives at the current priority will still execute as the order of execution on same priority is undefined).

By calling $compile again with the same priority (9999), we resume directive compilation for any directive of a lower priority level.

This use of $compile appears to be undocumented, so use at your own risk.

I'd really like a nicer pattern for follow for this problem. Please let me know if there's a more maintainable way to achieve this end result. Thanks!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top