AngularJS: how to bind a constant object to a directive - javascript

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.

Related

Compile dynamic HTML and bind variable from JSON response - Directive with Isolated scope - AngularJS

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

How to render html in Angularjs directive?

I am trying to make custom directive to display:
Will {{selectedAsset}} go {{red}} or {{green}}?
It looks like this on the screen:
AUD/USD will down(red color) or up(green color)?
The {{red}} and {{green}} part should have its own color . Hence I am trying to wrap it with a span having desired classes.
But it is not working, below is the code:
<trade-header selected-asset="selectedAsset" red="widgetMessage.goDown" green="widgetMessage.goUp"></trade-header>
var widgetMessage = {
"Trade_Header": "Will {{selectedAsset}} go {{red}} or {{green}}?",
"goUp": "up",
"goDown": "down"}
myApp.directive("tradeHeader", function($sce) {
return {
restrict: "AE",
scope: {
selectedAsset: "=",
green:"=",
red:"="
},
link: function(scope, element, attrs) {
scope.green = $sce.trustAsHtml('<span class="goOrDonwLabel upGreen">' + scope.green + '</span>');
},
template: widgetMessage.Trade_Header,
}
});
widgetMessage.Trade_Header need to be variable cause the design.
This consequence will be : "AUD/USD will down or <span class="Green">up</span>?"
I need it to be compiled as HTML, any suggestions?
Your html should be in the template. When you're trying to manipulate HTML in code, that's usually a sign you're not using templates the right way. Try something like this:
<trade-header selected-asset="selectedAsset" red="widgetMessage.goDown" green="widgetMessage.goUp"></trade-header>
var widgetMessage = {
"Trade_Header": 'Will {{selectedAsset}} go {{red}} or <span ng-class="greenLabel">{{green}}</span>?',
"goUp": "up",
"goDown": "down"}
myApp.directive("tradeHeader", function($sce) {
return {
restrict: "AE",
scope: {
selectedAsset: "=",
green:"=",
red:"="
},
link: function(scope, element, attrs) {
scope.greenLabel = widgetMessage.goUp; // logic for setting green's class here
},
template: widgetMessage.Trade_Header,
}
});

Angular: compiling template from string to string

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.

Set ng-model to a be named the same as variable rather than referencing that variable?

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.

angularJS + jQuery jeditable plugin work together

I am trying to write a directive for the jeditable plugin so when it changes the value, it will change also edit the model of the edited element.
So i wrote something like that, JS Fiddle
but i don`t know how to get the object that bound to the object in the list.
JS:
var app = angular.module("app", []);
app.controller('ctrl', function ($scope) {
$scope.lst = [{
id: 1,
name: "item1"
}, {
id: 1,
name: "item1"
}, {
id: 2,
name: "item2"
}, {
id: 3,
name: "item3"
}, {
id: 3,
name: "item3"
}];
});
app.directive('uiEditable', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.editable("/echo/json/", {
onblur: 'submit',
onsubmit: function (response, settings) {
//here i need to update the model
}
});
}
};
});
This uses ngModel to update back to the model. (so don't forget ng-model on element)
app.directive('uiEditable', function () {
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
element.editable(function (val) {
var tVal = $.trim(val);
if (ngModel.$viewValue !== tVal)
scope.$apply(function () { return ngModel.$setViewValue(tVal); });
return tVal;
});
}
};
});
Why are you using the jeditable plugin? This plugin seems to only duplicate in jQuery what you could already do in angular using ng-model alone and no plugin required.
If you just want to create text which can be edited in place like jEditable does, instead of creating a custom directive simply using ng-submit, ng-click, ng-hide and ng-model. Here's a rough example.
The view:
<form ng-submit="submit()">
<div ng-hide="showEdit"
ng-click="showEdit = true">
{{foo.bar}}
</div>
<div>
<input type="text"
ng-show="showEdit"
ng-model="foo.bar" />
</div>
<a href="#" ng-show="showEdit"
ng-click="submit();">done</a>
</form>
And the controller:
app.controller('myCtrl', function($scope) {
$scope.foo = {
bar: 'some text'
};
$scope.showEdit = false;
$scope.submit = function() {
// hide the edit field
$scope.showEdit = false;
// submit form
console.log('submit form');
}
});
Pass your item in in an isolated scope:
app.directive('uiEditable', function(){
return {
restrict: 'A',
scope: {
item: '='
},
link: function(scope, element, attrs){
element.editable("/echo/json/", {
onblur: 'submit',
onsubmit: function(response, settings){
alert(scope.item);
}
});
}
};
});
'scope.item' will now give you a reference to the item inside your directive.

Categories