如何协调Angular的“始终使用点与ngModel”规则与隔离范围?

时间:2014-10-20 06:53:41

标签: javascript angularjs twitter-bootstrap angularjs-scope

我正在使用Bootstrap开发一个Angular应用程序。

为了最大限度地减少HTML上的Bootstrap占用空间,我已经为表单引入了两个指令:

形状control.js

module.directive('formControl', function() {
  return {
    restrict : 'E',
    templateUrl : 'form-control.tmpl.html',
    scope: {
      label: '@'
    },
    transclude : true
  };
});

形状control.tmpl.html

<div class="form-group">
  <label class="control-label col-sm-2">
    {{ label }}
  </label>
  <div class="col-sm-10"
       ng-transclude>
  </div>
</div>

对于各种表单输入字段,我还对该指令有一些“扩展”。 e.g:

形式输入-text.js

module.directive('formInputText', function() {
  return {
    restrict : 'E',
    templateUrl : 'form-input-text.tmpl.html',
    scope: {
      label: '@',
      value: '=ngModel'
    }
  };
});

形式输入-text.tmpl.html

<form-control label="{{label}}">
  <input type="text"
         class="form-control"
         ng-model="value">
</form-control>

app.html

<form-input-text label="Name"
                 ng-model="person.name">
</form-input-text>

这里我遇到了一个问题。在这个例子中有许多范围:

appScope = { person : { name : "John" } };
isolateScope = {
  label: "Name",
  value: "John" // bound two-way with appScope.person.name
};
transcludeScope = {
  __proto__: isolateScope,
  label: "Name", // inherited from isolateScope
  value: "John" // inherited from isolateScope
};

如果我更改输入文本框中的文本,则仅修改transcludeScope

appScope = { person : { name : "John" } };
isolateScope = {
  label: "Name",
  value: "John" // bound two-way with appScope.person.name
};
transcludeScope = {
  __proto__: isolateScope,
  label: "Name", // inherited from isolateScope
  value: "Alice" // overrides value from isolateScope
};

这是因为<input>直接绑定到transcludeScope的属性。 transcludeScope.value直接更改,父作用域isolateScope不受影响。因此,输入中的任何模型更改都不会使其返回appScope

我想要做的是在appScope.person.nameisolateScope的嵌套属性之间创建双向绑定,例如isolateScope.model.value

理想情况下,我想宣布我的指令:

形式输入-text.js

module.directive('formInputText', function() {
  return {
    restrict : 'E',
    templateUrl : 'form-input-text.tmpl.html',
    scope: {
      model: {
        label: '@',
        value: '=ngModel'
      }
    }
  };
});

这将允许被转换的部分绑定到model.value,这会使isolateScope更改可见,这会将isolateScope中的更改传播回appScope

Angular似乎没有直接支持此用法。

任何人都可以向我指出支持此用例的Angular功能,或者如果没有,提供解决方法吗?

编辑:

目前,我的解决方案是将form-control模板内嵌到form-input-text模板中。

形式输入-text.tmpl.html

<div class="form-group">
  <label class="control-label col-sm-2">
    {{ label }}
  </label>
  <div class="col-sm-10">
    <input type="text"
           class="form-control"
           ng-model="value">
  </div>
</div>

这消除了ng-transclude引入的子范围,但它也复制了标记,这是我希望重构到一个地方的。

2 个答案:

答案 0 :(得分:2)

对范围的思考实际上是在错误的轨道上进行,我不认为翻译与它有很大关系。要“正确”执行此操作,您应该与ngModelController集成。这允许任何后来的集成解析器和格式化程序(可能包含验证逻辑)在适当的时间运行。它有点复杂,因为你有2个:应用程序中的父元素和指令模板中的元素,每个都有2个“管道”要集成:

  • 模型值 - &gt;查看值
  • 查看值 - &gt;模型值

然后将父ngModelController的视图值用作内部ngModelController的模型值。因此总体管道看起来像

  • 父模型值 - &gt;父视图值 - &gt;内部模型值 - &gt;内部观点值
  • 内部视图值 - &gt;内部模型值 - &gt;父视图值 - &gt;父模型值

要做到这一点:

  • 确保您在指令定义中require: 'ngModel'有权访问父ngModelController

  • 从父级ngModelController到内部的更改是使用父$render的{​​{1}}方法完成的,使用其ngModelController。这可以确保父$viewValue中的任何函数都已运行。

  • 内部指令的用户启动更改是通过向其$formatters数组添加一个函数来完成的,该数组在父$viewChangeListeners上调用$setViewValue。要从链接函数作用域访问它,您需要一个命名表单和输入元素。一个轻微的烦恼是表单只在指令的链接功能运行后才在指令的范围内注册,所以你需要一个观察者来访问它。

  • 如果出现任何奇怪现象,请确保ngModelController中的模型位于对象中。 (我不确定这在技术上是否必要)

  • 然后需要将模型放在内部指令的formInputText对象中。

把它们放在一起,

scope

它的模板看起来像

app.directive('formInputText', function() {
  return {
    restrict : 'E',
    templateUrl : 'form-input-text.tmpl.html',
    scope: {
      label: '@'
    },
    require: 'ngModel',
    link: function(scope, element, attrs, ngModelController) {
      scope.model = {};

      // Propagate changes from parent model to local
      ngModelController.$render = function() {
        scope.model.value = ngModelController.$viewValue;
      };

      // Propagate local user-initiated changes to parent model
      scope.$watch('form', function(form) {
        if (!form) return;
        form.input.$viewChangeListeners.push(function() {
          ngModelController.$setViewValue(form.input.$modelValue);
        });       
      });
    }
  };
});

可以看到http://plnkr.co/edit/vLGa6c55Ll4wV46a9HRi?p=preview

答案 1 :(得分:1)

我将为您的案例使用自定义控件,如here所述,使自定义<form-input-*>指令成为真正的控件。它确实需要一些额外的工作。概述解决方案的简单版本:

<强>形式输入-text.js

app.directive('formInputText', function() {
    return {
        restrict : 'E',
        template : '<form-control label="{{label}}"><input type="text" class="form-control" /></form-control>',
        scope: {
            label: '@'
        },
        require: 'ngModel',
        link: function(scope, elem, attrs, ngModel) {
            var input = angular.element(elem[0].querySelectorAll("input")[0]);

            ngModel.$render = function() {
                input.val(ngModel.$viewValue || '');
            };

            input.on('blur keyup change', function() {
                scope.$apply(read);
            });

            function read() {
                ngModel.$setViewValue(input.val());
            }
        }
    };
});

简而言之,根据文档,您require ngModel并实施其方法。 ngModel只是应用于您的控件的另一个指令,其他功能也可以使用,例如自定义验证程序,ng-required等。

工作小提琴:http://jsfiddle.net/1n53q59z/

请记住,根据您的使用情况,您可能需要进行一些调整。