Directly access form element from form.FormController? - angularjs

I'm working on several components (directives) to aid in form validation. I'd like the components to be aware of the related input element's state (such as required). For example...
Markup:
<form name="editUser">
<control-label input="editUser.name">Name</control-label>
<input type="text" name="name" ng-model="user.name" required/>
</form>
Directive:
app.directive("controlLabel", function() {
return {
restrict: "E",
replace: true,
transclude: true,
scope: {
input: "=input"
},
template:
'<label class="control-label">'+
'<span ng-transclude>{{label}}</span>'+
'<span ng-if="input.required"> (required!)</span>'+ // doesn't work?
'</label>'
};
});
Output:
<form name="editUser">
<label>
<span>Name</span>
<span>(required!)</span>
</label>
<input type="text" name="name" ng-model="user.name" required/>
</form>
The source for form.FormController leaves me to believe this isn't possible. Is there any way to at least get access to the attrs on the element? I thought about using a decorator, but so far I haven't been able to figure out how that would be done.

You should use input.$validators.required instead of input.required as:
myApp.directive("controlLabel", function() {
return {
restrict: "E",
replace: true,
transclude: true,
scope: {
input: "=input"
},
template:
'<label class="control-label">'+
'<span ng-transclude>{{label}}</span>'+
'<span ng-if="input.$validators.required"> (required!)</span>'+
'</label>'
};
});
Working Demo

First, there's an error in your code. If you're using transclude, you will have to declare it in your directive, or you will get an error.
As for your problem, within a directive, you can access DOM element. And for this particular use case, you can even get by without creating isolated scope or extra attribute at all:
app.directive("controlLabel", function() {
return {
restrict: "E",
replace: true,
scope: true, // if needed, can also use empty isolated scope { }
transclude: true, // must declare this
template:
'<label class="control-label">' +
'<span ng-transclude>{{label}}</span>' +
'<span ng-if="show"> (required!)</span>' +
'</label>',
link: function(scope, jqElem, attrs) {
// check if next sibling of this directive is required
scope.show = (jqElem.next().attr("required") === 'required');
}
};
});
Now your control label is really minimal and DRY, and the logic is well encapsulated within a directive. The only requirement is that your directive target must be the next sibling of the label:
<control-label>Name</control-label>
<input type="text" name="name" ng-model="user.name" required/>
Of course, if needed, you can use your jquery-fu to change the link function to suit your other requirements.

Related

Decorating AngularJS Directive template inside the directive

I have a directive which is defined, say, as below:
angular.module('some-module').directive('someDirective', function() {
return {
restrict: 'E',
replace: 'true',
templateUrl: 'some-template.html',
link: link,
require: '^form',
transclude: true,
scope: {
decorate: '=',
}
};
});
Let's say this is how the some-template.html looks (there is more in the actual template though):
<div ng-transclude></div>
And this is how I will use the directive:
<some-directive decorate="true">
<input name="x" type="number" ng-model="x">
<input name="y" type="number" ng-model="y">
</some-directive>
<some-directive decorate="false">
<input name="a" type="number" ng-model="a">
<input name="b" type="number" ng-model="b">
</some-directive>
What I want the directive to do is to manipulate the DOM so that if decorate is true then, the two input fields should be decorated with some divs as below:
<div class="some-outer-class">
<div class="some-class-1">
<input name="x" type="number" ng-model="x">
</div>
<div class="some-class-2">
<input name="y" type="number" ng-model="y">
</div>
<div><i class="some-glyph-icon"></i></div>
</div>
If the decorate attribute is false, or absent, the directive shouldn't do any manipulation.
Couldn't figure out how to do this. Any help is appreciated.
You can simply modify the template in link function :
Demo
link: function(scope, elem, attrs){
if(scope.decorate || attrs.decorate != null){
elem.find('INPUT').wrap('<div class="decorate-class"></div>')
}
}
You can do this inside the directive. You first define a controller inside your directive as follows:
angular.module('some-module').directive('someDirective', function() {
var controller = function($scope) {
//The controller methods
};
return {
restrict: 'E',
replace: 'true',
templateUrl: 'some-template.html',
link: link,
require: '^form',
transclude: true,
scope: {
decorate: '=',
},
controller: controller,
controllerAs: 'myCtrl'
};
});
Inside the controller, you check the decorate value, and make the DOM manipulation accordingly. You can access the decorate value from your controller via the $scope.
var controller = function($scope) {
if($scope.decorate){
//Make the DOM manipulation
}
};
DOM manipulation is done as follows:
var initialInput = document.querySelector('query'); //You have to select your desired input elements here
var decoratedInput = document.createElement("div");
decoratedInput.className += " some-class-1";
decoratedInput.innerHTML = "<input name='x' type='number' ng-model='x'>";
initialInput.parentNode.replaceChild(decoratedInput, initialInput);

angular 1.4 directive toggle view

I want to show two different inputs depending on a toggle attribute.
No I have the problem that I should define each attribute/property of the input in my directive, but the binding doesn't work.
Directive:
angular.module('directive')
.directive('inputBlock', function () {
return {
restrict: 'AEC',
replace: true,
scope: {
model: '=',
modernStyle:'=',
name:'=',
type:'=',
label:'='
},
link: function (scope, elem, attrs, ctrl) {},
templateUrl: 'views/templates/inputBlockTemplate.html'
};
});
Template:
<div>
<div ng-if="!modernStyle">
<label>{{label}}</label>
<input ng-model="model" name="{{name}}" type="{{type}}"/>
</div>
<md-input-container ng-if="modernStyle">
<label>{{label}}</label>
<input ng-model="model" name="{{name}}" type=" {{type}}"/>
</md-input-container>
</div>
Usage:
<input-block model="name" label="'firstname'" modern-style="true" name="'firstname'" type="'text'">
</input-block>
Is it possible to do something like a toggle in directives?
Furthermore is it possible to redirect the bindings to directives?
ng-if creates a new child scope, try change by ng-show/ng-hide
Understanding Scopes
Other considerations:
As you use name,label and type as text inside your directive is not necesary that it will be binding, I recomend use # instead of =, or access it directly from the attrs link argument.
The scope.model could be set as ngModel to use the default angular directive.
Javascript:
angular.module("directive").directive("inputBlock", function () {
return {
restrict: "AEC",
replace: true,
scope: {
ngModel: "=",
modernStyle:"#",
//name:"#",
//type:"#",
//label:"#"
},
link: function (scope, elem, attrs, ctrl) {
scope.type=attrs.type;
scope.name=attrs.name;
scope.label=attrs.label;
},
templateUrl: "views/templates/inputBlockTemplate.html"
};
});
Template:
<div>
<div ng-show="!scope.modernStyle">
<label>{{scope.label}}</label>
<input ng-model="scope.model" name="{{scope.name}}" type="{{scope.type}}"/>
</div>
<md-input-container ng-show="scope.modernStyle">
<label>{{scope.label}}</label>
<input ng-model="scope.model" name="{{scope.name}}" type={{scope.type}}"/>
</md-input-container>
</div>
Usage:
<input-block ng-model="name" label="firstname" modern-style="true" name="firstname" type="text">
</input-block>

Create custom input directive in angular

I would like to create a custom input that looks like that:
<my-input ng-model="aScopeProperty" placeholder="Enter text"
data-title="This is my title"></my-input>
my-input should receive any property that regular input can get (like placeholder and etc...).
the output should be like this (myInputTemplate.html):
<div class="my-input">
{{title}}
<input type="text" ng-model="text" />
</div>
I created a directive:
myApp.directive('myInput', function(){
return {
restrict: 'E',
require: 'ngModel',
templateUrl: '/myInput/myInputTemplate.html',
replace: true,
scope: {
text: '=ngModel',
title: '=title'
},
}
});
the ng-model is bindded ok now,
my question is:
How can I pass the attributes (like placeholder and etc) from my-input to the inside input?
I think that I approached it the wrong way, maybe I need to do it like that:
<input my-input ng-model="aScopeProperty" placeholder="Enter text"
data-title="This is my title"></input>
and to wrap the input with:
<div class="my-input">
{{title}}
<here will be the original input>
</div>
directive call should be like
<my-input ng-model="aScopeProperty" placeholder="'Enter text'" title="'This is my title'"></my-input>
note the placeholder="'Enter text'" Enter text with in quotes ('), this indicate these values are string so angular will not search for scope variable.
and in the directive
myApp.directive('myInput', function(){
return {
restrict: 'E',
require: 'ngModel',
templateUrl: '/myInput/myInputTemplate.html',
replace: true,
scope: {
text: '=ngModel',
title: '=title',
placeholder : '=placeholder'
},
}
});
and the template
<div class="my-input">
{{title}}
<input type="text" ng-model="text" placeholder="{{ placeholder }}" />
</div>
here is the demo Plunker
You can use ng-attr as the following:
<input type="text" ng-model="text" ng-attr-placeholder="{{placeholder}}"/>
And send placeholder as attribute in your scope as the following:
scope: {
text: '=ngModel',
title: '=title',
placeholder : '=placeholder'
}
I recommend to read about ng-attr-attrName, and this useful answer.
Dynamic attributes
Read my question, and accepted answer.
The second approach succeeded!
final code:
<input my-input ng-model="aScopeProperty" placeholder="Enter text"
data-title="This is my title">
The Directive:
app.directive('myInput', function () {
return {
restrict: 'A',
scope: {
title: '=title'
},
link: function ($scope, $element) {
var wrap = angular.element('<div class="my-input-wrapper" />');
$element.addClass('form-control').removeAttr('my-input');
wrap.insertBefore($element);
$element.appendTo(wrap);
if ($scope.title) {
var title = angular.element('<span class="my-title">' + $scope.title + '</span>');
title.appendTo(wrap);
}
},
}
});
I even created my first Plunker for it, unfortunately, the Plunker don't works because it doesn't recognize: insertBefore and appendTo
http://plnkr.co/edit/XnFM75vOBg4ifHUQzGOt?p=preview

Angular directive ng-repeat to generate pair of elements input+span

Here is my request, using directive I try to generate this structure above :
<div class='myClass'>
<input id="id0" name="name0" type="radio" checked>
<label for="id0" ng-click="myAction('0')"> 0</label>
<input id="id1" name="name1" type="radio">
<label for="id1" ng-click="myAction('1')"> 1</label>
<input id="id2" name="name2" type="radio">
<label for="id2" ng-click="myAction('2')"> 2</label>
...3,4,5...
span(class="endSpanClass")
Not very trivial:
I have tried using directive in many ways, ng-repeat but no success to get this pair of input/label together, I mean input+label in same order as in this example.
Then on first element, I expect to get "checked" attribute.
The last span has to be set at the end also.
That kind of try
.directive('myDirective', ['$timeout', function(timeout) {
return {
restrict: 'A',
controller: 'MyController',
scope: {myList: '='},
template: '<input id="id0" name="view" type="radio">' +
'<label for="id0" ng-click="myAction(\'day\'> 0 </label>',
transclude: false,
replace: true,
link: function($scope, element, attr, ctrl) {
}
};
}])
And my directive call
div(class='myClass')
input(my-directive, my-list='list', ng-repeat="item in list", id="0", name="name0", type="radio")
Gives nothing,
If you can help and gives me some advices, thanks.
J.
Following is what I would do:
<label ng-repeat="name in names">
<input name="{{name}}" type="radio"
ng-checked="$index==0"
ng-click="myAction($index)"/>
{{$index}}
</label>
wrapping form control by label which would simplify the code
also make very explicit what label control what control.
(just a standard html technique.)
use ng-checked to control checked state.
I would avoid using directive for this short code unless
it is used many times in many places.
if you use directive, template has to have single root element.
(so you can actually resort to technique [1] above.)
Here is a simple directive definition:
app.directive('myDirective', ['$timeout', function(timeout) {
return {
restrict: 'E',
scope: {id: '='},
template: '<div><input id="id{{id}}" name="view" type="radio">' +
'<label for="id{{id}}" ng-click="myAction(\'{{id}}\')">{{id}}</label></div>',
replace: true
};
}])
Along with a working example: http://plnkr.co/edit/spQRs5xs43P1FOX7YQzH?p=preview

Angularjs: validation not working when control is based on directive

Being rather new to Angularjs, I am creating textbox-label combinations in Angularjs using directives. It's working very well, but I can't get validation to work. Here is a stripped-down example.
The Html:
<form name="form" novalidate ng-app="myapp">
<input type="text" name="myfield" ng-model="myfield" required />{{myfield}}
<span ng-show="form.myfield.$error.required">ERROR MSG WORKING</span>
<br>
<div mydirective FIELD="myfield2" />
</form>
The Javascript:
var myapp = angular.module('myapp', []);
myapp.directive('mydirective', function () {
return {
restrict: 'A',
scope: { ngModel: '=' },
template: '<input type="text" name="FIELD" ng-model="FIELD" />{{FIELD}}
<span ng-show="form.FIELD.$error.required">ERROR MSG NOT WORKING</span>'
};
});
The hard coded input - myfield - works, the other - myfield2 - doesn't (the binding does, just not the required-error message).
How do I tell the ng-show attribute to sort of "replace" FIELD in form.FIELD.$error.required by myfield2?
Here is a jsFiddle.
The problem is that your directive creates a new scope for the directive, this new scope does not have access to the form object in the parent scope.
I came up with two solutions, though I suspect there is a more elegant "Angular" way to do this:
Passing down the form object
Your view becomes:
<div mydirective FIELD="myfield2" form="form" />
And the scope definition object:
return {
restrict: 'A',
scope: {
ngModel: '=',
form: '='
},
template: '<input type="text" name="FIELD" ng-model="FIELD" required/>{{FIELD}}<span ng-show="form.FIELD.$error.required">ERROR MSG NOT WORKING</span>'
};
I've updated the fiddle with this code: http://jsfiddle.net/pTapw/4/
Using a controller
return {
restrict: 'A',
controller: function($scope){
$scope.form = $scope.$parent.form;
},
scope: {
ngModel: '='
},
template: '<input type="text" name="FIELD" ng-model="FIELD" required/>{{FIELD}}<span ng-show="form.FIELD.$error.required">ERROR MSG NOT WORKING</span>'
};

Resources