angularjs textarea with colors (with html5 contenteditable) - angularjs

I'm trying to create an editor which does "syntax highlighting",
it is rather simple:
yellow -> <span style="color:yellow">yellow</span>
I'm also using <code contenteditable> html5 tag to replace <textarea>, and have color output.
I started from angularjs documentation, and created the following simple directive. It does work, except it do not update the contenteditable area with the generated html.
If I use a element.html(htmlTrusted) instead of ngModel.$setViewValue(htmlTrusted), everything works, except the cursor jumps to the beginning at each keypress.
directive:
app.directive("contenteditable", function($sce) {
return {
restrict: "A", // only activate on element attribute
require: "?ngModel", // get ng-model, if not provided in html, then null
link: function(scope, element, attrs, ngModel) {
if (!ngModel) {return;} // do nothing if no ng-model
element.on('blur keyup change', function() {
console.log('app.directive->contenteditable->link->element.on()');
//runs at each event inside <div contenteditable>
scope.$evalAsync(read);
});
function read() {
console.log('app.directive->contenteditable->link->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 = '';
}
html = html.replace(/</, '<');
html = html.replace(/>/, '>');
html = html.replace(/<span\ style=\"color:\w+\">(.*?)<\/span>/g, "$1");
html = html.replace('yellow', '<span style="color:yellow">yellow</span>');
html = html.replace('green', '<span style="color:green">green</span>');
html = html.replace('purple', '<span style="color:purple">purple</span>');
html = html.replace('blue', '<span style="color:yellow">blue</span>');
console.log('read()-> html:', html);
var htmlTrusted = $sce.trustAsHtml(html);
ngModel.$setViewValue(htmlTrusted);
}
read(); // INITIALIZATION, run read() when initializing
}
};
});
html:
<body ng-app="MyApp">
<code contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>This <span style="color:purple">text is purple.</span> Change me!</code>
<hr>
<pre>{{userContent}}</pre>
</body>
plunkr: demo (type yellow, green or blue into the change me input area)
I tried scope.$apply(), ngModel.$render() but has no effect. I must miss something really obvious...
The links I already read through:
others' plunker demo 1
others' plunker demo 2
angularjs documentation's example
$sce.trustAsHtml stackoverflow question
setViewValue stackoverflow question
setViewValue not updating stackoverflow question
Any help is much appreciated. Please see the plunker demo above.

After almost a year, I finally settled to Codemirror, and I was never happier.
I'm doing side-by-side markdown source editing with live update (with syntax highlighting, so even a bit more advanced than stackoverflow's editing page.)
I created a simple codeEditor angular directive, which requires codeMirror, and uses it.
For completeness, here is the component sourcecode:
$ cat components/codeEditor/code-editor.html
<div class="code-editor"></div>
$ cat codeEditor.js
'use strict';
angular.module('myApp')
.directive('codeEditor', function($timeout, TextUtils){
return {
restrict: 'E',
replace: true,
require: '?ngModel',
transclude: true,
scope: {
syntax: '#',
theme: '#'
},
templateUrl: 'components/codeEditor/code-editor.html',
link: function(scope, element, attrs, ngModelCtrl, transclude){
// Initialize Codemirror
var option = {
mode: scope.syntax || 'xml',
theme: scope.theme || 'default',
lineNumbers: true
};
if (option.mode === 'xml') {
option.htmlMode = true;
}
scope.$on('toedit', function () { //event
//This is required to correctly refresh the codemirror view.
// otherwise the view stuck with 'Both <code...empty.' initial text.
$timeout(function() {
editor.refresh();
});
});
// Require CodeMirror
if (angular.isUndefined(window.CodeMirror)) {
throw new Error('codeEditor.js needs CodeMirror to work... (o rly?)');
}
var editor = window.CodeMirror(element[0], option);
// Handle setting the editor when the model changes if ngModel exists
if(ngModelCtrl) {
// Timeout is required here to give ngModel a chance to setup. This prevents
// a value of undefined getting passed as the view is rendered for the first
// time, which causes CodeMirror to throw an error.
$timeout(function(){
ngModelCtrl.$render = function() {
if (!!ngModelCtrl.$viewValue) {
// overwrite <code-editor>SOMETHING</code-editor>
// if the $scope.content.code (ngModelCtrl.$viewValue) is not empty.
editor.setValue(ngModelCtrl.$viewValue); //THIRD happening
}
};
ngModelCtrl.$render();
});
}
transclude(scope, function(clonedEl){
var initialText = clonedEl.text();
if (!!initialText) {
initialText = TextUtils.normalizeWhitespace(initialText);
} else {
initialText = 'Both <code-editor> tag and $scope.content.code is empty.';
}
editor.setValue(initialText); // FIRST happening
// Handle setting the model if ngModel exists
if(ngModelCtrl){
// Wrap these initial setting calls in a $timeout to give angular a chance
// to setup the view and set any initial model values that may be set in the view
$timeout(function(){
// Populate the initial ng-model if it exists and is empty.
// Prioritize the value in ngModel.
if(initialText && !ngModelCtrl.$viewValue){
ngModelCtrl.$setViewValue(initialText); //SECOND happening
}
// Whenever the editor emits any change events, update the value
// of the model.
editor.on('change', function(){
ngModelCtrl.$setViewValue(editor.getValue());
});
});
}
});
// Clean up the CodeMirror change event whenever the directive is destroyed
scope.$on('$destroy', function(){
editor.off('change');
});
}
};
});
There is also inside the components/codeEditor/vendor directory the full codemirror sourcecode.
I can highly recommend codeMirror. It is a rocksolid component, works in
every browser combination (firefox, firefox for android, chromium).

Related

Get updated content from pre-tag in AngularJS

I have a pre-tag which is binded to FileReader-Result to parse text. The tag is editable and I would like to get the updated content after a click on a button. Unfortunately I always get the origin text content.
HTML
<pre contenteditable="true" id="rangy" class="document-content textareac"
ng-bind="vm.document.content" ng-hide="document.mode=='edit'"
contenteditable>
</pre>
JS
var txt = vm.document.content;
I've tried to get it by a query select, but It do not work. It gives me an HTML-object.
t = angular.element(document.querySelector('#rangy'));
alert(t);
// alert(JSON.stringify(t);
The AngularJS documentation has an example of a contenteditable directive that enables the ng-model directive for the element.
app.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
<div contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>Change me!</div>
For more information, see
AngularJS ngModelController API Reference - Custom Control Example

Changing HTML of an element dynamically through a directive

I'm trying to change the HTML of an element based on a variable that is passed as an attribute of a directive.
The content is supposed to be changing back to the 'This is the original content...'. How come it doesn't work?
HTML
<div ng-app="myApp" ng-controller="myCtrl">
<div data-loading-data='{{testObj}}'>
<p> This is the original content. changingVar is '{{testObj.changingVar}}'</p>
</div>
</div>
JS
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, $timeout) {
$scope.testObj = {};
$scope.testObj.changingVar = false;
$timeout(function() {
console.log("time out is done ! the original content should be restored now (from somewhere inside the directive!)");
$scope.testObj.changingVar = true;
}, 5000);
});
app.directive('loadingData', function() {
return {
restrict: 'AE',
replace: 'false',
link: function(scope, elem, attrs) {
var originalHTML = elem[0].innerHTML;
elem.replaceWith("<p>This is modified content</p>");
// When testObj.changingVar will be true, I want the original content (which is stored in var 'originalHTML') to be set!
// How to do?
// ........................................... ?
// ........................................... ?
}
}
});
First answer was useful; sorry, I accidently saved the jsfiddle with some commented out parts. Updated now!
There was an answer which suggested using objects is useful (passed by reference) instead of passing a single variable (passed by value) - that was great to know.
I updated the jsFiddle again to illustrate what I am trying to do better:
https://jsfiddle.net/4xbLrg5e/6/
First things you are passing testObj means you don't want to use inherited scope with directive.
So,I am assuming you want to use isolated scope.
As per this,
Here is the changes,
HTML :
<div test-data='testObj'>
<p> This is the original content. changingVar is '{{testObj.changingVar}}'</p>
</div>
Directive JS :
There are some correction,
1. replace : false; // implicitly it is false and not 'false';
2. You should not replace directive with html until unless you dont want to refer it again.
You can put watch to properties of passed object if you want update the html as per data changes.
You should use isolated scope with directive as per above assumption.
This is nothing but passed by reference using =
app.directive('loadingData', function() {
return {
restrict: 'AE',
replace: false,
scope : {testData:'=testData'},
link: function(scope, elem, attrs) {
var originalHTML = elem[0].innerHTML;
elem.html("<p>This is modified content</p>");
scope.$watch(function(){
return scope.testData.changingVar;
},function(nVal){
console.log('1')
elem.html("<p>Here is the original content</p>");
})
}
}
});
Here is the updated fiddle

JQuery UI Spinner is not updating ng-model in angular

Angular's ng-model is not updating when using jquery-ui spinner.
Here is the jsfiddle http://jsfiddle.net/gCzg7/1/
<div ng-app>
<div ng-controller="SpinnerCtrl">
<input type="text" id="spinner" ng-model="spinner"/><br/>
Value: {{spinner}}
</div>
</div>
<script>
$('#spinner').spinner({});
</script>
If you update the text box by typing it works fine (you can see the text change). But if you use the up or down arrows the model does not change.
Late answer, but... there's a very simple and clean "Angular way" to make sure that the spinner's spin events handle the update against ngModel without resorting to $apply (and especially without resorting to $parse or an emulation thereof).
All you need to do is define a very small directive with two traits:
The directive is placed as an attribute on the input element you want to turn into a spinner; and
The directive configures the spinner such that the spin event listener calls the ngModel controller's $setViewValue method with the spin event value.
Here's the directive in all its clear, tiny glory:
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
Note that $setViewValue is intended for exactly this situation:
This method should be called when an input directive wants to change
the view value; typically, this is done from within a DOM event
handler.
Here's a link to a working demo.
If the demo link provided above dies for some reason, here's the full example script:
(function () {
'use strict';
angular.module('ExampleApp', [])
.controller('ExampleController', ExampleController)
.directive('jqSpinner', jqSpinner);
function ExampleController() {
var c = this;
c.exampleValue = 123;
};
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
})();
And the minimal example template:
<div ng-app="ExampleApp" ng-controller="ExampleController as c">
<input jq-spinner ng-model="c.exampleValue" />
<p>{{c.exampleValue}}</p>
</div>
Your fiddle is showing something else.
Besides this: Angular can not know about any changes that occur from outside its scope without being aknowledged.
If you change a variable of the angular-scope from OUTSIDE angular, you need to call the apply()-Method to make Angular recognize those changes. Despite that implementing a spinner can be easily achieved with angular itself, in your case you must:
1. Move the spinner inside the SpinnerCtrl
2. Add the following to the SpinnerCtrl:
$('#spinner').spinner({
change: function( event, ui ) {
$scope.apply();
}
}
If you really need or want the jQuery-Plugin, then its probably best to not even have it in the controller itself, but put it inside a directive, since all DOM-Manipulation is ment to happen within directives in angular. But this is something that the AngularJS-Tutorials will also tell you.
Charminbear is right about needing $scope.$apply(). Their were several problems with this approach however. The 'change' event only fires when the spinner's focus is removed. So you have to click the spinner then click somewhere else. The 'spin' event is fired on each click. In addition, the model needs to be updated before $scope.$apply() is called.
Here is a working jsfiddle http://jsfiddle.net/3PVdE/
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
var mdlAttr = $(this).attr('ng-model').split(".");
if (mdlAttr.length > 1) {
var objAttr = mdlAttr[mdlAttr.length - 1];
var s = $scope[mdlAttr[0]];
for (var i = 0; i < mdlAttr.length - 2; i++) {
s = s[mdlAttr[i]];
}
s[objAttr] = ui.value;
} else {
$scope[mdlAttr[0]] = ui.value;
}
$scope.$apply();
}
}, 0);
});
Here's a similar question and approach https://stackoverflow.com/a/12167566/584761
as #Charminbear said angular is not aware of the change.
However the problem is not angular is not aware of a change to the model rather that it is not aware to the change of the input.
here is a directive that fixes that:
directives.directive('numeric', function() {
return function(scope, element, attrs) {
$(element).spinner({
change: function(event, ui) {
$(element).change();
}
});
};
});
by running $(element).change() you inform angular that the input has changed and then angular updates the model and rebinds.
note change runs on blur of the input this might not be what you want.
I know I'm late to the party, but I do it by updating the model with the ui.value in the spin event. Here's the updated fiddle.
function SpinnerCtrl($scope, $timeout) {
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
$scope.spinner = ui.value;
$scope.$apply();
}
}, 0);
});
}
If this method is "wrong", any suggestions would be appreciated.
Here is a solution that updates the model like coder’s solution, but it uses $parse instead of parsing the ng-model parameter itself.
app.directive('spinner', function($parse) {
return function(scope, element, attrs) {
$(element).spinner({
spin: function(event, ui) {
setTimeout(function() {
scope.$apply(function() {
scope._spinnerVal = = element.val();
$parse(attrs.ngModel + "=_spinnerVal")(scope);
delete scope._spinnerVal;
});
}, 0);
}
});
};
});

Compiling Element Causes Input Caret Position to Move to End

I am having a problem with a directive. The purpose of the directive is to easily add validation without having to manually add ng-class (among other things) to elements in order to get the error state to show up. I just simply want to put a "validation" directive on my element and have the appropriate classes (and error messages) be generated when there is an error state.
As far as the validation goes, it is working great, but it causes an odd side effect. Whenever I am editing a value in an input box that has the validation directive on it, it moves the caret to the end of the text in the input field. It appears to be the fact that I'm compiling the element (in this case the parent element which contains this element).
Here is a jsbin showing the problem. To reproduce, type a value in the field, then put the caret in the middle of the value you just typed and try typing another character. Notice it moves you to the end of the field. Notice that if you delete the value, the field label turns red as expected to show a validation error (the field is required).
Here is the directive (from the jsbin):
angular.module('app', [])
.directive('validation', function($compile) {
return {
require: 'ngModel',
restrict: 'A',
compile: function(compileElement, attrs) {
var formName = compileElement[0].form.name;
compileElement.removeAttr('validation');
compileElement.parent().attr('ng-class', formName + "['" + attrs.name + "'].$invalid && " + formName + "['" + attrs.name + "'].$dirty ? 'error' : ''");
return function(scope, element) {
$compile(element.parent())(scope);
}
}
};
});
And here is the html:
<html>
<head>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.1/angular.min.js"></script>
</head>
<body ng-app="app">
<form name="subscribeForm">
<label>
First Name
<input type="text"
id="firstName"
name="firstName"
ng-model="userInfo.FirstName"
required
validation/>
</label>
</form>
</body>
</html>
not sure if you've figured this out but I encountered a similar problem. found a solution at Preserving cursor position with angularjs. for convenience, below is the directive snippet that would solve this issue.
app.directive('cleanInput', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModelController) {
var el = element[0];
function clean(x) {
return x && x.toUpperCase().replace(/[^A-Z\d]/g, '');
}
ngModelController.$parsers.push(function(val) {
var cleaned = clean(val);
// Avoid infinite loop of $setViewValue <-> $parsers
if (cleaned === val) return val;
var start = el.selectionStart;
var end = el.selectionEnd + cleaned.length - val.length;
// element.val(cleaned) does not behave with
// repeated invalid elements
ngModelController.$setViewValue(cleaned);
ngModelController.$render();
el.setSelectionRange(start, end);
return cleaned;
});
}
}
});
the directive had a different purpose though so modify it according to your requirements.
If you're not using the built in validation model/process you're doing it wrong. Check out the tutorial on the angular-js website:
http://code.angularjs.org/1.2.13/docs/guide/forms
Also, you shouldn't be doing element manipulation in the compile stage.
Update
You need to have a look at the section called Custom Validation.
Use the ctrl.$parsers approach. You add your parser on to the list of parsers and your fn will run any time the model changes. You then use the ctrl.$setValidity('strNameOfValidation', true) to set the validity. Angular will then add a class for you - called .ng-valid-float or .ng-invalid-float.
var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/;
app.directive('smartFloat', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
if (FLOAT_REGEXP.test(viewValue)) {
ctrl.$setValidity('float', true);
return parseFloat(viewValue.replace(',', '.'));
} else {
ctrl.$setValidity('float', false);
return undefined;
}
});
}
};
});

JSFiddle + TinyMCE + AngularJS

I'm experiencing an issue trying to use tinymce API inside an angular directive in JSFiddle.
Here is the example
The tinymce editor is initialised just fine, there's no errors in browser console. But I get 'undefined' if I try to get an instance of the tinymce Editor.
The question is: why does tinymce.get(id); result in undefined?
HTML:
<div ng-app="myApp">
<div ng-controller="MainCtrl">
<my-editor ng-model="text"></my-editor>
</div>
</div>
JS:
var app = angular.module('myApp', []);
app.controller('MainCtrl', function($scope) {
});
app.directive('myEditor', function () {
var uniqueId = 0;
return {
restrict: 'E',
require: 'ngModel',
scope: true,
template: '<textarea></textarea>',
link: function (scope, element, attrs, ngModel) {
var id = 'myEditor_' + uniqueId++;
element.find('textarea').attr('id', id);
tinymce.init({
selector: '#' + id
});
var editor = tinymce.get(id);
alert(editor); **// why is this undefined?**
}
}
});
I've also played with options in Frameworks & Extensions section of JSFiddle. But with no success.
You are dealing with the issue, where the elements have not been appended to the dom when you are doing your alert. (look at the html in firebug)
<my-editor ng-model="text" class="ng-scope ng-pristine ng-valid">
<textarea id="myEditor_0"></textarea>
</my-editor>
Placing the alert inside of a setTimeout will allow you to alert() the object.
setTimeout(function(){
var editor = tinymce.get(id);
alert(editor); // why is this undefined?
},0);
The proper way to go is to set the init_instance_callback option in tinyMCE.init or in tinymceOptions if you are using angular-ui-tinymce :
$scope.tinymceOptions = {
init_instance_callback : function(editor) {
MyCtrl.editor=editor
console.log("Editor: " + editor.id + " is now initialized.");
editor.on('Change', function(editor, e) {
MyCtrl.func()
});
}
Additionally to the answer Mark gave:
You will need to wait for the editor to get initalized and ready to be usable.
For this you may use the onInit tinymce handler. onInit gets fired when the editor is ready.

Resources