【问题标题】:Is there a simple way to make a whole DOM subtree readonly with angularjs?有没有一种简单的方法可以用 angularjs 使整个 DOM 子树只读?
【发布时间】:2014-07-19 23:45:46
【问题描述】:

我有几个inputs,当某些属性发生变化时(例如,购物车被签出),它们都应该变成readonly

我可以在每个这样的输入中添加ng-readonly=cart.checkedOut,这很无聊,容易忘记,而且由于有些部分无法访问cart而变得复杂。

所以我正在考虑修改 input(以及 textareaselect,它们只是错误命名的输入),以便它在其 DOM 祖先中查找类 readonly 并可能将自己设为只读。我的问题是与ng-readonly 的交互,这可能使input 出于不同的原因只读。看起来我必须修改 ng-readonly,但 AFAIK 无法修改现有指令,只能在同名中添加另一个行为。

不知何故,我感到迷茫;我确信有一个简单的解决方案,但是当我深入研究时,一切都变得复杂了。告诉我正确的方向就足够了。或者可能是原始问题的不同解决方案。

【问题讨论】:

  • 使用可以检查任何范围条件并相应设置属性的指令

标签: angularjs readonly


【解决方案1】:

请注意,readonly 属性会被多个表单输入元素忽略。

因此,ng-readonly 对于某些组件可能无法正常工作(即它不会阻止用户交互编辑值)。

也就是说,解决您的问题的一种方法是制作一个指令来监视签出状态并动态添加或删除适当的输入元素属性,使它们看起来被锁定以进行编辑。

就像我上面提到的,您需要关注的属性取决于您使用的元素类型。对于那些readonly 不适用的情况,例如select,您可以选择使用disable。但是,禁用的元素不能提交,这就是为什么hidden输入类型经常被用来补充select元素的原因。在您的情况下,这似乎也是不可取的,因为您不希望触摸您的 DOM。

要仍然使用disable 并解决后一个问题,在提交表单时,您可以制作另一个指令以从具有它们的输入元素中去除所有disabled 属性,确保所有输入数据都被提交。

这是关于如何动态管理输入元素属性的JSBin 的基本演示。

【讨论】:

    【解决方案2】:

    这是我设计的一个解决方案,它扩展了 inputselecttextarea 指令。

    在应用程序的配置块中使用$provide.decorator,我们可以增强这些指令的行为,而无需触及它们的内部实现。

    我建议您做的是为您从包含$scope 广播的事件设置一个侦听器。附加一个回调,检查当前输入指令的nodeName - 并关闭用户输入(使用readOnlydisabled)。

    类似的东西:

    app.config(function ($provide) {
    
      var directives = [
        'input',
        'select',
        'textarea'
      ];
    
      var eventName = '__disableInputs';
    
      angular.forEach(directives, function (directive) {
    
        $provide.decorator(directive + 'Directive', function ($delegate, $controller) {
    
          // Decorating directives returns an array. Select the first index.
          var directive = $delegate[0];
    
          // If a controller is tied to the directive, store a reference to it.
          if (directive.controller) {
            var origCtrl = directive.controller;
          }
    
          // Define a new controller on the directive.
          directive.controller = function ($scope, $element, $attrs) {
    
            if (origCtrl) {
              // Extend this new controller with the old behaviour of the 
              // original controller.
              angular.extend(this, $controller(origCtrl, { 
                $scope: $scope,
                $element: $element,
                $attrs: $attrs
              }));
            }
    
            // Add an event listener to the directive controller.
            var unlisten = $scope.$on(eventName, function () {
              if ($element[0].nodeName === 'SELECT') {
                $element[0].disabled = true;
              } else {
                $element[0].readOnly = true;
              }
            });
    
            $scope.$on('$destroy', unlisten);
          };
    
          // Return the original (augmented) directive.
          return $delegate;      
        });
      });
    });
    

    然后,我们可以简单地从控制器(或其他地方)触发行为:

    app.controller('ctrl', function ($scope, $timeout) {
    
      $timeout(function () {
        $scope.$broadcast('__disableInput', {});
      }, 5000);
    
    });
    

    现在,每个 inputselecttextarea 目前都居住在广播中 范围应应用其相应的回调,并且不能通过常规进行编辑 用户的意思了。

    也有可能从 $rootScope 触发这个 - 但是;使用内置 ng 指令时, 我会说添加我们自己的自定义标志来决定指令是否侦听 eventName 是否在我们的配置块 (__disableInput) 中定义。

    如果我们想添加这个自定义标志, 我们应该将新的directive.controller 函数更改为如下所示:

    if (origCtrl) {
      angular.extend(this, $controller(origCtrl, { 
        $scope: $scope,
        $element: $element,
        $attrs: $attrs
      }));  
    }
    
    // Only add our eventlistener if canBeDisabled is set.
    if ($attrs.canBeDisabled !== undefined) {
    
      var unlisten = $scope.$on(eventName, function (e, data) {
        if ($element[0].nodeName === 'SELECT') {
          $element[0].disabled = true;
        } else {
          $element[0].readOnly = true;
        }
      });
    
      $scope.$on('$destroy', unlisten);
    }
    

    那么在我们看来,我们会这样做:

    <input ng-model="someModel.property" can-be-disabled type="text">
    

    现在,这是一个粗略的示例,可能还有一些我尚未探索的陷阱。 但我确实认为,如果你想要一个简单的方式,这是朝着正确方向迈出的一步 扩展ng 指令而不必触及它们的内部和/或创建 新指令。

    我没有做的一件事是将eventName定义为app.value(或app.constant), 这样我们就不必在配置块中定义 eventName 或担心拼写错误 在设置此事件的广播者时。我们只需将值注入到我们的上下文中 并将其用作事件的名称。

    这是给你的 JSBin:http://jsbin.com/xipokuyi/5/edit

    还有一个更深入地探索这种方法的文章的链接: http://angular-tips.com/blog/2013/09/experiment-decorating-directives/

    【讨论】:

      【解决方案3】:

      tl;博士;

      辅助服务用作指令和控制器之间的通信机制。服务注册和取消注册元素(在指令链接函数中)并通过函数调用(在控制器中)启用和禁用所有注册的元素。

      解决方案 1 -- 任何位置的只读元素

      HERE 是我的解决方案的种子1,其中元素可能位于 DOM 树中的任何位置。

      辅助服务

      app.factory("demoReadonlyState", function(){
      
        var elements = [];
        var state = false;
      
        return {
          enable : function(){
            state = false;
            for (var i = 0, len = elements.length; i < len; i++){
              elements[i].removeAttribute('readonly');
            }
          },
          disable : function(){
            state = true;
            for (var i = 0, len = elements.length; i < len; i++){
              elements[i].setAttribute('readonly', "True");
            }
          },
          addElement : function(domEl) { 
            elements.push(domEl);
            if(state){
              domEl.setAttribute('readonly', state);
            }
            else {
              domEl.removeAttribute('readonly');
            }
            console.log("add -- # elements: ", elements.length);
          },
          removeElement : function(el) {
            var index = elements.indexOf(el);
            if (index > -1) {
              elements.splice(index, 1);
            }
            console.log("remove -- # elements: ", elements.length);
          }
        }
      })
      

      指令

      app.directive("demoReadonly", function(demoReadonlyState){
      
        return {
          link : function(scope, element){
            console.log(demoReadonlyState);
            demoReadonlyState.addElement(element[0]);
            element.on('$destroy', function(){
              demoReadonlyState.removeElement(element);
            })
          }
        }
      })
      

      用法

      JS:

      app.controller("MainCtrl", function($scope, demoReadonlyState){
        $scope.roState = demoReadonlyState;
        $scope.roState.enable();
        //$scope.roState.disable();
      });
      

      HTML:

      <body ng-controller="MainCtrl">
          <input demo-readonly />
          <input />
          <input demo-readonly />
          <input />
          <input demo-readonly />
          <input />
      
          <div>
            <button ng-click="roState.enable()">enable</button>
            <button ng-click="roState.disable()">disable</button>
          </div>
      
          <div>
            <button ng-click="arr.push([])">add element</button>
            <button ng-click="arr.length && arr.splice(0,1)">remove element</button>
          </div>
      
          <div ng-repeat="x in arr">
            <input demo-readonly/>
          </div>
      </body>
      

      1 我的意思是种子是不可配置的。在正常的解决方案中,指令可以传递一个字符串 (demo-readonly="type_X"),它指定一组元素应该是 enabled/disabled 在一起,而所有其他组保持不变。

      解决方案 2 -- DOM 子树中的只读元素

      HERE 是一种稍微不同的方法,它使用单个指令处理子树。它可以处理多个相互独立的子树。在这种形式下,它不适用于动态ng-repeat well2,但可以轻松实现更强大的机制(尽管需要使用额外的指令)。

      辅助服务

      app.factory("demoReadonlyState", function(){
      
        var elements = {};
      
        return {
          enable : function(type){
            element = elements[type];
            var inputs = element.querySelectorAll('input');
            for (var i = 0, len = inputs.length; i < len; i++){
              inputs[i].removeAttribute('readonly');
            }
          },
          disable : function(type){
            element = elements[type];
            var inputs = element.querySelectorAll('input');
            for (var i = 0, len = inputs.length; i < len; i++){
              inputs[i].setAttribute('readonly', "True");
            }
          },
          addElement : function(type, domEl) { 
            elements[type] = domEl;
          },
          removeElement : function(type) {
            delete elements[type];
          }
        }
      })
      

      指令

      app.directive("demoReadonly", function(demoReadonlyState){
      
        return {
          scope : {
            type : "@demoReadonly"
          },
          link : function(scope, element){
            demoReadonlyState.addElement(scope.type, element[0]);
            element.on('$destroy', function(){
              demoReadonlyState.removeElement(scope.type);
            })
          }
        }
      })
      

      用法

      JS:

      app.controller("MainCtrl", function($scope, demoReadonlyState){
        $scope.roState = demoReadonlyState;
        $scope.roState.enable('XXX');
        //$scope.roState.disable('XXX');
      });
      

      HTML:

        <body ng-controller="MainCtrl">
          <div>
            <button ng-click="roState.enable('XXX')">enable</button>
            <button ng-click="roState.disable('XXX')">disable</button>
          </div>
      
      
          <div demo-readonly="XXX">
            <input/>
            <input/>
            <div>
              <input/>
              <input/>
            </div>
          </div>
      
        </body>
      

      1 基本上使用动态ng-repeat 需要在每次更新集合后手动调用enable/disable

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-03-14
        • 1970-01-01
        • 2010-09-24
        • 1970-01-01
        • 1970-01-01
        • 2014-01-10
        • 1970-01-01
        相关资源
        最近更新 更多