Supposedly, I have a directive with scope
scope: {
columns: '=',
}
How can I achieve this?
<my-directive columns="[{ field:'id', displayName: 'ID' },
{ field:'title', displayName: 'Title' },
{ field:'address', displayName: 'Address' },
{ field:'city', displayName: 'City' }]" />
Apparently Angular's compiler have problems with figuring out that it's an array, although it doesn't have a problem with standard JS objects passed this way - {}. Is there any nifty way to do this? Does the fact that it works with objects is just a coincidence?
Please keep in mind that I know, that I can set this as $scope parameter in Controller and pass just the parameter name from $scope. But I would really like to learn if it's possible to do it straight from HTML.
--
Update with full code:
This is how it is used in template
<es-paged-data-grid columns="[
{ field:'id', displayName: 'ID' },
{ field:'title', displayName: 'Title' },
{ field:'address', displayName: 'Address' },
{ field:'city', displayName: 'City' }
]">
</es-paged-data-grid>
This is the directive:
app.directive('esPagedDataGrid', function () {
var definition = {
restrict: "E",
replace: false,
transclude: true,
scope: {
columns: '=',
},
templateUrl: 'application/directive/pagedDataGrid/PagedDataGrid.html',
controller: ['$scope', '$element', '$attrs', '$transclude', function($scope, $element, $attrs, $transclude) {
var dataGridOptions = {};
if ($scope.columns) {
dataGridOptions.columnDefs = $scope.columns;
}
$scope.dataGridOptions = dataGridOptions;
}]
};
return definition;
});
This is the directive's template:
<div ng-grid="dataGridOptions">
</div>
You have mentioned in one of your comments that your directive throws a nonassign error. A nonassign error occurs when a directive attempts to modify an isolate scope defined using the = notation wherein the assigned attribute is an expression that is not two-way data bound(not a scope variable).
Probably somewhere in your directive, you may have attempted to change scope.columns directly such as, scope.columns = []; or any other scope property that is assigned via the = notation. Try removing that and it might solve your problem.
UPDATE:
trying changing this:
dataGridOptions.columnDefs = $scope.columns;
to this:
dataGridOptions.columnDefs = angular.copy($scope.columns);
I suspect the ng-grid directive is probably doing some manipulation on the columnDefs options, since columnDefs has a direct reference towards $scope.columns property then any manipulation performed in the columnDefs options would likely affect $scope.columns.
Well AFAIK i guess a way to initialize data in DOM is using the ng-init directive.
So the directive can look like,
app.directive('testd', function() {
return {
scope: {
options: "=ngInit"
},
link: function(scope, e, a) {
console.log('test', scope.options);
},
template: 'test'
};
});
And you can supply the array via ng-init,
<div testd ng-init="[{ field:'id', displayName: 'ID' },
{ field:'title', displayName: 'Title' },
{ field:'address', displayName: 'Address' },
{ field:'city', displayName: 'City' }]"></div>
Sample Demo: http://plnkr.co/edit/MwOPLm16KTOr2Q3rY6LK?p=preview
Well one more way would be to pass it via an attribute. Though it will be taken as string, you can use eval() to convert it to object and assign it to a scope variable. Plnkr is also updated for the same. Included columns:"#" and use eval(attrs.columns) to convert it to array
Related
I need to render dynamic template from database and also need to bind the variables to expressions.
My response JSON will look like this,
[{
"htmlTemplate": "<div>{{name}}</div><div>{{age}}</div>",
"bindData": {
"name": "safeer",
"age" : "25"
}
}, {
"htmlTemplate": "<span>{{name}}</span><div>{{address}}</div>",
"bindData": {
"name": "john",
"address":"qwerty"
}
}, {
"htmlTemplate": "<h4>{{name}}</h4><h2>{{country}}</h2>",
"bindData": {
"name": "james",
"country":"India",
"state" : "Kerala"
}
}]
I have created a directive as per the answer to the question Compiling dynamic HTML strings from database
In html, demo.html
<div dynamic="html"></div>
In directive, directive.js
var app = angular.module('app', []);
app.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
link: function (scope, ele, attrs) {
scope.$watch(attrs.dynamic, function(html) {
ele.html(html);
$compile(ele.contents())(scope);
});
}
};
});
It will render html template and replace variable with $scope variable name finely.
But I need to render each htmlTemplate with its corresponding bindData. That is render each template with isolated scope data. Need to generalize directive.
Any help appreciated! Thanks in advance.
When you receive the JSON data you can use angular.fromJSON to decode the json string into the array (unless you are using the $http.get() which already does that for you)...
//request the JSON from server
jsonString = SomeFactory.fetchDataFromServer();
$scope.dataArray = angular.fromJson(jsonString);
...and then use the ngRepeat to create multiple elements:
<div ng-repeat="element in dataArray" dynamic="element"></div>
Modify your directive like this:
app.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
link: function (scope, ele, attrs) {
scope.bindData = {};
scope.$watch(attrs.dynamic, function(dynamic) {
console.log('Watch called');
ele.html(dynamic.htmlTemplate); //here's your htmlTemplate being compiled
$compile(ele.contents())(scope);
scope.bindData = dynamic.bindData; //here's your bound data ifyou need the reference
scope.name = dynamic.bindData.name; //bound data property name
}, true); //deep watch
}
};
});
Edit: Or you could simply pass element.htmlTemplate and element.bindData separately to the directive through two separate attributes as mentioned in an answer by user Vineet, which makes more sense.
Edit2: Fixed some bugs.
Here's the fiddle: Fiddle
With generalising I assume you want to make it like a component and resuse it whenever you want.
I would suggest to separate the scope of this directive. Bind your "html" and "name" both of them from parent.
AFTER EDITING:
app.directive('dynamic', function ($compile) {
return {
restrict: 'A',
replace: true,
scope : {
html : "=dynamic"
},
link: function (scope, ele, attrs) {
ele.html(scope.html);
$compile(ele.contents())(scope);
var unbindWatcher = $scope.$watch(
attrs.bindData,
function(binddata) {
if ( binddata ) {
angular.extend(scope,binddata);
// Once the data has been binded to scope,
// there's no more need to watch the change
// in the model value.
unbindWatcher();
}
}
);
}
};
});
And your html as:
<div dynamic="html" bindData="bindData"></div>
In this edit, what I have done is following 3 things:
1) Watching your attribute bindData - which will contain your db stored bind data. I have not included this in scope of directive because I want to include its properties in the scope so that you can bind your 'name', etc. from bindData in db to the templates.
2)Extending the bindData object into scope of your directive. Now your scope will have 'name', etc.
3)Destroying the watcher as soon as bindData is read for first time. This ensures that any change in bindData variable inside parent will not be conveyed to directive after first bind.
But still keep the scope separated to make it work properly.
Hope this will solve your problem
I am relatively new to Angular and got stuck on a custom directive.
I am trying to create a dynamic grid as a custom directive.
I already got that part working as in this example:
working grid as custom directive
There are certain scenarios where I need to set attributes on some of the elements of the grid.
This part has got me stumped.
I am planning on including the attributes as an array inside the object and then just putting it in the html tag of the associated entry.
This part is demonstrated here:
broken grid as custom directive with dynamic attributes
If you look at the "entries" array in the controller, I have now changed it to include an "attributes" array which will contain objects specifying the attribute name and property. These attributes should then be applied to the associated column.
e.g.
(First entry of the array)
col1: {
text: 'Obj1.col1',
attributes: [{
attr: 'ng-class',
attrVal: 'propVal == "" ? "someClass" : "someOtherClass"'
}, {
attr: 'id',
attrVal: '{{propName}}{{$index}}'
}]
},
...Truncated for brevity
This array entry should then be translated to:
<td ng-class="propVal == '' ? 'someClass' : 'someOtherClass'" id="col11">Obj1.col1</td>
I have read a couple of articles about the execution order of compile, controller, pre-link and post-link functions and have played around with different orders and trying to invoke compiling myself, but it all has failed.
Probably because I lack a deeper understanding of how it all ties together.
If someone can help me out or point me in the right direction if I'm heading down the wrong path, I would greatly appreciate that.
Okay, I finally figured out how to generate the grid dynamically using embedded custom directives inside a parent custom directive.
Here is a plunker showing how I did it:
Plunker with working dynamic grid
I have the Html templates defined as:
<div ng-grid ng-collection="entries" ng-collection-headings="headings" ng-button-click="theAction(inp)">
<div ng-checkbox-column></div>
</div>
and then the ng-grid directive as:
.directive("ngGrid", function () {
return {
restrict: "A",
scope: {
ngCollectionHeadings: "=",
ngCollection: "=",
ngButtonClick: "&"
},
template: function (element, attrs) {
var children = element.html();
children = children.trim().replace(/div/g, "td");
var htmlText = "<input type='button' ng-click='buttonClicked()' value='From the grid directive' /><table class='table table-bordered'><thead><tr><th ng-repeat='heading in ngCollectionHeadings'>{{heading}}</th></tr></thead><tbody><tr id='item{{$index}}' ng-repeat='item in ngCollection'>" + children + "</tr></tbody></table>";
return htmlText;
},
controller: function ($scope, $element) {
$scope.buttonClicked = function (inp) {
if (typeof inp != 'undefined')
inp = inp + ", through the grid directive.";
else
inp = "From the grid directive.";
$scope.ngButtonClick({ inp: inp });
};
}
};
})
and finally the ng-checkbox-column directive:
.directive("ngCheckboxColumn", function () {
return {
restrict: "A",
template: function (element, attributes) {
var htmlText = "<td><label><input type='checkbox' ng-model='item.checked' ng-click='tempButtonClicked()' /> From the checkbox directive.</label></td>";
return htmlText;
},
controller: function ($scope, $element) {
$scope.tempButtonClicked = function () {
var val = "From the checkbox directive";
$scope.buttonClicked(val);
};
}
};
})
My data collections are pretty straight forward:
$scope.headings = {
head1: 'Heading 1',
head2: 'Heading 2',
head3: 'Heading 3'
};
$scope.entries = [{
col1: 'Obj1.col1',
col2: 'Obj1.col2',
col3: 'Obj1.col3',
checked: false
}, {
col1: 'Obj2.col1',
col2: 'Obj2.col2',
col3: 'Obj2.col3',
checked: false
}, {
col1: 'Obj3.col1',
col2: 'Obj3.col2',
col3: 'Obj3.col3',
checked: false
}, {
col1: 'Obj4.col1',
col2: 'Obj4.col2',
col3: 'Obj4.col3',
checked: false
}];
This is still not entirely completed, but you should get the basic idea.
See the fiddle
test.directive('testMe', ['$compile', function($compile) {
return {
restrict: 'EA',
transcluded: true,
link: function(scope, element, attrs) {
scope.state = 'compiled';
//a = $(element.html()); //this gives: Error: Syntax error, unrecognized expression: Something I actually want to save {{state}}
a = $('<div>' + element.html() + '</div>');
var el = $compile(a)(scope);
scope.compiled = element.html();
},
}
}]);
For some reason I want to compile template with a given scope to a string, and after asking Uncle Google and doing some experiments I gave up.
Does annyone knows how to do this? Maybe my approach is wrong at all?
I want to notice that as a result I need template compiled to string, saved in a variable.
EDIT
To be more specific, here's what i need:
var template = "<p>{{variable}}</p>";
$scope.variable = "test";
var compiled = //magic goes here
alert(compiled); // gives <p>test</p>
I recently stumbled onto a similar problem and after several hours i was able to solve it with a little help from this post:
Blog post from Ben Lesh
I wanted to create a ComboBox to select an Image for another Entity to save in a relational Database. Of course my Entity had other relations too and so I described them in a JSON-File
//image
{ id: 4, path: 'http://www.example.com/myimage.png', name: 'Picture of my cat' }
//entity
{ id: 7, foo: 'bar', imageId: 4, anotherEntityId: 12}
//anotherEntity
{ id: 12, name: 'My Entity'}
I now wanted to create a Formular for entering new entities and for the foreign keys I wanted a combobox
I then declared another JSON-Object, containing every column of entity and also how i wanted them rendered
{cols:
[
{
name: 'imageId',
displayName: 'Image',
type: Image,
template: '<img src="{{e.path}}" />{{e.name}}'
},
...
]}
To do so i created a new directive, called nComboBoxRenderer
<ng-combo-box-renderer data="image", template="column.template"></ng-combo-box-renderer>
-
directives.directive('ngComboBoxRenderer', ['$compile', function($compile) {
return {
restrict: "E",
scope: {
e: '=data', // in my case this is the image
template: '=template' // the template
},
link: function($scope, element, attributes) {
var tl = angular.element("<span>" + $scope.template + "</span>");
compiled = $compile(tl);
element.append(tl);
compiled($scope);
}
}
}]);
While this is not the exact same use case as you have, the process involved appears to be the same.
I have an dierective that looks like this:
.directive('gdInputField', function() {
return {
//will only work for elements, not attributes.
restrict: 'E',
template: "<label for='{{name}}'>{{label}}</label><input name='{{key}}' id='{{name}}' type='{{type}}' ng-required='{{required}}' /><p>{{entry.1949113882}}</p>",
scope: {
label: '#label',
name: '#name',
key: "#key",
required: "#required",
type: "#type"
},
};
})
I wish to add the value that is set with #key as the name of the model for the input-field. If I set ng-model='key'. I get the string that is #key as the content of the ng-model.
This is seems to be the result:
$scope={
someting: #key
}
What I want is:
$scope={
#key: '';
}
if the user writes something in the input #key should update.
Also, what is the current name of the model-bidning or how could I find out?
Rewrite your code as follows
app.directive('gdInputField', function () {
return {
//will only work for elements, not attributes.
restrict: 'E',
template: "<label for='{{name}}'>{{label}}</label><input name='{{inputName}}' ng-model='key' id='{{name}}' type='{{type}}' ng-required='{{required}}' /><p>{{key}}</p>",
link: function (scope, elem, attrs) {
scope.inputName = attrs.key;
},
scope: {
label: '#label',
name: '#name',
key: "=",
required: "#required",
type: "#type"
},
};
});
Key point is that the key should be bidirectional to be used as model.
I've created a directive with a binding using "scope". In some cases, I want to bind a constant object. For instance, with HTML:
<div ng-controller="Ctrl">
<greeting person="{firstName: 'Bob', lastName: 'Jones'}"></greeting>
</div>
and JavaScript:
var app = angular.module('myApp', []);
app.controller("Ctrl", function($scope) {
});
app.directive("greeting", function () {
return {
restrict: "E",
replace: true,
scope: {
person: "="
},
template:
'<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
Although this works, it also causes a JavaScript error:
Error: 10 $digest() iterations reached. Aborting!
(Fiddle demonstrating the problem)
What's the correct way to bind a constant object without causing the error?
Here's the solution I came up with, based on #sh0ber's answer:
Implement a custom link function. If the attribute is valid JSON, then it's a constant value, so we only evaluate it once. Otherwise, watch and update the value as normal (in other words, try to behave as a = binding). scope needs to be set to true to make sure that the assigned value only affects this instance of the directive.
(Example on jsFiddle)
HTML:
<div ng-controller="Ctrl">
<greeting person='{"firstName": "Bob", "lastName": "Jones"}'></greeting>
<greeting person="jim"></greeting>
</div>
JavaScript:
var app = angular.module('myApp', []);
app.controller("Ctrl", function($scope) {
$scope.jim = {firstName: 'Jim', lastName: "Bloggs"};
});
app.directive("greeting", function () {
return {
restrict: "E",
replace: true,
scope: true,
link: function(scope, elements, attrs) {
try {
scope.person = JSON.parse(attrs.person);
} catch (e) {
scope.$watch(function() {
return scope.$parent.$eval(attrs.person);
}, function(newValue, oldValue) {
scope.person = newValue;
});
}
},
template: '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
You are getting that error because Angular is evaluating the expression every time. '=' is for variable names.
Here are two alternative ways to achieve the same think without the error.
First Solution:
app.controller("Ctrl", function($scope) {
$scope.person = {firstName: 'Bob', lastName: 'Jones'};
});
app.directive("greeting", function () {
return {
restrict: "E",
replace: true,
scope: {
person: "="
},
template:
'<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
<greeting person="person"></greeting>
Second Solution:
app.directive("greeting2", function () {
return {
restrict: "E",
replace: true,
scope: {
firstName: "#",
lastName: "#"
},
template:
'<p>Hello {{firstName}} {{lastName}}</p>'
};
});
<greeting2 first-name="Bob" last-Name="Jones"></greeting2>
http://jsfiddle.net/7bNAd/82/
Another option:
app.directive("greeting", function () {
return {
restrict: "E",
link: function(scope,element,attrs){
scope.person = scope.$eval(attrs.person);
},
template: '<p>Hello {{person.firstName}} {{person.lastName}}</p>'
};
});
This is because if you use the = type of scope field link, the attribute value is being observed for changes, but tested for reference equality (with !==) rather than tested deeply for equality. Specifying object literal in-line will cause angular to create the new object whenever the atribute is accessed for getting its value — thus when angular does dirty-checking, comparing the old value to the current one always signals the change.
One way to overcome that would be to modify angular's source as described here:
https://github.com/mgonto/angular.js/commit/09d19353a2ba0de8edcf625aa7a21464be830f02.
Otherwise, you could create your object in the controller and reference it by name in the element's attribute:
HTML
<div ng-controller="Ctrl">
<greeting person="personObj"></greeting>
</div>
JS
app.controller("Ctrl", function($scope)
{
$scope.personObj = { firstName : 'Bob', lastName : 'Jones' };
});
Yet another way is to create the object in the parent element's ng-init directive and later reference it by name (but this one is less readable):
<div ng-controller="Ctrl" ng-init="personObj = { firstName : 'Bob', lastName : 'Jones' }">
<greeting person="personObj"></greeting>
</div>
I don't particularly like using eval(), but if you really want to get this to work with the HTML you provided:
app.directive("greeting", function() {
return {
restrict: "E",
compile: function(element, attrs) {
eval("var person = " + attrs.person);
var htmlText = '<p>Hello ' + person.firstName + ' ' + person.lastName + '</p>';
element.replaceWith(htmlText);
}
};
});
I had the same problem, I solved it by parsing the json in the compile step:
angular.module('foo', []).
directive('myDirective', function () {
return {
scope: {
myData: '#'
},
controller: function ($scope, $timeout) {
$timeout(function () {
console.log($scope.myData);
});
},
template: "{{myData | json}} a is {{myData.a}} b is {{myData.b}}",
compile: function (element, attrs) {
attrs['myData'] = angular.fromJson(attrs['myData']);
}
};
});
The one drawback is that the $scope isn't initially populated when the controller first runs.
Here's a JSFiddle with this code.