I'm trying do a small reusable component in AngularJS using directives.
I have made good progress but I have a problem with the validations. For example the required validation not working. I think is "binding" issue.
My HTML code: also in http://jsfiddle.net/pQwht/17/
<html ng-app="myApp">
<body>
<form ng-controller="Ctrl"
id="paymentCallForm"
action="#"
name="paymentCallForm">
<table>
<tr tdfield
labelname="Primary Account Number:"
fieldname="primaryAccountNumber"
title="Primary title"
>
</tr>
</table>
My directive script:
angular.module('myApp').directive('tdfield', function() {
return {
restrict: 'A',
replace:false,
transclude: false,
scope: { labelname: '#', fieldname: '#', title: '#'},
templateUrl:'element.html'
};
});
My element.html code:
<td id="lbl_paymentReference" class="formInputLabelWrapper">{{labelname}}</td>
<td class="formInputTextWrapper">
<input id="{{fieldname}}"
name="{{fieldname}}"
title="{{title}}"
class="large empty"
required>
<span data-ng-show="paymentCallForm.{{fieldname}}.$error.required"
class="error">Error</span></td>
Well, I solved this, but for what a price. There is a number of issues and angular related among them. I may not recall all, so here is the working example https://github.com/yaroslav-ulanovych/soq16245177.
When you define scope parameter like scope: { labelname: '#', fieldname: '#', title: '#'}, (with an object as a value), that creates an isolated scope, which means not inherited from parent one's. Therefore here ng-show="paymentCallForm.{{fieldname}}.$error.required" is no access to the form. As a workaround ng-show="$parent.paymentCallForm.{{fieldname}}.$error.required", but I didn't check whether your inputs are published in the form in case of the isolated scope. Or scope: true and inject attributes into the scope manually.
compile: function() {
return {
pre: function (scope, element, attr) {
scope.fieldname = attr.fieldname;
}
}
}
Note on using prelink function, so that it's called before children are linked.
Next ng-show will actually use not interpolated expression and there is obviously no property named {{fieldname}} in the form. That is fixed in later versions of Angular, don't know when exactly, cause I'm using master.
But what is not fixed is ngModelController. It gets it's name very early so publishes itself on the form under wrong one. I had to fix that myself, good that there is only one line to do that, in file src/ng/directive/input.js.
// add
modelCtrl.$name = attr.name;
// before this
formCtrl.$addControl(modelCtrl);
I believe you need a controller attached to your view. The form object will be attached to property with the id of the form on $scope object of this controller. Once you add that, I think it will start showing up.
Related
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>
I just answered a question here
How i send value when i click button to server and popup directive
His code looked like this
<div ng-controller="IndexController">
<label id="number" >5</label>
<input type="button" ng-click="getUserDetails()" >
</div>
He was trying to access the value of the label element with id #number.
I understand that in Angular, DOM manipulation and querying should be done in a custom directive, and not inside a controller. I therefore gave this solution.
Since you want to access the value of a number, you are interacting with the DOM. The best solution for this is creating custom directives. Here is an example.
angular.module('myApp', [])
.directive('getValue', function({
restrict: 'A',
link: function(scope, elem, attrs) {
elem.bind("click", function() {
scope.value = elem.prev().text();
});
}
});
I know that in a directive's link function, you pass in scope, element and attrs as a param. In this line, I'm trying to set a scope variable to the label's value.
scope.value = elem.prev().text();
But I have a suspicion that I'm not doing it correctly. Since directives are the way to go, how can I do this.
I also checked this as a reference
Easiest way to pass an AngularJS scope variable from directive to controller?
I've read about the controller function inside directives, but I need the link function for getting the value from the DOM.
There's no need for a custom directive for that. ng-click can work:
$scope.getUserDetails = function(e){
$scope.value = e.target.previousElementSibling.innerHTML;
}
HTML
<div ng-controller="IndexController">
<label id="number" >5</label>
<input type="button" ng-click="getUserDetails($event)" >
</div>
$event
I am new to angularJS. I am trying to implement the a simple functionality wherein there are a number of checkbox options and one specific function needs to be called on click of these checkbox options something similar to
$(parentDiv).on("click",'input[type="checkbox"]', functionToBeCalled});
I am trying to use a directive for this section with content similar to
<div>
<span ng-repeat="subsection in section"><input type="checkbox" value="subsection.name"></input> {{subsection.name}} </span>
</div>
Directory
app.directive('myDir', function() {
return {
restrict: 'AE',
replace: true,
templateUrl: 'filter.html',
link: function(scope, elem, attrs) {
elem.find("input").bind('click', function() {
//do some data manipulation
});
});
};
});
When i use the link function I find that if I use elem.find('input[type="checkbox"]) gives me an empty object so it is not compiled before that. Kindly let me know how to address this situation. I did a bit of a research and found out about compile function which manipulates DOM before linking,but i am not able to think of the approach ahead. Any help will be appreciated. Thanks in advance.
Use ngClick or ngChange Directive inn angularjs
<div>
<span ng-repeat="subsection in section">
<input type="checkbox" value="subsection.name" ng-click="myFunc(subsection.name)" /> {{filter.name}} </span>
</div>
here in the example i used ng-click = "myFunc" and pased the value in that function
It might help for us to know how you directive looks like and specifically what element that directive is attached to, since you're speaking about elem.
Nevertheless, if you just need functions to be called when the checkbox is clicked, you can use built-in angular functions for this:
ngChange
ngClick
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/
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.