AngularJS custom directive with input element, pass validator from outside - javascript

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

Related

Using a reusable directive for form inputs

I just read through the directive docs and I'm still not understanding how I'd accomplish the following with reusable code. I have multiple form fields that would best be used in a select > option setup, however I'm looking to replace that with a directive with a template because of how mobile browsers handle selects (iOS magnifies the options and some of my values are too long to be viewed in the display).
So my template would look something like this:
<div class="list">
<div class="option" ng-repeat="option in form.questionOneOptions" ng-click="selectOption(option)">
{{option}}
<i class="checkIcon" ng-if="option.isSelected"></i>
</div>
</div>
This would be the only thing on the detail page. Its parent page being the list of fields you're filling out, which is where the data needs to be available. I just don't know where to start on the directive. There is an isolated scope for each question, which holds the options for that question. There needs to be a way to give the directive the list of options. There is an encompassing scope for all the questions to keep the recorded answers in a single object like form.
I know I could do this with a single controller and copy/pasting the above and changing form.questionOneOptions with one massive object living in the controller, but I'm trying to do this the right way by limiting my DOM manipulation to directives.
You'll want to use the html you have there as the template for your directive. Then you implement selectOptions in your link function.
app.directive('gsQuestion', function() {
return {
restrict: 'E',
require: 'ngModel',
scope: {
ngModel: '=',
options: '='
},
template:'<div class="list">'+
'<div class="option" ng-repeat="option in options" ng-click="selectOption(option)">'+
'{{option}}'+
'<i class="checkIcon" ng-if="option.isSelected"></i>'+
'</div></div>',
link: function(scope, element, attrs) {
scope.selectOption = function(option)
{
// implement selectOption
}
}
};
});
Then you can use the directive in your html.
<gs-question ng-model="myValue1" options="form.questionOneOptions"></gs-question>
<gs-question ng-model="myValue2" options="form.questionTwoOptions"></gs-question>
Just to be clear, a directive can share data to/from the view through the use of the directive's $scope variables.
angular.module('app', [])
.directive('mySampleDirective', function(){
return{
restrict: 'AE',
scope: {
data: '=' // this sets up a two way binding
}
},
link: function(scope, element, attributes){
console.log(scope.data) // <---- this is where you would do DOM manipulation,
// because you have access to the element.
}
})
And then in your markup, pass in the data you want to make available in your directive.
<my-sample-directive data="FeeFee"></my-sample-directive>

Issue with element.html() in Angular directive

I am new to Angular. I have a directive and in the linkFunction I am able to set the scope to a certain value by using attributes["attribute-value"]. I was expecting element.html() to provide the inner html of the directive.
.directive("sfGroupbar", function () {
var linkFunction = function (scope, element, attributes) {
scope.text = element.html();
scope.attr = attributes["text"];
};
return {
restrict: 'E',
templateUrl: "controls/groupbar.html",
link: linkFunction,
scope: {}
};
In my view, I am using the directive like so...
<sf-groupbar warning="50" error="80" text="Web Server">Some Text</sf-groupbar>
In groupbar.html, I am using the code like so...
<div ng-controller="groupbarController">
{{text}} <br />
{{attr}}
</div>
I was expecting to see "Some Text" and "Web Server" as output. I am getting only Web Server as output, and instead of "Some Text", I am getting the following as output...
<div ng-controller="groupbarController">
{{text}} <br />
{{attr}}
</div>
You have to set transclude property of the directive to true and have to include ng-transclude attribute inside the template or templateurl 's HTML element to make innerHTML of the directive to render.
Here is the working plunker based on your code,
http://embed.plnkr.co/sXoLPxeFA21fxzzeAcVs/preview
Hope this helps!!!!
You need to include text and the other attributes in your scope definition like so
scope { text : '=' } and also you might wanna add transclude option to true to try and get the text inside your directive.
I'm sure you'll be interested to look at this part of the documentation
Directive to manipulate the DOM

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

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/

Angular ng-model bind to element name

Is there a syntax that will allow me to use ng-model to bind to a scope property with the same name as the element I am binding to?
So, in the following example:
<input name="myinputname" type="text" ng-model="?" />
is there something I can use as the ng-model value that will resolve to "myinputname", without requiring me to hard-code it?
If you could add the ng-model from the server side, it will have a better performance.
But in case you really want to do it in client-side, you could write a custom directive that will add the ng-model automatically at compile time like this:
app.directive('myModel', function($compile) {
return {
restrict: 'A',
replace: false,
priority: 1000,
terminal: true, // these terminal and a high priority will stop all other directive from being compiled at first run
link: function (scope, element, attrs) {
attrs.$set('ngModel', attrs.name); // set value of ng-model to be the same as the name attribute
attrs.$set('myModel', null); // remove itself to avoid a recusion
$compile(element)(scope); // begin compiling other directives
}
};
});
and use it like this:
<input type="text" name="myinputname" my-model />
after the first compilation it will automatically become:
<input type="text" name="myinputname" ng-model="myinputname" />
Plunker example: http://plnkr.co/edit/hBQQMDTr6cYtHzFvoAaQ?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

Categories