AngularJS: Compiling the output of a directive - javascript

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.

Related

Change elements in a directive template based on scope data

I have a directive nested within an ng-repeat. The ng-repeat item is passed to the directive. I am attempting to generate a directive template (for testing) or templateUrl with variable elements based on a key/value in the item passed to the directive. Essentially, if item.number > 50 make the button red else make it blue.
I may be using the wrong tool to solve the problem. The goal is to use something like this to change Bootstrap tags. For instance the logic:
if item.number > 50:
class="btn btn-danger"
else:
class="btn btn-success"
If possible I'm trying to solve this with using templateUrl: as I'd like the button to launch a bootstrap modal and that's a lot to fit into the basic template option. It's much cleaner to pass the template individual scope variables.
Here is a JSFiddle that tries to describe the problem.
html
<div ng-controller="TableCtrl">
<table>
<thead>
<tr>
<th>#</th>
<th>Button</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in buttons">
<td>{{item.id}}</td>
<td new-button item='item'></td>
</tr>
</tbody>
</table>
</div>
app.js
var myApp = angular.module('myApp', []);
function TableCtrl($scope) {
$scope.buttons = {
button1: {
id: 1,
number: '10',
},
button2: {
id: 2,
munber: '85',
}
};
};
myApp.directive('newButton', function() {
return {
restrict: 'A',
replace: true,
scope: {
item: '=',
},
link: function(elem, attrs, scope) {
// This is most likely not the right location for this
/*if (item.number > 50) {
button.color = red
}, else {
button.color = blue
}; */
},
template: '<td><button type="button">{{button.color}}</button></td>'
}
});
Perhaps you can use an ng-class for this:
<button ng-class="{
'btn-danger': item.number > 50,
'btn-success': item.number <= 50
}"></button>
See https://docs.angularjs.org/api/ng/directive/ngClass
If you really need a custom directive you could try using it like this
link: function(scope,elem,attrs) {
var item=scope.item;
if (item.number > 50) {
elem.addClass("btn-danger");
} else {
elem.addClass("btn-success");
}
}
But I think that for what you're trying to achieve it's better to use the ngClass directive as follows:
<button type="button" item="item" class="btn" ng-class="item.number > 50?'btn-danger':'btn-success'"></button>
Looking at your example code, there are a few points to note:
Typo in button 2's 'munber' property.
The link function doesn't use dependency injection, so the order of the arguments does matter. Scope needs to be moved first.
Your commented out bit of code is close to working, but you need to address the variables as properties of scope - item is on scope, and the button object you are creating needs to be created on scope in order to be addressed as 'button.' from your view template.
This works (it would be better, as others have said, to use ng-class, rather than class plus moustache syntax, but I wanted to stay as close to your code sample as possible):
HTML
<div ng-controller="TableCtrl">
<table>
<thead>
<tr>
<th>#</th>
<th>Button</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in buttons">
<td>{{item.id}}</td>
<td new-button item='item'></td>
</tr>
</tbody>
</table>
</div>
JS
var myApp = angular.module('myApp', []);
function TableCtrl($scope) {
$scope.buttons = {
button1: {
id: 1,
number: '10',
},
button2: {
id: 2,
number: '85',
}
};
};
myApp.directive('newButton', function() {
return {
restrict: 'A',
replace: true,
scope: {
item: '=',
},
link: function(scope, elem, attrs) {
scope.button = {};
if (scope.item.number > 50) {
scope.button.class = 'btn btn-danger';
} else {
scope.button.class = 'btn btn-success';
};
},
template: '<td><button type="button" class="{{button.class}}">Press Me?</button></td>'
}
});
CSS
.btn-danger {
background-color: red;
}
.btn-success {
background-color: green;
}
Modified JSFiddle

AngularJS : directive does not update scope after $http response in parent scope

I got this directive:
.directive('studentTable', [function() {
return {
restrict: 'A',
replace: true,
scope: {
students: "=",
collapsedTableRows: "="
},
templateUrl: 'partials/studentTable.html',
link: function(scope, elem, attrs) {
...
}
}
}
Template:
<table class="table">
<thead>
<tr>
<th><b>Name</b></th>
<th><b>Surname</b></th>
<th><b>Group</b></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="student in students track by $index">
<td>{{ student.name }}</td>
<td>{{ student.surname }}</td>
<td>{{ student.group }}</td>
</tr>
</tbody>
</table>
Use directive in my html like this:
<div student-table students="students"
collapsedTableRows="collapsedTableRows"></div>
And the parent controller:
.controller('SchoolController', ['$scope', 'User', function($scope, User){
$scope.students = [];
$scope.collapsedTableRows = [];
$scope.search = function(value) {
if(value) {
var orgId = $state.params.id;
var search = User.searchByOrg(orgId, value);
search.success(function (data) {
$scope.students = data;
$scope.collapsedTableRows = [];
_(data).forEach(function () {
$scope.collapsedTableRows.push(true);
});
});
}
}
}])
Now at the beginnig, the table is empty, because no users in students array. After I click search, and get list of students object, I put them to scope variable, but the directive does not update, neither it find change in model (scope.$watch('students',...). What am I missing?
P.S. If I simulate the data using $httpBackend, directive works as it should.
Please make sure that data object returning array of student because somtimes you have to use data.data that simple demo should helps you:
http://plnkr.co/edit/UMHfzD4oSCv27PnD6Y6v?p=preview
$http.get('studen.json').then(function(students) {
$scope.students = students.data; //<-students.data here
},
function(msg) {
console.log(msg)
})
You should try changing the controller this way
...
$scope.$apply(function() {
$scope.students = data;
})
...
This will start a digest loop, if it's not already in progress.
Another form that will do almost the same thing is this:
...
$scope.students = data;
$scope.$digest()
...
PS:
The first method is just a wrapper that execute a $rootScope.$digest() after evaluating the function, considering that a $digest evaluates the current scope and all it's children calling it on the $rootScope is pretty heavy.
So the second method should be preferred if it works.

Angularjs dynamic directive inside ngrepeat

Look at example:
$scope.fields = [{
name: 'Email',
dir : "abc"
}, {
name: 'List',
dir : "ddd"
}];
app.directive('abc', function () {});
app.directive('ddd', function () {});
<table class="table table-hover">
<tr ng-repeat="p in fields">
<input {{p.dir}} ngmodel="p" />
</tr>
</table>
How can I write code, that p.dir will dynamically convert to a directive ?
My example: hhttp://jsbin.com/vejib/1/edit
Try this directive:
app.directive('dynamicDirective',function($compile){
return {
restrict: 'A',
replace: false,
terminal: true,
priority: 1000,
link:function(scope,element,attrs){
element.attr(scope.$eval(attrs.dynamicDirective),"");//add dynamic directive
element.removeAttr("dynamic-directive"); //remove the attribute to avoid indefinite loop
element.removeAttr("data-dynamic-directive");
$compile(element)(scope);
}
};
});
Use it:
<table class="table table-hover">
<tr ng-repeat="p in people">
<td dynamic-directive="p.dir" blah="p"></td>
</tr>
</table>
DEMO
For more information on how this directive works and why we have to add terminal:true and priority: 1000. Check out Add directives from directive in AngularJS
You could put this:
<input {{p.dir}} ngmodel="p" />
also in a directive. You could construct this HTML string in JavaScript and attach it to the DOM. And you would also need to compile the resulting element using the $compile service, so that the dynamic directives will be compiled.
Some dummy sample code (not tested, but should look something like this):
app.directive('dynamicInput', function($compile){
return {
link: function(scope, element){
var htmlString = '<input ' + scope.field.dir + ' ng-model="p"/>';
element.replaceWith(htmlString);
$compile(angular.element(element))(scope);
}
}
});
More info here.

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

angularjs directive binding name attribute to template element

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' .../>

Categories