How do I specify an angular directive in the link of another? - angularjs

I'm trying to create a server-validate directive, which asynchronously validates form input by submitting a partial form to our server back-end and parsing the response. I hoped to use it like this:
<input type="text" ng-model="stuff" server-validate />
(I'm combining it with another directive on the wrapping <form>, that specifies what URL to use etc...) In order for the form not to submit validation requests on page load, I need to set ng-model-options="{ updateOn: 'blur' }", but I'd like to *not* have to do this on every element in the form. Instead, I'd like theserver-validate` to specify this behavior as well.
I've tried a couple of things in the link function, for example attrs['ngModelOptions'] = '{updateOn: "blur"}' and attrs['ngModelOptions'] = { updateOn: 'blur' }, but neither had any effect at all.
Is there a way to apply this through my own directive, without having to specify anything else?

They have a great example of what you want over at the docs:
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);
}
}
};
}]);
So the only thing that looks like what you would change would be to remove the keyup and change events from the element.on. Then in your blur you would also do the server request. Docs on ngModelController: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

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 placeholder via AngularJS directive?

I have a text input field whose placeholder I want to change every few seconds. However I don't want to pollute my controller with this so I want to encapsulate this functionality into a directive.
This is what my directive looks like:
myApp.directive('searchBox', ['$interval', function($interval) {
return {
restrict: 'A',
link(scope, element, attrs) {
$interval(function() {
attrs.placeholder = 'New';
}, 1000);
}
}
}])
And the html:
<input type="text" class="form-control" placeholder="Old" ng-model="search" search-box>
However the placeholder stubbornly doesn't change, even though in the console attrs.placeholder can be seen to change to 'Test' from 'Hello'. Any ideas?
PLUNKR: https://plnkr.co/edit/Oy1M8FPTXxzB9oYMJqlx?p=preview
You cannot change attributes values via the attr object (it's just a static reflection of your element attributes). Instead, update your element using element.attr('placeholder', 'Test') or attrs.$set('placeholder', 'Test').

angularjs textarea with colors (with html5 contenteditable)

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).

Modifying a directive to 'watch' a ctrl property?

I have a directive which checks if they click submit on a form. If so, it adds the 'submitted' class. This allows me to style the form inputs as red only when they have submitted a form (I hate having it red in real-time as they're typing).
'use strict';
app.directive('formSubmitted', function() {
return {
restrict: 'A',
require: 'form',
link: function(scope, element, attrs, ctrl) {
ctrl.$submitted = false;
element.on('submit', function() {
scope.$apply(function() {
ctrl.$submitted = true;
element.addClass('submitted');
});
});
}
};
});
The problem is 'resetting' a form once it has been submitted successfully... I need to remove the submitted class from inside the controller. I have tried a lot of ways to do this without success... as in this pseudo-code...
angular.element($scope.myForm).removeClass('submitted');
What I am thinking is instead of doing that from the controller (which doesn't work anyway), that I try to make the 'submitted' class mirror the $submitted property on ctrl... This way I could do...
$scope.myForm.$submitted = false and the class would update appropriately.
I have no idea even where to begin with though, and googling isn't helping...
Thanks!
A simple approach I have used in situations like this is leveraging the Angular ngClass directive and binding to a property on the controller that maintains whether the state is submitted or not. Something like so:
<button ng-click="isSubmitted = !isSubmitted">Submit</button>
<form ng-class="{submitted: isSubmitted}">
</form>
You can use the ng-class directive:
<form name="myForm" ng-class="{submitted: $submitted}">
See the doc here: https://docs.angularjs.org/api/ng/directive/ngClass
Within the controller handling the form submission, you certainly have a submit function:
$scope.submit = function (form) {
$scope.$submitted = true;
if (form.$invalid) {
return;
}
// Actually send data to backend, eventually receiving a promise
promiseFormBackend = MyService.sendForm();
promiseFromBackend.then(function () {
$scope.$submitted = false: // resetting the form class
});
}

AngularJS prevent ngModel sync

I have a simple directive called po-datepicker, it displays a datepicker on the screen, but allows the user to type a date manually:
<input type="text" ng-model="model" po-datepicker required />
and this is the directive:
myApp.directive('poDatepicker', function () {
return {
require: ['?^ngModel'],
restrict: 'A',
link: function ($scope, elem, attrs, ctrl) {
var ngModel = ctrl[0];
var picker = elem.datepicker();
picker.on('changeDate', function(e) {
ngModel.$setViewValue(e.date);
...
});
elem.parent().find('button').on('click', function() {
picker.datepicker('show');
});
var changeFn = function(e) {
// Here I have some logic that calls $setViewValue();
};
picker.on('hide', changeFn);
elem.on('keyup blur', changeFn);
}
};
});
this works as expected, but when I try to type a value in the input, it updates the ngModel, changing the variable in the scope, how can I prevent ngModel from being changed in the input?
Here is a plunkr, try manually writing a value and you'll understand what I'm talking.
Actually, after some research, I found a solution for this problem.
What I found on forums and questions is that I needed to unbind the element's events, like this:
elem.unbind('input').unbind('keydown').unbind('change');
But that solution didn't work as expected.
The problem is that I'm currently using Angular 1.2.x, I found out that you need also to set some priority to the directive, such as:
return {
require: ['?^ngModel'],
priority: 1,
...
}
The priority: 1 is needed in this case, because of the priority of some internal Angular.js directives.
Here is an updated plunker with the right priority set up.
Just add 'disabled' to the input http://plnkr.co/edit/xFeAmSCtKdNSQR1zbAsd?p=preview
<input type="text" class="form-control" ng-model="test" po-datepicker required feedback disabled/>

Resources