AngularJS transcluded nested directive - angularjs

i'm trying to create an angular directive like this :
<radio-button-group group="myGroup" label="My Group" data-model="myModel.attribute">
<radio-button id="value_1" value="1">Value 1</radio-button>
<radio-button id="value_2" value="2">Value 2</radio-button>
</radio-button-group>
The main goals are reuse and avoid repeating ng-model on every <input type="radio" ng-model="myModel.attribute"> and code duplication in general.
I've written the followings:
formjs.js
var formjsModule = angular.module('hp.formjs', []);
// Directive
formjsModule.directive('radioButtonGroup', function () {
return {
restrict: 'E',
transclude: true,
scope: {
dataModel: '#',
group: '#',
label: '#?'
},
templateUrl: function (elem, attr) {
return 'formjs/radio/radio-group-tpl.html';
}
}
}).directive('radioButton', function () {
return {
restrict: 'E',
require: ['^radioButtonGroup'],
transclude: true,
scope: {
value: '#',
dataModel:'#',
group: '#'
},
templateUrl: function (elem, attr) {
return 'formjs/radio/radio-item-tpl.html';
}
}
});
formjs/radio/radio-item-tpl.html
<input type="radio" name="{{group}}" id="{{group+'_'+id}}" ng-value="value" ng-model="dataModel"/>
formjs/radio/radio-group-tpl.html
{{label}} m: {{dataModel}}
<div class="col-sm-10" id="g_{{group}}" ng-transclude>
</div>
</div>
I'm aware of transcluded scope, but i don't see the elegant way to do this binding.
Help, please!

It has to be directive? Wouldn't this be sufficient?
// HTML template
<div ng-repeat="option in options">
<input type="radio" ng-model="$parent.selected" ng-value="option"> {{ option }}
</div>
...
// Controller
$scope.selected = null;
$scope.options = ['Radio one', 'Radio two', 'Radio three'];
Of course, if you insist, you CAN make it a directive.
Usage in HTML, where options is array like above.
<radio-group group-name="My group"
ng-model="selected"
options="options"></radio-group>
Actual directive
app.directive('radioGroup', function () {
return {
restrict: 'E',
scope: {
groupName: '#',
options: '=',
ngModel: '='
},
template: '<legend>{{ groupName }}</legend>' +
'<div ng-repeat="option in options">' +
' <input type="radio" ' +
' ng-model="$parent.ngModel" ng-value="option"> {{ option }}' +
'</div>'
};
});

Related

Two way binding with controller in directive with isolated scope

Im developing a directive, where when the user will click UPDATE, a field for EmployeeId will show up, and that employeeId field on the directive should be bound directly to the controller when changes. If i isolate the scope it doesn't change the controller employeeId, if i dont, than all directives gets toggled.
Plunker: https://plnkr.co/edit/aq06WnfniN51iR7ZtFLc?p=preview
code:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.employeeId = 'Josh'
$scope.employeePin = '1234'
$scope.updateFields = function() {
alert(1);
}
});
app.directive('crudEmp', function($parse) {
var html = '<div class="form-inline">\
<input type="text" class="form-control"\
placeholder="Employe ID"\
ng-model="employeeId"\
required\
ng-show="isEditing"/>\
<input type="password" class="form-control"\
placeholder="Employe PIN"\
ng-model="employeePin"\
ng-show="user.AdminSecurity && isEditing"\
ng-required="user.AdminSecurity"\
disabled/>\
<div class="col-md-12 text-right noPadding pt10 operationButtons" id="opBtn">\
<div class="col-md-12 noPadding" ng-hide="isEditing">\
<button class="btn btn-info" ng-click="toggleEditing()">Edit</button>\
</div>\
<div class="col-md-12 text-right noPadding" ng-hide="!isEditing">\
<button class="btn btn-success mr80 updateBtn" ng-click="ctrl.onClick(); toggleEditing()" ng-disabled="ctrl.valid">Update</button>\
<button class="btn btn-warning" ng-click="toggleEditing()">Cancel</button>\
</div>\
</div>';
return {
restrict: 'E',
template: html,
replace: true,
bindToController: {
employeeId: '=',
employeePin: '=',
valid: '=',
onClick: '&',
},
// scope: true, // When i make the scope true, it doesnt change the EmployeeId defined in the controller
link: function(scope, element, attr){
scope.onClick = attr.onClick;
scope.toggleEditing = function() {
scope.isEditing = !scope.isEditing;
if(scope.isEditing){
element.closest('form').find(':input:not(.operationButtons button)').attr('disabled', false);
var getAllInputs = element.closest('form').find(':input').filter(':visible:enabled');
if(getAllInputs[0]){
getAllInputs[0].focus();
}
}else{
element.closest('form').find(':input:not(.operationButtons button)').attr('disabled', true);
}
};
},
controllerAs: 'ctrl',
controller: function($scope) {
var ctrl = this;
ctrl.onClick = function() {
$scope.$eval($scope.onClick);
};
},
};
});
If I understand you currently it would be solved by changing:
...
restrict: 'E',
template: html,
replace: true,
bindToController: {
employeeId: '=',
employeePin: '=',
valid: '=',
onClick: '&',
},
// scope: true, // When i make the scope true, it doesnt change
To:
...
restrict: 'E',
template: html,
replace: true,
scope: {
employeeId: '=',
employeePin: '=',
valid: '=',
onClick: '&',
},
...
You can verify the behaviour in my plunker: https://plnkr.co/edit/Xia8BhlfFJ6mqgLjaoOp?p=preview

Concatenating directive attribute to template?

Is there a possible way to use the value of an attribute and use it to concatenate in the directive template
<text-input txt-info="textName" ng-model="pi.medRecNumber"> </text-input>
hcare.directive('textInput', function() {
return {
restrict: 'AE', //attribute or element
scope: {
bindedModel: "=ngModel", // Element Model Data
txtInfo:'#', // Serve as name and id attribute value
},
template:
'<div class="form-group tcell-220" data-ng-class="{true: \'has-error\'}[submitted && formHcare.'+txtInfo+'.$invalid]">'+
'<div class="fg-line">' +
'<input id="'+txtInfo+'" name="'+txtInfo+'" data-ng-model="bindedModel" type="text" class="input-h25 form-control" placeholder="M#####-#" >'+
'</div>'+
'<small class="help-block" data-ng-show="submitted && formHcare.'+txtInfo+'.$error.required">Field Required</small>'+
'</div>',
replace: true,
require: '?ngModel',
link: function($scope, elem){
// LOGIC HERE
}
};
});
I think I got it working..
The thing with directives is that you have the same scope as the parent so you don't really need the 'bindedModel' variable and you can approach the txtInfo attribute with {{}} as we always do in angular.
Here is your code with my fixes to make it working:
hcare.directive('textInput', function() {
return {
restrict: 'AE', //attribute or element
replace: true,
scope: {
//bindedModel: "=ngModel", // Element Model Data
txtInfo:'#', // Serve as name and id attribute value
},
template:
'<div class="form-group tcell-220" data-ng-class="{true: \'has-error\'}[submitted && formHcare.{{txtInfo}}.$invalid]">'+
'<div class="fg-line">' +
'<input id="{{txtInfo}}" name="{{txtInfo}}" data-ng-model="pi.medRecNumber" type="text" class="input-h25 form-control" placeholder="M#####-#" >'+
'</div>'+
'<small class="help-block" data-ng-show="submitted && formHcare.{{txtInfo}}.$error.required">Field Required</small>'+
'</div>',
//require: '?ngModel',
link: function($scope, elem){
// LOGIC HERE
}
};
});
don't forget to initialize the pi object in the controller for it to work:
hcare.controller('AppCtrl', function($scope) {
$scope.pi = {};
});
If you want to make sure it is working you should use the browser's inspect element option on this input field.

ng-repeat affects the wrong place

I have written a custom directive named fRadioButton in AngularJS. As far as I know, ngRepeat directive affects the tag that it's used with. However in my case, ngRepeat behaves strangely. Here are the details:
My directive template:
<label>
<input type="radio" value="fValue"/>
{{fName}}
</label>
My directive's JavaScript file:
directivesModule.directive('fRadioButton', function() {
return {
restrict: 'EA',
replace: true,
scope: {
fName: '#',
fValue: '='
},
transclude: false,
templateUrl: '/directives/f-radio-button.html'
};
});
I use the directive from any page as follows:
<f-radio-button ng-repeat="period in myCtrl.periods track by $index"
f-name="period.name" f-value="{{$index}}""></f-radio-button>
According to the ngRepeat, I expect the genereated HTML to be in the following format:
<label></label>
<label></label>
<label></label>
<label></label>
However, it is generated as follows:
<label>
<f-radio-button></f-radio-button>
<f-radio-button></f-radio-button>
<f-radio-button></f-radio-button>
<f-radio-button></f-radio-button>
</label>
How can I directly duplicate the label tags with ngRepeat? I have tried it with replace=false, but it didn't work either.
I guess that replace=true runs before the ng-repeat. Is there a way to run ng-repeat before the replace=true?
You can pass an array into your directive and render radio items inside:
.directive('fRadioButton', function() {
return {
restrict: 'EA',
replace: true,
scope: {
model: '=ngModel',
options: '=',
fName: '#',
fValue: '#'
},
template: function(element, attrs) {
return '<div> {{model}}' +
'<label ng-repeat="option in options">' +
' <input type="radio" ng-model="$parent.model" ng-value="{{ option.' + attrs.fValue + ' }}" name="' + attrs.name + '" />' +
' {{option.' + attrs.fName + '}}' +
'</label></div>'
}
};
});
Then you could use it like this:
<f-radio-button options="myCtrl.periods" ng-model="selected" f-name="name" f-value="id"></f-radio-button>
Here is a simple demo:
Demo: http://plnkr.co/edit/Xcvt46ljV58513saq7BC?p=info
ng-repeat repeats everything that is inside the element you attach it to
<div ng-repeat="period in myCtrl.periods track by $index">
<f-radio-button f-name="period.name" f-value="{{$index}}"></f-radio-button>
</div>
EDIT
Alternatively
myApp.directive('fRadioButtons', function() {
return {
restrict: 'EA',
replace: true,
scope: {
periods: "="
},
template: '<label ng-repeat="period in periods"><input type="radio" f-name="{{period.name}}" value="{{$index}}"/>{{period.name}}</label>'
};
});
function MyCtrl($scope) {
$scope.periods = [{name: "foo"}, {name: "bar"}, {name: "foobar"}];
}
http://jsfiddle.net/Lvc0u55v/3010/

Dynamically generate angular directive within a loop

I'm trying to dynamically compile and append a directive with a dynamic attribute in the directive; however, I keep getting an error.
Anyone know why?
http://plnkr.co/edit/wOb9UuFwXR0dCZx2e2xg?p=preview
angular.module('plunker', [])
.controller('MainCtrl', function($scope) {
var vm = this;
vm.foo = {
bar: 'world'
};
}).directive('addEditConfig', AddEditConfig)
.directive('inputElement', InputElement);
function AddEditConfig($compile) {
var directive = {
restrict: 'E',
scope: {
edit: '#',
config: '=',
display: '=',
parentController: '='
},
template: '<div id="dynamicForm"></div>',
controllerAs: 'vm',
bindToController: true,
controller: AddEditConfigController,
link: linkFunction
}
return directive;
function linkFunction(scope, element, attr, ctrl) {
var params = [{description: 'foo', value:'bar'}, {description: 'foo', value:'bar'}, {description: 'foo', value:'bar'}, {description: 'foo', value:'bar'}];
for (var i = 0; i < params.length; i++) {
angular.element(document.getElementById('dynamicForm')).append($compile('<input-element param="' + params[i] + '"></input-element>')(scope));
}
}
};
function AddEditConfigController() {
};
function InputElement() {
var directive = {
restrict: 'E',
scope: {
param: '='
},
controllerAs: 'vm',
bindToController: true,
template: '<div class="form-group"> <div class="col-md-8"> <input id="port" name="textinput" type="text" placeholder="{{vm.param.description}}" class="form-control input-md" ng-model="vm.param.value"/> </div></div>',
controller: InputElementController,
link: linkFunction
}
return directive;
function linkFunction(scope, el, attr, ctrl) {
}
};
function InputElementController() {
var vm = this;
}
The problem (at least one of them) is in this statement :
angular.element(document.getElementById('dynamicForm')).append($compile('<input-element param="' + params[i] + '"></input-element>')(scope));
Writing this param="' + params[i] + '" generates the following DOM param="[Object Object]".
So It will never work as expected.
One way to achieve this :
angular.module('plunker', [])
.controller('MainCtrl', function($scope) {
var vm = this;
vm.foo = {
bar: 'world'
};
}).directive('addEditConfig', AddEditConfig)
.directive('inputElement', InputElement);
function AddEditConfig($compile) {
var directive = {
restrict: 'E',
scope: {
edit: '#',
config: '=',
display: '=',
parentController: '='
},
template: '<div id="dynamicForm"><input-element param="param" ng-repeat="param in vm.params"></input-element></div>',
controllerAs: 'vm',
controller: AddEditConfigController
}
return directive;
};
function AddEditConfigController() {
this.params = [{description: 'foo', value:'bar'}, {description: 'foo1', value:'bar1'}, {description: 'foo2', value:'bar2'}, {description: 'foo3', value:''}];
};
function InputElement() {
var directive = {
restrict: 'E',
scope: {
param: '='
},
template: '<div class="form-group"> <div class="col-md-8"> <input id="port" name="textinput" type="text" placeholder="{{param.description}}" class="form-control input-md" ng-model="param.value"/> </div></div>'
}
return directive;
};
http://plnkr.co/edit/KdPvJdBsRwbjNsBCIvVh?p=preview

AngularJS - Different template in directive

I have a form based on twitter bootstrap, each field have it's own configuration
// controller (the template shows this in ng-repeat
$scope.fields = [{name:"f1", label:"Field 1", with_button: false},
{name:"f2", label:"Field 2", with_button: true}]
I'm trying to make a "conditional directive" that customize the template according to "field.with_button"
// Without button
<div class="controls">
<input type="text" id="i_{{field.name}}">
</div>
// With button
<div class="controls">
<div class="input-append">
<input type="text" id="i_{{field.name}}">
<span class="add-on">bt</span>
</div>
</div>
I searched a lot and didn't find any solution, I tried to create only one div and put contents inside with a compiler function but it didn't parse, and if I call $apply it crashes.
How could I make this directive?
wrong My last try:
angular.module('mymodule',[]).directive('ssField', function() {
return {
transclude:false,
scope: {
field: '='
},
restrict: 'E',
replace:true,
template: '<div class="controls">{{innerContent}}</div>',
controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
$scope.$eval('$scope.innerContent = \'<input type="text" id="input_{{field.name}}" placeholder="{{field.name}}" class="input-xlarge">\'');
}]
};
});
//<ss-field field="{{field}}"></ss-field>
You can use the $http and $compile services to do what you're after.
http://plnkr.co/edit/Xt9khe?p=preview
This plnkr should demostrate what needs to be done, but basically:
Use $http to load the template depending on the condition.
Compile the loaded template against the current scope with $compile.
angular.module('mymodule',[]).directive('ssField', ['$http', '$compile', function($http, $compile) {
return {
transclude:false,
scope: {
field: '='
},
restrict: 'E',
replace:true,
template: '<div class="controls"></div>',
link: function(scope, element, attrs) {
var template;
var withButtonTmpl = 'with_button.html';
var withoutButtonTmpl = 'without_button.html';
if (scope.field.with_button) {
$http.get(withButtonTmpl).then(function(tmpl) {
template = $compile(tmpl.data)(scope);
element.append(template);
});
} else {
$http.get(withoutButtonTmpl).then(function(tmpl) {
template = $compile(tmpl.data)(scope);
element.append(template);
});
}
}
};
}]);
You can change the directive to be more robust so the URLs aren't directly embedded in the directive for re-usability, etc., but the concept should be similar.
Just to further expand on Cuing Vo's answer here is something similar to what I use(without using external partials and additional $http calls):
http://jsfiddle.net/LvUdQ/
myApp.directive('myDirective',['$compile', function($compile) {
return {
restrict: 'E',
template: '<hr/>',
link: function (scope, element, attrs, ngModelCtrl) {
var template = {
'templ1':'<div>Template 1</div>',
'templ2':'<div>Template 2</div>',
'default':'<div>Template Default</div>'
};
var templateObj;
if(attrs.templateName){
templateObj = $compile(template[attrs.templateName])(scope);
}else{
templateObj = $compile(template['default'])(scope);
}
element.append(templateObj);
}
};
}]);
However Im not quite sure its by the bible from performance perspective.
In AngularJS, directly manipulate the DOM must only be a last resort solution. Here, you can simply use the ngSwitch directive :
angular.module('mymodule',[]).directive('ssField', function() {
return {
transclude:false,
scope: {
field: '='
},
restrict: 'E',
replace:true,
template:
'<div class="controls" data-ng-switch="field.with_button">' +
'<input type="text" id="i_{{field.name}}" data-ng-switch-when="false">' +
'<div class="input-append" data-ng-switch-default>' +
'<input type="text" id="i_{{field.name}}">' +
'<span class="add-on">bt</span>' +
'</div>' +
'</div>',
};
});

Resources