AngularJS attribute directive. How to add another attribute directive on 'compile' - javascript

I would like create an attribute directive which add an icon on a button when it's disabled.
Like this Fiddle
However, I would also add the ng-disabled directive on compile (with the same disabled-button value)
What is the best way ?
If I add the attribute ng-disabled on compile function, it never compile.
So, if I re-compile my element on link function, I have to remove ng-tranclude directive due to an error. More, my events, like ng-click, are triggered twice.
Bonus question: Is it possible to restrict my attribute directive to html elements like <a> or <button> ?
Thx

I'm afraid you cannot add directives dynamically to the element that contains your directive. The reason is that your compile function will be called after Angular has processed the directive's element and determined what the directives are attached to it. Adding another attribute at this point is too late, discovery has already taken place.
There may be ways to do it that I don't know of (and I would be interested in seeing any stable, non-hackish one).
I can suggest an alternative that may suit you: manually place ng-disabled on the button, but for brevity and consistency let the expression of ng-disabled drive your directive, i.e.:
<button ng-click="ctrl.click()" ng-disabled="ctrl.disabled" disabled-button>
Directive code:
.directive('disabledButton', function($parse) {
return {
restrict: 'A',
transclude: true,
scope: {
},
template: '<span ng-show="disabled">X</span><span ng-transclude></span>',
link: function (scope, elem, attrs) {
var disabled = $parse(attrs.ngDisabled);
scope.disabled = false;
scope.$watch(
function() {
return disabled(scope.$parent);
},
function(newval) {
scope.disabled = newval;
}
);
}
};
})
Fiddle: http://jsfiddle.net/3orwupo5/1/
Or you can manually set the disabled property of the button: http://jsfiddle.net/y5ezvj5L/

Related

AngularJS custom directive with input element, pass validator from outside

I'm using a simple custom directive for a modified input field which occurs throughout my application:
app.directive('editor', function() {
return {
restrict: 'E',
templateUrl: 'editor.html',
scope: { value: '=' }
};
});
The editor.html basically creates an input element with additional controls. Simplified it looks like this:
<div>
<input ng-model="value">
<!-- more code here -->
</div>
I access my directive using <editor value="{{object.name}}"></editor>. This works perfect. Now I need to perform different validations on the input. The necessary validators to use vary, so I would like to be able to pass the actual validators to my directive. Something like:
<editor value="{{object.name}}" validator-a validator-b></editor>
or
<editor value="{{object.name}}" validators="validatorA,validatorB"></editor>
How could I achieve that?
You are creating a custom input control, so you might as well support ng-model - which is the right thing to do.
And, validators - built-in and custom - only require: "ngModel" for their function and they are independent (by design) from the underlying DOM implementation, so having ng-model automatically supports all ng-model-based validators.
Having ng-model support will also make your directive participate in form validation.
Since you are using an existing directive inside the template (i.e. <input>), you don't even need to bother with DOM, as you would've had you built something from scratch.
app.directive("editor", function(){
return {
require: "?ngModel",
scope: true,
template: "<input ng-model='value' ng-change='onChange()'>",
link: function(scope, element, attrs, ngModel){
if (!ngModel) return;
scope.onChange = function(){
ngModel.$setViewValue(scope.value);
};
ngModel.$render = function(){
scope.value = ngModel.$modelValue;
};
}
};
});
Then you can just apply validators directly:
<editor ng-model="foo" minlength="3"></editor>
http://plnkr.co/edit/k21Oem6kT8SXUefyhbI6?p=preview

AngularJS - ng-model fails on contenteditable <span>

I'm learning AngularJS. I've come across something I can't explain, nor can I find any explanation for (or solution).
I have a simple AngularJS app and I am attempting to bind a <span contenteditable="true"> to a value, but it doesn't work. EG:
<!-- Works as expected -->
<input data-ng-model="chunk.value"></input>
<!-- Shows value, but doesn't bind - changes not reflected in model -->
<span contenteditable="true">{{chunk.value}}</span>
<!-- This is empty -->
<span contenteditable="true" data-ng-model="chunk.value"></span>
How can I make the last span use 2-way binding, such that editing its value updates chunk.value and vice versa?
ng-bind! Use ng-bind for one-way binding in 'span'.
Please refer here for an example: https://docs.angularjs.org/api/ng/directive/ngBind
So your line would be:
<span contenteditable="true" ng-bind="chunk.value"></span>
Hope this help
To make ng-model work with contenteditable <span> elements, use a custom directive:
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);
}
}
};
}]);
Usage:
<span contenteditable ng-model="userContent">Change me!</span>
<p>{{userContent}}</p>
For more infomation, see
AngularJS ngModelController API Reference - Custom Control Example
AngularJS Developer Reference - Creating Custom Directives
The DEMO
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);
}
}
};
}]);
[contenteditable] {
border: 1px solid black;
background-color: white;
min-height: 20px;
}
<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/angular-sanitize/angular-sanitize.js"></script>
<body ng-app="customControl">
<span contenteditable ng-model="userContent">Change me!</span>
<hr>
Content={{userContent}}
</body>
The ngModel won't work as #VtoCorleone pointed out. ngModel docs
The ngModel directive binds an input,select, textarea (or custom form control) to a property on the scope using NgModelController, which is created and exposed by this directive.
You may have a look at the contenteditable directive.
Otherwise, Potential workaround: have a function that gets called. That function then updates the $scope.chunk.value within your controller. And it would take care of the other elements' contents as the binding gets updated.
I'm not sure the exact look or functionality that you're going for, but just put it inside of a <textarea> and style it to look like a <span> (no border or background, etc). Then when it is in focus, you add additional styling to know that it can be edited. This way would allow you to use the ng-model as it is intended to be used. Here is a basic implementation of this approach: Plunker
ng-model is not meant to be used with span. If you absolutely need that you can write a custom directive for this. This directive will set up a keydown,keyup listener on the contentEditable span and update the scope model (within $apply()). This will bind span content to model.
I quickly created a plunker for you. Check it out. It syncs the <span> content to scope model. Open up the browser console to see the scope model update whenever you type something.
By adding ng-model-options="{ getterSetter: true }" behavior to an element that has ng-model attached to it. You can also add ng-model-options="{ getterSetter: true }" to a <form>, which will enable this behavior for all <input>s within it.
Example shows how to use ngModel with a getter/setter:
demo page

AngularJS ngIf prevents finding element inside directive

I have an AngularJS directive that includes an ngIf and I would like to modify some of the DOM inside the ngIf in the directive link function. Unfortunately it seems that ngIf prevents me from finding DOM elements within it in the link function.
Here is the code for the directive:
directive('column', function () {
return {
templateUrl: 'views/column.html',
restrict: 'E',
scope: {
column: '='
},
controller: ['$scope', function ($scope) {
$scope.editing = true;
$scope.toggleEditing = function () {
$scope.editing = !$scope.editing;
};
}],
link: function postLink(scope, element) {
var select = element.find('select');
console.log(select); // See if it can find the select element
// var types = scope.column.types();
// add types as options to the select element
}
};
});
And here is the simplified html of the directive:
<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-if="editing">
<select></select>
</form>
</div>
Here is the link to the jsFiddle example http://jsfiddle.net/dedalusj/Y49Xx/1/
The element.find call in the link function returns an empty array but as soon as I remove the ngIf from the form it returns the proper select DOM element. I have the feeling that I'm doing this the wrong way.
UPDATE
Thanks for the answers but I found another solution. I simply created another directive that encapsulate the form, added it to the column directive template with ng-if="editing".
The form directive doesn't have it's own scope so it effectively operates out of the column directive scope and has always access to the select element because it's inside its DOM tree. I pay the cost of an extra directive but I don't have to use the $timeout hack. I created a new jsFiddle to illustrate the solution http://jsfiddle.net/dedalusj/nx3vX/1/
Thanks #Michael but I can't simply use the ng-option because the types array comes from an XML file and its elements are other angular.element objects which cannot be inserted easily with ng-option.
The ngIf directive works by using Angular's transclusion feature. What happens during the compile/link cycle is:
The content inside the ngIf is removed from the DOM when it is compiled
Angular runs the link functions. The ngIf's link function is run before
the link function of the directive using it. When ngIf's link function
runs, it uses $scope.$watch() to watch the value of the ng-if
attribute.
Your directive's link function runs, at this point the content of the ngIf is not part of the DOM
The watch set up in step (2) is called, and ngIf will then call the $transclude function to insert the contents of the ngIf into the DOM if the ng-if attribute value is truthy.
Any watch functions, $timeout calls or use of $scope.$evalAsync that you registered in your directive's link function will run.
So if you want to access elements inside the ngIf's content, the code needs to run after step 4 above. This means that any functions registered with $scope.$watch, $timeout or $scope.$evalAsync in your directive's link function will work. For a one-time piece of setup code, I would probably opt for $scope.$evalAsync:
angular.directive('yourDirective', function () {
return {
...
link: function(scope, elem) {
scope.$evalAsync(function () {
// code that runs after conditional content
// with ng-if has been added to DOM, if the ng-if
// is enabled
});
}
};
});
As #moderndegree has said, ngIf removes the element it's applied to from the DOM, so you won't be able to find it when it's not there. But, you could write your directive in a way to workaround that:
controller: function ($scope, $element, $timeout) {
$scope.toggleEditing = function () {
$scope.editing = !$scope.editing;
$timeout(function() {
var select = $element.find('select');
select.append('<option>Value 1</option>')
.append('<option>Value 2</option>')
.append('<option>Value 3</option>');
});
};
}
Updated jsFiddle here.
The trick here is to delay the find() call by using $timeout with a 0 interval in order to wait for Angular to update the DOM.
UPDATE
After giving some more thought to your code, I realize that perhaps you can let Angular do the hard work for you:
Javascript
directive('column', function () {
return {
templateUrl: 'views/column.html',
restrict: 'E',
scope: {
column: '='
},
controller: ['$scope', function ($scope) {
$scope.editing = true;
$scope.toggleEditing = function () {
$scope.editing = !$scope.editing;
};
}],
};
});
HTML
<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-if="editing">
<select ng-model="type" ng-options="type for type in column.types"></select>
</form>
</div>
jsFiddle
Now you don't need to worry about finding the select element at the right time and populating it. Angular does all of that for you. :)
You can put your code from the link function inside $timeout.
$timeout(function(){
var select = element.find('select');
console.log(select);
});
Don't forget to inject $timeout in your directive
directive('column', function ($timeout) {
I was facing this same issue and i was able to resolve it using ng-show, this prevents this issue because ngIf removes the element it's applied to the DOM, so you won't be able to find it when it's not there.
so in your case:
<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-show="editing">
<select></select>
</form>
will work OK.
Cheers.

Angularjs - ng-if - bypass / work around the scope logic

I am aware that use of ng-if dictates that ng-if destroys the scope on the element unlike the use of ng-show or ng-hide.. I however need to use ng-if (ng-show / hide is not an option) because I actually need the element to not render on the page when ng-if is falsely..
I use ng-if as part of a directive template..
My directive
app.directive("myDirective", function ($parse) {
return {
restrict: "E",
scope:{},
controller: function ($scope, $element, $attrs) {
// controller code
},
templateUrl: "template.html",
compile: function(elm, attrs){
var expFn = $parse(attrs.atr1 + '.' + attrs.atr2);
return function(scope,elm,attrs){
scope.$parent.$watch(expFn, function(val){
scope.exp = val;
})
scope.$watch('exp', function(val){
expFn.assign(scope.$parent, val)
})
}
}
}
})
My template
<div ng-click="view = !view">{{exp}}</div>
<div ng-if="view">
<input type="text" ng-model="exp"><br/>
<div class="btn btn-default" ng-click="Submit()">Submit</div>
</div>
Is there a way to "bypass" the ng-if behavior,,, to KEEP the scope,, or recreate it?
I created the attach-if directive for this purpose.
The code is available at https://github.com/Sparks-Creative-Limited/angular-attach-if and, if you use bower, it's registered as a downloadable component with the following details:
angular-attach-if 0.1.0
There is also a demo page to show how it works.
If you are using ng-if it won't create the element at all period. So the directive does not exist in that instance. I know you mentioned ng-hide/show is not an option but I think its the only option. What is your reason for not wanting it to render and keep its scope? which by the way defeats the purpose of the directive. If you are doing some logic in the directive that doesn't require the view, it should belong somewhere else.
If the scope has single/few variable, ng-init="obj=$parent.obj" on the same tag where ng-if is, would help.

How can I use an angularjs directive to dynamically add attributes for form validation?

I've got a directive that I'm using to validate form fields. I want to dynamically add the validation with the directive. Here's what my directive looks like:
app.directive('validatedInput', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var constraints = {
'ng-maxlength' : 10,
'data-required': ''
}
angular.forEach(constraints, function(value, key) {
element.attr(key, value);
}
$compile(element)(scope);
}
}
}
And here is my markup:
<input type='number' name='fieldName' ng-model='data.test' validated-input></input>
So basically what I want is for the directive to add ng-maxlength=10 and data-required='' to my input element so that the validation can work (this is a trivial case; in the future I plan on getting the constraints object from the back-end using a service). The problem is, the directive seems to add the attributes to the DOM, but the validation doesn't work. In other words, if I inspect the HTML using my browser's dev tools, the markup looks correct, but the functionality isn't there. Any thoughts?
Much appreciated
I only have a bit of Angular experience (~6 weeks), but everything I've seen in regards to Directives to do DOM in Angular is clunky as fu...nky chicken...
I'm doing this currently with jQuery (which has zero chicken funk DOM manip. wise) to do a $compile after dynamically adding ng-attributes (in this case so the INPUT name is calculated for the crappy bootstrap/angular datepicker control I'm using currently):
//# Hook the .calendars object via the required Angular attributes
$('I.icon-calendar').each(function (i, obj) {
var $s, $p = $(this).parent();
//# Set the ng-click of the .parent button
$p.attr('ng-click', "calendars.open($event)");
//# Setup the calendar INPUT
$s = $p.siblings('INPUT[datepicker-options]');
$s.attr('is-open', "calendars.isOpen['" + $s.attr('name') + "']");
//# Re-$compile the DOM elements so all of the above added Angular attributes are processed
$compile($s.get(0))($scope);
$compile($p.get(0))($scope);
});
$compile needs to be "injected" (I hate that term, why can't we just say "passed"?) into the controller, a'la:
myApp.factory("Tools", function ($http, $q, $timeout, $compile) {...}
The only issue I've had thus far is some controls/plugins/etc. add DOM elements on $compile, so currently I'm wrestling the dragon of multiple datepicker UL.dropdown-menu's being added to the DOM, to which I have no answer (but I did find this question on the way, so there's that).

Categories