angularjs directive binding name attribute to template element - javascript

I am trying create a wrapper directive over select and I am trying to assign the 'name 'attribute to the select
directive
<form name=myform>
<selectformfield label="Select Orders" id="id_1" name="orderselection"
selectedval="obj.order" options="Orders" />
</form>
I have my directive defined as
mainApp
.directive(
'selectformfield',
function() {
return {
restrict : 'E',
transclude : true,
scope : {
label : '#',
id : '#',
selectedval : '=',
options : '=',
name: '='
},
template : "<select class='form-control' ng-model='selectedval' name='{{name}}' ng-options='item as item.name for item in options' required><option value=''>-- select --</option></select>"
};
});
I am trying to access the select's name attribute through myform in the controller something like console.log($scope.myForm.orderselection) and I get undefined
If I hardcode the name in the directive then I am able to access the attribute console.log($scope.myForm.orderselection)
I am missing anything here. Do I have to do any post compile or something ?

Khanh TO is correct in that you need to setup your name correctly when trying to access to through your isolated scope. Here is a working example of what I believe you are trying to accomplish. I've added comments to the code where I've changed what you had.
plunker
Javascript:
var app = angular.module('plunker', [])
.controller('MainCtrl', function ($scope, $log) {
$scope.model = {
person: {
name: 'World'
},
people: [{
name: 'Bob'
}, {
name: 'Harry'
}, {
name: 'World'
}]
};
})
.directive('selectformfield', function ($compile) {
return {
restrict: 'E',
replace: true, // Probably want replace instead of transclude
scope: {
label: '#',
id: '#',
selectedval: '=',
options: '=',
name: '#' // Change name to read the literal value of the attr
},
// change name='{{ name }}' to be ng-attr-name='{{ name }}' to support interpolation
template: "<select class='form-control' ng-model='selectedval' ng-attr-name='{{name}}' ng-options='item as item.name for item in options' required><option value=''>-- select --</option></select>"
};
});
HTML:
<body ng-controller="MainCtrl">
<p>Hello {{ model.person.name}}!</p>
<form name='myForm'>
<label for='orderselection'>Say hello to: </label>
<selectformfield label="Select Orders" id="id_1" name="orderselection"
selectedval="model.person" options="model.people"></selectformfield>
<p ng-class='{valid: myForm.$valid, invalid: myForm.$invalid }'>The form is valid: {{ myForm.$valid }}</p>
<p ng-class='{valid: myForm.orderselection.$valid, invalid: myForm.orderselection.$invalid }'>The people select field is valid: {{ myForm.orderselection.$valid }}</p>
</form>
</body>
CSS:
.valid {
color: green;
}
.invalid {
color: red;
}

Accessing the DOM directly in $scope is bad practice and should be avoided at all costs. In MVC structure like angular, instead of accessing the DOM (view) to get its state and data, access the models instead ($scope). In your case, you're binding the name of your directive to the orderselection property of your parent scope. Also notice that a form is an instance of FormController. The form instance can optionally be published into the scope using the name attribute. In your case, you create a new property on the parent scope.
You could try accessing the name like this if you're in your parent scope:
console.log( $scope.myform.orderselection );
Or if you're in your directive scope.
console.log( $scope.name);
Because your scope directive name property binds to your parent scope orderselection property, you need to assign a value to your parent scope property or it will be undefined. Like this:
$scope.myform.orderselection = "orderselection ";
If you need to do validation inside your directive, since you already bind the name attribute with the orderselection. You could do it like this:
template : "<select class='form-control' ng-attr-name='{{name}}' ng-disabled='[name].$invalid' .../>

Related

How do I dynamically access a property inside an angularjs directive?

I have a directive when given an array it creates check boxes for that list of items using the Name as the label for the checkbox.
Html
<div>
<a style="float:right; margin-bottom: 5px;" ng-click="selectNone()" href="#">Select None</a>
<a style="float:right; margin-bottom: 5px;margin-left: 10px" ng-click="selectAll()" href="#">Select All</a>
<div class="cleared"></div>
<div class="form-input form-list">
<label ng-repeat="item in valuelist | orderBy:'Name'">
<input type="checkbox" checklist-model="model" checklist-value="item" /> {{item.Name}}<br />
</label>
</div>
JS
'use strict';
growllApp.directive('checkboxlist', [function () {
return {
restrict: 'E',
templateUrl: 'scripts/modules/checkboxlist.template.html',
controller: 'checkboxlistController',
scope: {
model: "=",
value: "=",
valuelist: "="
}
}
}]);
growllApp.controller('checkboxlistController', ['$scope', '$routeParams', '$location', '$cookieStore', function ($scope, $routeParams, $location, $cookieStore) {
$scope.selectAll = function () {
$scope.model = angular.copy($scope.valuelist);
};
$scope.selectNone = function () {
$scope.model = [];
};
}]);
Using the directive
<checkboxlist ng-show="applyToProducts" model="coupon.Products" value="Name" valuelist="productsList"></checkboxlist>
All this works fine in most scenarios. However, I have a scenario where the array I'm passing in has objects in it that do not have the property "Name" but instead have the property "Title".
How do I use the attribute I have in the directive called "value" for the part that determines the label.
So in the directive html instead of saying {{item.Name}} how can I specify {{item.Title}} by passing in that the property name is title in certain cases?
You can change the 'value' attribute of your directive to '#' (it means the value will be sent as a string to your directive. You can read more in this article )
scope: {
model: "=",
value: "#",
valuelist: "="
}
and to get the object's property, you can do:
{{item[value]}}
In addition to the answer suggesting a ternary operator you could simplify that to be something like
{{ item.Name || item.Title }}
and you could have as many alternatives as you need.
However, a more robust approach I think would be to do like someone else suggested and say
scope : {
...
key:'#'
...
}
where key is the name of the property and the access it like
{{ item[key] }}
You could try using a ternary operator:
{{ item.Name ? item.Name : item.Title }}
You would probably need to use a custom comparator for the orderBy then.

AngularJS: ngChange not called from directive

Brief intro to my problem
I have a directive that dynamically shows a list of checkboxes. It has a parameter called options that should be an array like the following, in order to show the list of checkboxes correctly. For example:
var options = [
{
id: 1,
label: 'option #1'
},
{
id: 2,
label: 'option #2'
},
{
id: 3,
label: 'option #3'
}
];
So, by passing this array to my directive, a group of three checkboxes would be shown.
Also, the directive requires ngModel that will have the result of checking/unchecking the checkboxes (this object is always passed initialized). For example:
var result = {
"1": true,
"2": true,
"3": false
};
This case means that the first and second checkboxes (options with id=1 and id=2) are checked and the third (option with id=3) is unchecked.
My directive
template.html
<div ng-repeat="option in options track by $index">
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="result[option.id]">
{{ ::option.label }}
</label>
</div>
</div>
directive.js
angular
.module('myApp')
.directive('myDirective', myDirective);
function myDirective() {
var directive = {
templateUrl: 'template.html',
restrict: 'E',
require: 'ngModel',
scope: {
options: '='
},
link: linkFunc
};
return directive;
function linkFunc(scope, element, attrs, ngModel) {
scope.result;
ngModel.$render = setResult;
function setResult() {
scope.result = ngModel.$viewValue;
};
};
};
What I want to achieve
Wherever I use my directive, I want to be able to trigger a function whenever the ngModel changes. Of course, I would like to achieve this using ngChange. So far I have the following:
<my-directive
name="myName"
options="ctrlVM.options"
ng-model="ctrlVM.result"
ng-change="ctrlVM.selectionChanged()">
</my-directive>
but the .selectionChanged() function is not triggered whenever the model changes. Anyone has any idea why this is not working as I am expecting it to work?
First thing first, please try to provide jsfiddle, codepen etc code snippet link so that it will be easy for others to answer your question.
The problem in your case is that you are never updating the ctrlVM.result object as you are passing the object's reference and that reference never change even if you manually update the model by calling ngModel.$setViewValue().
To solve the problem, just update the model by manually calling ngModel.$setViewValue() and pass in the new Object so that the reference changes and that will trigger the ngChange directives logic.
I've added the logic to do that and it will successfully trigger the change. Look at the code below:
angular
.module('myApp', [])
.directive('myDirective', myDirective)
.controller('MyController', function($timeout) {
var vm = this;
vm.options = [{
id: 1,
label: 'option #1'
}, {
id: 2,
label: 'option #2'
}, {
id: 3,
label: 'option #3'
}];
vm.result = {
"1": true,
"2": true,
"3": false
};
vm.selectionChanged = function() {
vm.isChanged = true;
$timeout(function() {
vm.isChanged = false;
}, 500)
}
});
function myDirective() {
var directive = {
templateUrl: 'template.html',
restrict: 'E',
require: 'ngModel',
scope: {
options: '='
},
link: linkFunc
};
return directive;
function linkFunc(scope, element, attrs, ngModel) {
scope.result;
ngModel.$render = setResult;
function setResult() {
scope.result = ngModel.$viewValue;
};
scope.updateValue = function(val) {
ngModel.$setViewValue(Object.assign({}, val))
}
};
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular.min.js"></script>
<div ng-app="myApp">
<script type="text/ng-template" id="template.html">
<div ng-repeat="option in options track by $index">
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="result[option.id]" ng-click="updateValue(result)">
{{ ::option.label }}
</label>
</div>
</div>
</script>
<div ng-controller="MyController as ctrlVM">
<my-directive name="myName" options="ctrlVM.options" ng-model="ctrlVM.result" ng-change="ctrlVM.selectionChanged()">
</my-directive>
<div> Data: {{ctrlVM.result}} </div>
<div> isChanged: {{ctrlVM.isChanged}} </div>
</div>
</div>
#Gaurav correctly identified the problem (ng-change is never called because the object reference does not change). Here is a simpler solution that doesn't require manually cloning into the controller's model:
Add a binding for the ng-change attribute:
scope: {
options: '=',
ngChange: '&' // Add this, creates binding to `ctrlVM.selectionChanged()`
}
Add an ng-change to your checkbox template:
<input type="checkbox"
ng-model="result[option.id]" ng-change="ngChange()">
Now, when any checkbox changes it will automatically call the outer ng-change function without the intermediate step of cloning into the model.

AngularJS: Compiling the output of a directive

i have some kind of legacy angularjs code which creates a dynamic table using a directive where the controller can overwrite the behavior of the table (on how to display the data)
It consists of the following setup (simplified):
Directive's controller
.directive('datatable', [function () {
return {
scope: {
items: '=',
tablemetadata: '=',
processors: '=?'
},
controller: ...
$scope.processField = function processField(item, data){
if($scope.processors === undefined){return;}
for(var i = 0; i < $scope.processors.length; i++){
if($scope.processors[i].field===field){
var newData = $scope.processors[i].processor(item, data);
return $sce.trustAsHtml(newData);
}
}
return data;
};
...
Directive's Template
<tr ng-repeat="item in items">
<td ng-repeat="column in tableMetadata.columns" ng-bind-html="processField(column.field, $eval('item.'+column.field))"></td>
</tr>
Controller
$scope.myItems = [{id: 2, otherProperty: "text"}];
$scope.tableMetadata = {
columns: [
{field: 'id', headerKey: 'object id'},
{field: 'otherProperty', headerKey: 'some data'},
]
};
$scope.tableProcessors = [
{field: 'id', processor: function(entry, data){ //data = content of object.id
var retVal = "<a ng-click='alert(" + data + ");'>click me</a>";
return retVal;
}}
];
Controller's view
<datatable items="myItems" tablemetadata="tableMetadata" processors="tableProcessors"></datatable>
I need to generate buttons (or other html-elements) for some specific properties, like a link (like shown above).
The Button is displayed but the ng-click handler is not working. This makes sense since it wasn't compiled to the scope.
How do I correctly compile the new element and add it to the table?
In your link method in the directive you have to use
elem.append( $compile(html)(scope) );
As for separating the concerns cleanly, I would make each <td> its own directive that inherits what you are currently concatenating as a string in its isolated scope properties. Instead of
var retVal = "<a ng-click='alert(" + data + ");'>click me</a>";
<tr ng-repeat="item in items">
<td ng-repeat="column in tableMetadata.columns" ng-bind-html="processField(column.field, $eval('item.'+column.field))"></td>
</tr>
use something like:
<tr ng-repeat="item in items">
<table-item ng-repeat="..." process-field="item"></table-item>
</tr>
/** directive compiles dynamically */
scope: {
processField: '='
},
link: function(scope, elem, attr, ctrl) {
var template = `<a ng-click="${ctrl.processField}"></a>`;
elem.append( $compile(template)(scope) );
}
A simple solution can be to not use an isolated scope.
Change your scope from scope: { ... } to scope: true and use $scope.$eval to evaluate your attributes.
Another solution (most elegant) can be to use angularjs transclusion (see here). But this solution ask to modify your dom representation of your directive.

AngularJs two-way-data-binding not working for custome directive input elements

I have created the custome directive for creating dynamic form elements based on backend data .I have created directive and template .Please find below my directive ,template and json string
angular.module('publicApp').directive('buildInput', function() {
return {
require: "ngModel",
restrict:"E",
scope:{
build:"=",
},
templateUrl:"views/directives/buildInput.html",
}
});
My template is
<label>{{build.reg_label}}
<span class="color-red">*</span>
</label>
<div ng-if="build.reg_type=='text'">
<input class="form-control" type="text" required="true" ng-model="buildData[build.id]">
</div>
<div ng-if="build.reg_type=='select'">
<select ng-options="reg_option.value as reg_option.label for reg_option in build.reg_options" ng-model="buildData[build.id]">
<option value="" ng-show="$first">-- {{build.reg_label}} --</option>
</select>
</div
>
view file is
<div class="form-group" ng-repeat="build in builds">
<build-input build="build" ></build-input>
</div>
My json string is
[
{
"id":"1",
"reg_label":"What is Your Name",
"reg_type":"text",
},
{
"id":"2",
"reg_label":"Select Gender",
"reg_options":[
{
"label":"Male",
"value":"M"
},
{
"label":"Female",
"value":"F"
}
],
"reg_type":"select",
}
]
why my modal not binding with scope variable buildData ?
Your directive only knows build variable since your (isolated) scope is defined like this:
scope:{
build:"=",
},
Sounds like your buildData variable is defined on the controller where builds is defined, so you should extend your scope to look like:
scope:{
build:"=",
buildData:"="
},
And your HTML directive declaration:
<build-input build="build" build-data="buildData" ></build-input>

angular: how to handle code provided by attributes

As an example of what I want, consider the following example
<select ng-options="option.text for option in options"></select>
In my directive I want to use something similar to ngOptions, because I need to create a list
For example, assume I have a directive barFoo, called as follows:
<bar-foo options="options"></bar-foo>
with a template/html as follows:
<ol>
<li ng-repeat="option in options" ng-bind="option.text"></li>
</ol>
What is needed to change all this into a call like
<bar-foo options="option.text for option in options"></bar-foo>
The main reason I need this is because I don't know the property name holding the label text (in this case it is text)
I provided a fiddle and see whether this helps. Instead of passing in "options.text for option in options", I set it up such that you pass the "options" array and then the field you want. I assumed the field will be set up as a variable; if it hard-coded, then you can just do field='someFieldName' instead.
http://jsfiddle.net/y376K/1/
HTML
<body ng-app='testApp'>
<div ng-controller='TestCtrl'>
<bar-foo options='options' field='{{optionsField}}'></bar-foo>
</div>
</body>
JS
angular.module('testApp', [])
.controller('TestCtrl', function($scope) {
$scope.options = [
{
text: 'Node.js rocks my socks',
language: 'Node.js',
},
{
text: 'Angular is hot',
language: 'Angular.js',
},
{
text: 'Backbone.js is mmmm',
language: 'Backbone.js',
}
];
$scope.optionsField = 'text';
})
.directive('barFoo', function() {
return {
restrict: 'E',
scope: {
options: '=',
field: '#'
},
template: '<ol><li ng-repeat="option in options" ng-bind="option[field]"></li>'
};
})
You can do this by parsing the attribute. The other solution would be to pass it as two attributes (see the other answer)
You should probably use a regexp for this, but I coded this quickly:
app.directive('barFoo',function($parse) {
return {
restrict: 'E',
scope: {},
templateUrl: "template.html",
link: function(scope,element,attrs) {
var splitOptions = attrs.options.split(' for ');
scope.fieldName = splitOptions[0].split('.')[1];
var repeatExp = splitOptions[1];
scope.valueName = repeatExp.split(' in ')[0];
var collectionName = repeatExp.split(' in ')[1];
scope.values = $parse(collectionName)(scope.$parent);
}
};
});
See this plnkr

Categories