AngularJS:子输入指令需要在父指令的范围内进行编译,以便ng-model绑定

时间:2022-03-03 19:39:08

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.

    为每个输入提供默认的配置(大小、验证规则、占位符、类等),但允许在必要时重写属性。因此,我们使用my-form指令的控制器来创建子指令。我们还希望这些子指令绑定到应用程序控制器的模型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.

    formModel是通过父指令的控制器公开的,但是我必须使用作用域手工编译子指令。$parent在链接函数中。这在我看来似乎有点臭,但是如果我尝试使用子指令的作用域,编译后的HTML包含正确的属性(它在源代码中是可见的),但是它没有绑定到控制器,并且在使用Batarang检查时它不会出现在任何作用域上。我猜我添加属性太晚了,但是不知道如何在前面添加属性。

  • 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?

    虽然我可以在每个子指令上使用ng-model,但这正是我要避免的。我希望结果视图非常干净,并且必须在每个字段上指定模型名称是重复的和容易出错的。我还能怎么解决这个问题呢?

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

这是一个jsfiddle,它具有一个可以工作的但是“有味道”的设置,用于描述我要完成的任务。

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));
        }
    };
});

2 个解决方案

#1


2  

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.

首先,为父表单指令建立一个独立的范围,并导入带有双向绑定的my-model属性。这可以通过指定范围:{model:'=myModel'}来实现。实际上没有必要指定原型范围继承,因为您的指示没有使用它。

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.

现在是设置您的子指令的时候了。在指令定义,使用要求:“^ myForm”来指定它必须驻留在父窗体指令。在编译函数中,添加ng-model=“模型”。{ id属性}”。不需要计算模型的名称,因为我们已经知道在父范围内“model”将解析为什么。最后,在链接函数中,只需调用前面设置的父控制器的编译函数。

.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.

只需很少的DOM操作,这个解决方案就很简单。它还保留了编译和链接输入表单字段到父范围的初衷,并尽可能减少入侵。

#2


1  

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.

这个问题也在AngularJS邮件列表中被提出来,在那里问题得到了回答,尽管解决方案导致了一些糟糕的代码。

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

下面是Daniel Tabuenca对AngularJS邮件列表的回复,为了解决这个问题,他做了一些改动。

.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.

首先,实例化myForm控制器。这发生在任何预链接之前,这使得将myForm的变量暴露给myFormName指令成为可能。

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

接下来,myFormName设置为最高优先级(9999),终端的属性设置为true。devdocs说:

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).

如果设置为true,那么当前优先级将是将要执行的最后一组指令(当前优先级上的任何指令仍将执行,因为相同优先级上的执行顺序未定义)。

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

通过再次调用具有相同优先级的$compile(9999),我们重新为任何低优先级的指令进行指令编译。

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

这种使用$compile的行为似乎是没有记录的,因此使用它要自负风险。

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!

我真的想要一个更好的模式来解决这个问题。请让我知道是否有更易于维护的方法来实现这个最终结果。谢谢!

#1


2  

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.

首先,为父表单指令建立一个独立的范围,并导入带有双向绑定的my-model属性。这可以通过指定范围:{model:'=myModel'}来实现。实际上没有必要指定原型范围继承,因为您的指示没有使用它。

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.

现在是设置您的子指令的时候了。在指令定义,使用要求:“^ myForm”来指定它必须驻留在父窗体指令。在编译函数中,添加ng-model=“模型”。{ id属性}”。不需要计算模型的名称,因为我们已经知道在父范围内“model”将解析为什么。最后,在链接函数中,只需调用前面设置的父控制器的编译函数。

.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.

只需很少的DOM操作,这个解决方案就很简单。它还保留了编译和链接输入表单字段到父范围的初衷,并尽可能减少入侵。

#2


1  

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.

这个问题也在AngularJS邮件列表中被提出来,在那里问题得到了回答,尽管解决方案导致了一些糟糕的代码。

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

下面是Daniel Tabuenca对AngularJS邮件列表的回复,为了解决这个问题,他做了一些改动。

.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.

首先,实例化myForm控制器。这发生在任何预链接之前,这使得将myForm的变量暴露给myFormName指令成为可能。

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

接下来,myFormName设置为最高优先级(9999),终端的属性设置为true。devdocs说:

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).

如果设置为true,那么当前优先级将是将要执行的最后一组指令(当前优先级上的任何指令仍将执行,因为相同优先级上的执行顺序未定义)。

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

通过再次调用具有相同优先级的$compile(9999),我们重新为任何低优先级的指令进行指令编译。

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

这种使用$compile的行为似乎是没有记录的,因此使用它要自负风险。

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!

我真的想要一个更好的模式来解决这个问题。请让我知道是否有更易于维护的方法来实现这个最终结果。谢谢!