angular dynamically adding contenteditable directive doesn't work? - angularjs

Runnable CODE: my code
I try to dynamically add contenteditable directive to the <div> element when it is double-clicked.
when I put contenteditable directive to <div> in the beginning, the ng-model still work, but when I remove it and add it dynamically in ng-dblclick callback, ng-model seams not work anymore.
It's kind of like this Question.
but I can't think of a angular-friendly way to finish my work here.
How can I fix this?
code: html
<div ng-app="customControl">
<form name="myForm" ng-controller="mainControl">
<!-- Dynamically adding contenteditable directive : doesn't work -->
<div name="myWidget" ng-model="userContent" ng-click="enableEdit($event)"
strip-br="true"
required>Change me!</div>
<hr>
<textarea ng-model="userContent"></textarea>
</form>
</div>
code: js
angular.module('customControl', []).
controller('mainControl', function($scope) {
$scope.enableEdit = function(e) {
$(e.target).attr('contenteditable', '');
}
})
.directive('contenteditable', function() {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if(!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html(ngModel.$viewValue || '');
};
// Listen for change events to enable binding
element.on('keyup change', function() {
scope.$apply(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser
// leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
});

Related

Angular - Add Attribute Based on Window Width

I found some answers to this already - tried them however none of them work for me.
I have the following div where I use the attribute equalizer:
<div class="text-center" equalizer='group'>
However I only want that attribute equalizer to exist if the window width is > 400.
So I also use this code in my controller:
$scope.windowWidth = $window.innerWidth;
$window.onresize = function(event) {
$timeout(function() {
$scope.windowWidth = $window.innerWidth;
});
};
Now I understand I can do something like:
equalizer="{{ windowWidth>400 ? 'group' : ''}}"
However the problem is whether or not I have a value in equalizer it is still applied - namely <div class="text-center" equalizer=''> works in the same as <div class="text-center" equalizer='group'>
So how do completely control whether that attribute is inserted or not?
TO ADD
The only solution i have is duplicating the code and the using ng-if
so :
<div ng-if="windowWidth<400" class="text-centre">
and
<div ng-if="windowWidth>=400" class="text-center" equalizer='group'>
Thanks.
I would create a resize directive that's updating a scope variable that you can check inside of your equalizer directive.
I'm not exactly sure what your equalizer directive is doing but something like in the demo below (or in this fiddle) should work.
In the demo I'm removing the equalizer directive for testing if the resize event is removed with the directive.
Adding a directive would look like the untested code below but I would do it like in the demo with ng-if:
var childScope = $scope.$new();
var directiveElement = angular.element('<equalizer></equalizer>');
$document.append($compile(directiveElement)(childScope));
(Here is a demo found with Google for adding directives dynamically.)
angular.module('demoApp', [])
.controller('mainController', function($scope) {
$scope.equalizerOptions = {
group: true
};
$scope.removeEqualizer = function() {
// just for testing if resize handler is removed
var equalizer = angular.element(document).find('equalizer');
//console.log(angular.element(document).find('equalizer').scope());
equalizer.scope().$destroy();
equalizer.remove();
}
})
.directive('equalizer', equalizerDir)
.directive('resize', resizeDir);
function resizeDir($window) {
return {
link: function(scope, element, attrs) {
scope.window = {};
function onResize(event) {
//console.log('Resized', scope, element, event);
scope.window.width = $window.innerWidth;
}
$window.onresize = function(evt) {
//console.log('Resize');
scope.$apply(onResize);
};
onResize(); //initial call
scope.$on('$destroy', function() {
//console.log('destroy dir');
$window.onresize = undefined;
});
}
};
}
function equalizerDir($timeout) {
return {
scope: {
options: '=',
width: '#'
},
template: '<div ng-if="width >= 400"><h2>equalizer with ng-if & resize dir</h2>' +
'<p>{{options.group ? \'grouped\': \'not grouped\'}}</p></div>',
link: function(scope, element, attrs) {
/*scope.$watchCollection('options', function() {
// would work but watch with ternary operator directly in markup is easier
scope.text = scope.options.group ? 'grouped': 'not grouped';
});*/
}
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.js"></script>
<div ng-app="demoApp" ng-controller="mainController">
window width debugging: {{window.width}}
<equalizer options="equalizerOptions" width="{{window.width}}" resize>
</equalizer>
<button ng-click="equalizerOptions.group = !equalizerOptions.group">
toggle group
</button>
<button ng-click="removeEqualizer()">
remove equalizer (test unbinding resize)
</button>
</div>

Contenteditable with ng-model doesn't work

I'm trying to store the value of a contenteditable to my JS code. But I can't find out why ng-model doesn't work in this case.
<div ng-app="Demo" ng-controller="main">
<input ng-model="inputValue"></input>
<div>{{inputValue}}</div> // Works fine with an input
<hr/>
<div contenteditable="true" ng-model="contentValue"></div>
<div>{{contentValue}}</div> // Doesn't work with a contenteditable
</div>
Is there a workaround to do that ?
See : JSFiddle
Note: I'm creating a Text editor, so the user should see the result, while I'm storing the HTML behind it. (ie. user see: "This is an example !", while I store: This is an <b>example</b> !)
contenteditable tag will not work directly with angular's ng-model because the way contenteditable rerender the dom element on every change.
You have to wrap it with a custom directive for that:
JS:
angular.module('customControl', ['ngSanitize']).
directive('contenteditable', ['$sce', function($sce) {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$evalAsync(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
}]);
HTML
<form name="myForm">
<div contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>Change me!</div>
<span ng-show="myForm.myWidget.$error.required">Required!</span>
<hr>
<textarea ng-model="userContent"></textarea>
</form>
Source it from the original docs
Just move the read function call into $render
angular.module('customControl', ['ngSanitize']).
directive('contenteditable', ['$sce', function($sce) {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
read(); // initialize
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$evalAsync(read);
});
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
}]);
Neither of the other answers worked for me. I needed the model's initial value to be rendered when the control was initialized. Instead of calling read(), I used this code inside the link function:
ngModel.$modelValue = scope.$eval(attrs.ngModel);
ngModel.$setViewValue(ngModel.$modelValue);
ngModel.$render()

Angular - How to "bind" when "oninput" with "contenteditable span"?

"<span contenteditable>{{ line.col2 }}</span>"
Hello,
This code is good at initialisation but if I edit the span, no bing is send and my array model never updated...
So, I have tried this :
<span contenteditable ng-model="line.col2" ng-blur="line.col2=element.text()"></span>
But "this.innerHTML" does not exist.
What can I do ?
Thank at all ;-)
you can remove the ng-blur and you will have to add this directive:
<span contenteditable ng-model="myModel"></span>
Here is the directive taken from the documentation:
.directive('contenteditable', function() {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if(!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html(ngModel.$viewValue || '');
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$apply(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
}
});
I only will point you to possible solution, then you need to parse/clean HTML better.
<span contenteditable data-ng-blur="bar = $event.target.innerHTML">
{{bar}}
</span>
// upd.
Angular events such as click, blur, focus, ... - fired with scope context, e.g. this will be current scope.
Use $event, be happy.
Solution with Mirrage and gab help :
<span contenteditable="true" ng-model="ligne.col2">{{ ligne.col2 }}</span>
app.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
// view -> model
element.bind('blur', function() {
scope.$apply(function() {
ctrl.$setViewValue(element.html());
});
});
// model -> view
ctrl.$render = function() {
element.html(ctrl.$viewValue);
};
// load init value from DOM
ctrl.$render();
}
};
});
Thank at all ;-)

Whats wrong with my AngularJS directive

To be able to watch the contents change for a contenteditable div or an input element, I have created the following directive:
app.directive('contenteditable',function() { return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
// view -> model
element.bind('input', function() {
scope.$apply(function() {
ctrl.$setViewValue(element["0"].tagName=="INPUT" ? element.val() : element.text());
scope.watchCallback(element.attr('data-ng-model'));
});
});
// model -> view
ctrl.$render = function() {
element.text(ctrl.$viewValue);
element.val(ctrl.$viewValue);
};
}};
});
My Test Controller looks like:
function TestController($scope) {
$scope.singleVal = "X";
$scope.multiVal = ["A"];
$scope.addRow = function() {
$scope.multiVal.push("");
};
$scope.watchCallback = function(modelName) {
console.log(modelName+" was changed");
};
}
When I test it against the following html, the singleVal (statically created) behaves well, but my multiVal (dynamically created using ng-repeat) doesnt. When I input a value, it just retains the original value (i.e the model is not getting refreshed). Please help.
<div data-ng-controller="TestController">
<div contenteditable="true" data-ng-model="singleVal"></div>
<button data-ng-click="addRow()">Add Row</button>
<table data-ng-repeat="val in multiVal"><tr><td>
<div contenteditable="true" data-ng-model="val"></div>
</td></tr></table>
</div>
You can't bind ngModel directly to a string in an array. You'll need to store an array of objects inside of multiVal:
$scope.multiVal = [{property: "A"}];
Demonstrated here: http://jsfiddle.net/YMJzN/
Btw, you'll also want to adjust $scope.addRow to do the same...
$scope.addRow = function() {
$scope.multiVal.push({property:'new'});
}

AngularJS and contentEditable two way binding doesn't work as expected

Why in the following example the initial rendered value is {{ person.name }} rather than David? How would you fix this?
Live example here
HTML:
<body ng-controller="MyCtrl">
<div contenteditable="true" ng-model="person.name">{{ person.name }}</div>
<pre ng-bind="person.name"></pre>
</body>
JS:
app.controller('MyCtrl', function($scope) {
$scope.person = {name: 'David'};
});
app.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
// view -> model
element.bind('blur', function() {
scope.$apply(function() {
ctrl.$setViewValue(element.html());
});
});
// model -> view
ctrl.$render = function() {
element.html(ctrl.$viewValue);
};
// load init value from DOM
ctrl.$setViewValue(element.html());
}
};
});
The problem is that you are updating the view value when the interpolation is not finished yet.
So removing
// load init value from DOM
ctrl.$setViewValue(element.html());
or replacing it with
ctrl.$render();
will resolve the issue.
Short answer
You're initializing the model from the DOM using this line:
ctrl.$setViewValue(element.html());
You obviously don't need to initialize it from the DOM, since you're setting the value in the controller. Just remove this initialization line.
Long answer (and probably to the different question)
This is actually a known issue: https://github.com/angular/angular.js/issues/528
See an official docs example here
Html:
<!doctype html>
<html ng-app="customControl">
<head>
<script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<form name="myForm">
<div contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>Change me!</div>
<span ng-show="myForm.myWidget.$error.required">Required!</span>
<hr>
<textarea ng-model="userContent"></textarea>
</form>
</body>
</html>
JavaScript:
angular.module('customControl', []).
directive('contenteditable', function() {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if(!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html(ngModel.$viewValue || '');
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$apply(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
});
Plunkr
Here is my understanding of Custom directives.
The code below is basic overview of two way binding.
you can see it working here as well.
http://plnkr.co/edit/8dhZw5W1JyPFUiY7sXjo
<!doctype html>
<html ng-app="customCtrl">
<head>
<script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script>
angular.module("customCtrl", []) //[] for setter
.directive("contenteditable", function () {
return {
restrict: "A", //A for Attribute, E for Element, C for Class & M for comment
require: "ngModel", //requiring ngModel to bind 2 ways.
link: linkFunc
}
//----------------------------------------------------------------------//
function linkFunc(scope, element, attributes,ngModelController) {
// From Html to View Model
// Attaching an event handler to trigger the View Model Update.
// Using scope.$apply to update View Model with a function as an
// argument that takes Value from the Html Page and update it on View Model
element.on("keyup blur change", function () {
scope.$apply(updateViewModel)
})
function updateViewModel() {
var htmlValue = element.text()
ngModelController.$setViewValue(htmlValue)
}
// from View Model to Html
// render method of Model Controller takes a function defining how
// to update the Html. Function gets the current value in the View Model
// with $viewValue property of Model Controller and I used element text method
// to update the Html just as we do in normal jQuery.
ngModelController.$render = updateHtml
function updateHtml() {
var viewModelValue = ngModelController.$viewValue
// if viewModelValue is change internally, and if it is
// undefined, it won't update the html. That's why "" is used.
viewModelValue = viewModelValue ? viewModelValue : ""
element.text(viewModelValue)
}
// General Notes:- ngModelController is a connection between backend View Model and the
// front end Html. So we can use $viewValue and $setViewValue property to view backend
// value and set backend value. For taking and setting Frontend Html Value, Element would suffice.
}
})
</script>
</head>
<body>
<form name="myForm">
<label>Enter some text!!</label>
<div contenteditable
name="myWidget" ng-model="userContent"
style="border: 1px solid lightgrey"></div>
<hr>
<textarea placeholder="Enter some text!!" ng-model="userContent"></textarea>
</form>
</body>
</html>
Hope, it helps someone out there.!!
Check this angularjs directive
https://github.com/clofus/angular-inputnlabel
http://clofus.com/viewarticles/109
You may run into issues using #Vanaun's code if a scope.$apply is already in progress. In this case I use $timeout instead which resolves the issue:
Html:
<!doctype html>
<html ng-app="customControl">
<head>
<script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<form name="myForm">
<div contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>Change me!</div>
<span ng-show="myForm.myWidget.$error.required">Required!</span>
<hr>
<textarea ng-model="userContent"></textarea>
</form>
</body>
</html>
JavaScript:
angular.module('customControl', []).
directive('contenteditable', function($timeout) {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if(!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html(ngModel.$viewValue || '');
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
$timeout(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
});
Working Example: Plunkr

Resources