1

我想要一个像这样的集合:

var array = [{innerText: "I was with ", index:0},{obj: "Mary", id: 1, index:1}, {innerText: " and ", index:2},{obj: "John", id: 2, index:3}]; 

和一个内容可编辑的 div,它会将所有内容都放在上面,但绑定到数组,这样当我更改 innerText 或表示对象的输入时,数组将相应地更新。

例如,没有 angularJS 的 div 看起来像这样:

<div contenteditable="true">
I was with <input type="text" value="Mary" data-index="1"/> and <input type="text" value="John" data-index="3"/>
</div>  

这应该与 div 中的退格键以及要插入的新输入或要键入的文本一起使用,从而相应地更新数组。

我知道可能我必须使用 Mutation Observers,但我不知道如何在这个复杂的示例中使用。我希望 AngularJs 能够与突变观察者进行更自动化的集成:/

我的原始方法是:我为整个集合制作了一个指令,为 innerText 制作了一个指令,为对象制作了一个指令。输入与对象名称的绑定当然有效,但在 contenteditable 的内部 DOM 发生突变时无效。同样将 {{innerText}} 作为 innerText 的模板并在 contenteditable 中使用它并不能保证有人会实际输入它,因此绑定将起作用(而不是在它之前或之后)

编辑:如果它使具有相同内容的集合变得更容易,那么可编辑仍然非常有用

var array = [{obj: "Mary", id: 1, index:1}, {obj: "John", id: 2, index:3}, {innerText: "I was with @ and @"]; 

Edit2:重新打开问题。以前接受的答案方法非常好,但今天我意识到它不是真正的双向绑定。它实际上是单向绑定。从视图到模型。如果使用提供的代码的更新版本(来自先前接受的答案)来获得类似的模型,则将奖励赏金

modelValue": [
    {
      "innerText": "abc",
      "index": 0
    },
    {
      "obj": "abc",
      "index": 1
    },
    {
      "innerText": "abc",
      "index": 2
    }
  ]

这将使视图:

"viewValue": "\n abc\n <input type=\"text\">\n abc\n "

该解决方案必须为服务提供代码,该服务将在按下新按钮时返回一个像上面那样的静态模型,并且控制器中的一个函数将把 modelValue 放在作用域中,并且模型将转换为上面的 viewValue .

Edit3:根据下面的更新答案,这里是真正的 2-way binding 如何在没有建议的 $watch 的情况下使用 compile pre-link 和 post-link 工作:

// Code goes here

var myApp = angular.module('myApp', []);
myApp.controller('test', ['$scope',
  function($scope) {
    $scope.addInput = function() {
      //Put in a directive if using for real
      var input = document.createElement('input');
      input.type = "text";
      $(input).attr("data-label","obj");
      $(input).attr("data-name","");
      $(input).attr("data-id","randomId");

      document.querySelector("div[contenteditable]").appendChild(input);
      input.focus();
    }

  }
]);

myApp.directive('contenteditable', ['$compile', function($compile) {
  return {
        require: 'ngModel',
        controller: [
            '$scope',
            function($scope) {

                 // Load initial value.

                $scope.getViewValue = function() {
                    var tempDiv = document.createElement("div");
                    angular.forEach($scope.model.modelValue, 
                        function(obj, index) {
                            if (obj.innerText) {
                                var newTextNode = document.createTextNode(" "+obj.innerText+" ");
                                tempDiv.appendChild(newTextNode);
                            } else if (obj.name) {
                                var newInput = document.createElement('input');
                                newInput.setAttribute('data-id',obj.id);
                                newInput.setAttribute('data-label', obj.label);
                                newInput.setAttribute('autosize', 'autosize');
                                newInput.setAttribute('data-name', obj.name);
                                newInput.setAttribute('value', obj.nickname);
                                newInput.setAttribute('type','text');
                                $(newInput).addClass('element-'+obj.label);
                                tempDiv.appendChild(newInput);
                            }
                        }
                    );
                    return tempDiv.innerHTML;
                };

                $scope.model = { "viewValue": "", "modelValue": [{"nickname":"Abc","index":0,"id":"2","label":"obj","name":"Abc"},{"innerText":"does something with","index":1},{"nickname":"bcd","index":3,"id":"0","label":"obj","name":"bcd"}] };

                $scope.model.viewValue = $scope.getViewValue();

        }],

        compile: function(elm, attrs){
 
             return {
                 pre: function(scope, elm, attrs, ctrl, transcludeFn){
                     
                    elm.html(scope.model.viewValue);
                    ctrl.$setViewValue(elm.html());

                    console.log(elm);
                    angular.forEach(elm[0].childNodes, function (node, index) {
                        if (node.nodeName === "INPUT") {
                           
                                $compile(node)(scope);
                            
                            

                        }
                    });
                    


                    //click all of them to make them autosize
                    $('div.editable input').click();

                 },
                 post: function(scope, elm, attrs, ctrl) {
                   

                    //prevent enter from being pressed
                    elm.bind('keydown',function(evt){
                        if (evt.keyCode == 13) {
                            evt.preventDefault();
                            return false;
                        }
                    });




                    //click all of them to make them autosize
                    $('div.editable input').click();


                    //Change listeners
                    elm.bind('blur keyup paste input click', function() {

                            var new$viewValue = {
                                viewValue: elm.html(),
                                modelValue: []
                            }
                            var index = 0;
                            angular.forEach(elm[0].childNodes, function(value, index) {
                                if (value.nodeName === "INPUT") {
                                    if (value.value) {

                                        var obj = {
                                            nickname: value.value,
                                            index: index,
                                            id: $(value).attr("data-id"),
                                            label: $(value).attr("data-label"),
                                            name: $(value).attr("data-name")
                                        };


                                        new$viewValue.modelValue.push(obj);

                                        //if type is entity


                                    } else {
                                        value.parentNode.removeChild(value);
                                    }
                                } else if (value.nodeName === "#text") {

                                    var last = null;
                                    if(new$viewValue.modelValue.length > 0){
                                        var last = new$viewValue.modelValue[new$viewValue.modelValue.length-1];
                                    }


                                    //if last was innerText (update it)
                                    if (last!=null && last.innerText){
                                        last.innerText += value.textContent.trim()
                                    }


                                    //else push it
                                    else {
                                        new$viewValue.modelValue.push({
                                            innerText: value.textContent.trim(),
                                            index: index
                                        });
                                    }
                                }
                                index++;
                            });
                            ctrl.$setViewValue(new$viewValue);
console.log(JSON.stringify(scope.model.modelValue));
                         
                    });

                }
             }
         },

    };
}]);
div > div > div {
  background-color: grey;
  min-width: 100px;
  min-height: 10px;
}
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
  <div ng-controller="test">
    <button ng-click="addInput()">Add Input</button>
    <div contenteditable="true" ng-model="model">

    </div>
    See Console</div>
</div>

4

1 回答 1

4

这是一种使用自定义指令的方法。我无法完全匹配您的数据模型,但这应该能够满足您的需求。

这是对象模型的样子:

{
  "viewValue": "\n abc\n <input type=\"text\">\n abc\n ",
  "modelValue": [
    {
      "innerText": "abc",
      "index": 0
    },
    {
      "obj": "abc",
      "index": 1
    },
    {
      "innerText": "abc",
      "index": 2
    }
  ]
}

viewValue是构成 的 html,contenteditable您所描述的内容在modelValue.

这里我们设置了一堆事件监听器(受这个问题的启发)并构建模型。

elm.bind('blur keyup paste input', function() {
    scope.$apply(function() {
        var new$viewValue = {
            viewValue: elm.html(),
            modelValue: []
        }
        var index = 0;
        angular.forEach(elm[0].childNodes, function(value, index) {
            if (value.nodeName === "INPUT") {
                if (value.value) {
                    new$viewValue.modelValue.push({
                        obj: value.value,
                        index: index
                    });
                } else {
                    value.parentNode.removeChild(value);
                }
            } else if (value.nodeName === "#text") {
                new$viewValue.modelValue.push({
                    innerText: value.textContent.trim(),
                    index: index
                });
            }
            index++;
        });
        ctrl.$setViewValue(new$viewValue);
    });
});

这样做是获取所有的childNodescontenteditable div检查它们是否属于输入或文本类型,并将适当的值添加到模型中。我们还存储 的 html 状态div以允许我们重绘视图。

调用 render 函数来绘制视图,我们将视图的 html 设置为我们存储在模型中的 html。

ctrl.$render = function() {
    elm.html(ctrl.$viewValue.viewValue);
    //Untested code that should add the text back into the fields if the model already exists
    angular.forEach(elm[0].childNodes, function (value, index) {
        if (value.nodeName === "INPUT") {
            if (ctrl.$viewValue.modelValue[index].obj) {
                 value.value = ctrl.$viewValue.modelValue[index].obj
            }
            else {
                 value.parentNode.removeChild(value);
            }
        }
    });
};

编辑:这是一种有两种方式数据绑定的方法:

scope.getViewValue = function() {
    var tempDiv = document.createElement("div");
    angular.forEach(ctrl.$viewValue.modelValue, function(value, index) {
      if (value.innerText) {
        var newTextNode = document.createTextNode(value.innerText);
        tempDiv.appendChild(newTextNode);
      } else if (value.obj) {
        var newInput = document.createElement('input');
        newInput.type = "text";
        newInput.value = value.obj;
        tempDiv.appendChild(newInput);
      }
    });
    return tempDiv.innerHTML;
};


scope.$watch(function() { return ctrl.$modelValue; }, function(newVal, oldVal) {
    var newViewValue = scope.getViewValue();
    ctrl.$setViewValue({
      "viewValue": newViewValue,
      "modelValue": ctrl.$viewValue.modelValue
    });
   ctrl.$render();
}, true);

这样做是在被引用的对象上设置一个观察者,ng-model并且每当它发生变化时,它都会重新计算innerHTML视图的。它有一个错误,当重绘字段时焦点会丢失。存储具有焦点的元素并在重绘时恢复它应该可以解决这个问题。

对于其余的代码,并查看它的实际效果,请查看下面的代码片段。我添加了一个按钮,用于添加额外的文本字段,以表明它支持添加更多输入。

// Code goes here

var myApp = angular.module('myApp', []);
myApp.controller('test', ['$scope',
  function($scope) {
    $scope.addInput = function() {
      //Put in a directive if using for real
      var input = document.createElement('input');
      input.type = "text";
      document.querySelector("div[contenteditable]").appendChild(input);
    }

    $scope.test = {
      "viewValue": "",
      "modelValue": [{
        "innerText": "abc",
        "index": 0
      }, {
        "obj": "abc",
        "index": 1
      }, {
        "innerText": "abc",
        "index": 2
      }]
    };
  }
]);

myApp.directive('contenteditable', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {

      //Change listeners
      elm.bind('blur keyup paste input', function() {
        scope.$apply(function() {
          var new$viewValue = {
            viewValue: elm.html(),
            modelValue: []
          };
          var index = 0;
          angular.forEach(elm[0].childNodes, function(value, index) {
            if (value.nodeName === "INPUT") {
              if (value.value) {
                new$viewValue.modelValue.push({
                  obj: value.value,
                  index: index
                });
              } else {
                value.parentNode.removeChild(value);
              }
            } else if (value.nodeName === "#text") {
              new$viewValue.modelValue.push({
                innerText: value.textContent.trim(),
                index: index
              });
            }
            index++;
          });
          ctrl.$setViewValue(new$viewValue);
        });
      });

      // Draw the field
      ctrl.$render = function() {
        elm.html(ctrl.$viewValue.viewValue);
        //Untested code that should add the text back into the fields if the model already exists
        angular.forEach(elm[0].childNodes, function(value, index) {
          if (value.nodeName === "INPUT") {
            if (ctrl.$viewValue.modelValue[index].obj) {
              value.value = ctrl.$viewValue.modelValue[index].obj;
            } else {
              value.parentNode.removeChild(value);
            }
          }
        });
      };

      // Load initial value.

      scope.getViewValue = function() {
        var tempDiv = document.createElement("div");
        angular.forEach(ctrl.$viewValue.modelValue, function(value, index) {
          if (value.innerText) {
            var newTextNode = document.createTextNode(value.innerText);
            tempDiv.appendChild(newTextNode);
          } else if (value.obj) {
            var newInput = document.createElement('input');
            newInput.type = "text";
            newInput.value = value.obj;
            tempDiv.appendChild(newInput);
          }
        });
        return tempDiv.innerHTML;
      };


      scope.$watch(function() { return ctrl.$modelValue; }, function(newVal, oldVal) {
        var newViewValue = scope.getViewValue();
        ctrl.$setViewValue({
          "viewValue": newViewValue,
          "modelValue": ctrl.$viewValue.modelValue
        });
       ctrl.$render();
      }, true);
    }
  };
});
div > div > div {
  background-color: grey;
  min-width: 100px;
  min-height: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
  <div ng-controller="test">
    <button ng-click="addInput()">Add Input</button>
    <div contenteditable="true" ng-model="test">

    </div>
    {{test}}</div>
</div>

于 2014-10-17T15:08:04.090 回答