I want to have a collection like:
var array = [{innerText: "I was with ", index:0},{obj: "Mary", id: 1, index:1}, {innerText: " and ", index:2},{obj: "John", id: 2, index:3}];
and a content editable div that will have all those up there but bound to the the array so that when I change either the innerText or the inputs that represent the objects the array will be updated accordingly.
For example the div would look something like that without the angularJS stuff:
<div contenteditable="true">
I was with <input type="text" value="Mary" data-index="1"/> and <input type="text" value="John" data-index="3"/>
</div>
This should work with backspace in the div and also new inputs to be inserted or text to be typed, updating the array accordingly.
I know that possibly I have to use Mutation Observers but I don't know how in this complicated example. I hoped for AngularJs to have some more automated integration with mutation observers :/
My primitive approach was: I made a directive for the whole collection, a directive for the innerText and a directive for the objects. The binding of the inputs with the object names works of course but not when the internal DOM of contenteditable is mutated. Also having {{innerText}} as a template for innerText and using it in contenteditable wasn't guaranteethat someone will actually type in it so the binding will work (and not before or after it)
Edit: If it makes it easier a collection like that with the same contenteditable is still very useful
var array = [{obj: "Mary", id: 1, index:1}, {obj: "John", id: 2, index:3}, {innerText: "I was with @ and @"];
Edit2: Reopened the question. The previously accepted answer approach was really good but today I realized it is not real 2-way binding. It is actually 1-way binding. Going from the view to the model. The bounty will be awarded if an updated version of the provided code (from the previously accepted answer) is used to get a model like
modelValue": [
{
"innerText": "abc",
"index": 0
},
{
"obj": "abc",
"index": 1
},
{
"innerText": "abc",
"index": 2
}
]
and this will make the view:
"viewValue": "\n abc\n <input type=\"text\">\n abc\n "
The solution will have to provide code for a service that will return a static model like the one above when a new button is pressed and a function in the controller that will put the modelValue in the scope and the model will be converted to the above viewValue.
Edit3: Based on the updated answer below here is how real 2-way binding works without suggested $watch by using compile pre-link and 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>