AngularJs Attribute Directive 2 Way Binding - javascript

I have an angular app like this
Plunker
Javascript:
(function(angular, module){
module.controller("TestController", function($scope){
$scope.magicValue = 1;
});
module.directive("valueDisplay", function () {
return {
restrict: "A",
template: '<span>Iso Val: </span>{{ value }}<br/><span>Iso Change: </span><input data-ng-model="value" />',
replace: false,
scope: { },
link: function (scope, element, attrs) {
var pValKey = attrs.valueDisplay;
// Copy value from parent Scope.
scope.value = scope.$parent[pValKey];
scope.$parent.$watch(pValKey, function(newValue) {
if(angular.equals(newValue, scope.value)) {
// Values are the same take no action
return;
}
// Update Local Value
scope.value = newValue;
});
scope.$watch('value', function(newValue) {
if(angular.equals(newValue, scope.$parent[pValKey])) {
// Values are the same take no action
return;
}
// Update Parent Value
scope.$parent[pValKey] = newValue;
});
}
};
});
}(angular, angular.module("Test", [])));
HTML:
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js#*" data-semver="1.2.0-rc2" src="http://code.angularjs.org/1.2.0-rc.2/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="Test">
<div ng-controller="TestController">
<ol>
<li>
<span>Parent Val: </span>{{ magicValue }}<br/>
<span>Parent Change:</span><input data-ng-model="magicValue" />
</li>
<li data-value-display="magicValue"></li>
</ol>
</div>
</body>
</html>
Ok so This works and all but I'm wondering if there is not a better way of doing this 2 way binding that I have setup here?
Keep in mind that I want Isolated Scope & that I know I can define extra Attributes and use the '=' to have 2 way data binding between parent and isolated scope I'd like something like that but where the data gets passed in to the directives attribute like I have here.

You can do this much more tersely using your isolated scope.
Here is an updated plunker.
You can two-way bind the value of your directive with value: '=valueDisplay'
The = tells angular you want two-way binding:
module.directive("valueDisplay", function () {
return {
restrict: "A",
template: '<span>Iso Val: </span>{{ value }}<br/><span>Iso Change: </span><input data-ng-model="value" />',
replace: false,
scope: { value: '=valueDisplay' },
link: function (scope, element, attrs) {
}
};
});

Related

Difference between passing attribute to directive in {{}} curly bracket and without curly brackets in angular?

I am trying to set watcher on an attribute in angular directive like this
angular.module("myApp",[]);
angular.module("myApp").directive('myDirective', function () {
return {
restrict: 'A',
scope: true,
link: function ($scope, element, attrs) {
$scope.$watch(function () {
return [attrs.attrOne];
}, function (newVal) {
console.log(newVal);
}, true);
}
};
});
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"> </script>
</head>
<body ng-app="myApp">
<div my-directive attr-one="{{x}}">{{x}}</div>
<input ng-model="x" />
</body>
</html>
In the above example I passed the ng-model in {{}} to the directive attribute and watcher works like charm
but when I try to pass ng-model directly to directive attribute the watcher doesn't work anymore, check the code below
angular.module("myApp",[]);
angular.module("myApp").directive('myDirective', function () {
return {
restrict: 'A',
scope: true,
link: function ($scope, element, attrs) {
$scope.$watch(function () {
return [attrs.attrOne];
}, function (newVal) {
console.log(newVal);
}, true);
}
};
});
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>
</head>
<body ng-app="myApp">
<div my-directive attr-one="x">{{x}}</div>
<input ng-model="x" />
</body>
</html>
I am not getting what magic is {{}} brackets doing here. Any explanation for some wise man?
EDIT: I don't want to create an isolated scope because the element on which it applies uses the parent scope inside it

Angular: parent directive isn't affected by changes in its child

I have a categoryList directive that generates a select box of categories. This directive works fine and when I select a category, the outer controller scoped property mentioned in ngModel is updated properly. But when I put categoryList in another directive (subCategoryList), the scope of subCategoryList isn't updated properly.
You can see this problematic behavior in this snippet: In the first select box, you can see that any change will be updated in the outer scope, but in the second select box, the changes are "stuck" inside the categoryList directive, and doesn't affect subCategoryList
angular.module('plunker', [])
.controller('MainCtrl', function($scope) {
}).directive('categoryList', function () {
return {
restrict: 'E',
template: 'categoryList debug: {{model}}<br/><br/><select ng-options="cat as cat.name for cat in categories track by cat.id" ng-model="model" class="form-control"><option value="">{{emptyOptLabel}}</option></select>',
scope: {
model: '=ngModel',
categories: '=?',
catIdField: '#',
emptyOptLabel:'#'
},
link: categoryListLink
};
function categoryListLink(scope, el, attrs) {
if (angular.isUndefined(scope.catIdField)) {
scope.catIdField = 'categoryId';
}
if(angular.isUndefined(scope.emptyOptLabel)){
scope.emptyOptLabel = 'category';
}
if( !scope.categories ) {
scope.categories = [
{
'categoryId':123,
'name':'cat1',
'subCategories':[
{
'subCategoryId':123,
'name':'subcat1'
}
]
}
];
}
prepareCats(scope.categories);
function prepareCats(cats){
cats.forEach(function (cat) {
cat.id = cat[scope.catIdField];
});
return cats;
}
}
}).directive('subCategoryList', function () {
return {
restrict: 'E',
template: 'subCategoryList debug:{{model}}<br/><br/><category-list ng-if="parent && parent.subCategories.length !== 0" ng-model="model" categories="parent.subCategories" cat-id-field="subCategoryId" empty-opt-label="sub category"></category-list>',
scope: {
model: '=ngModel',
parent: '='
}
};
});
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script data-require="angular.js#1.*" data-semver="1.4.3" src="https://code.angularjs.org/1.4.3/angular.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl as main">
Outer debug: {{main.cat}}<br/><br/>
<category-list ng-model="main.cat" empty-opt-label="category"></category-list><br/><br/>
<sub-category-list ng-model="main.subcat" parent="main.cat"></sub-category-list>
</body>
</html>
Someone has an idea what could be the problem here?
This is a scope issue related with ng-if in directive subCategoryList. If you remove it, the code starts working.

Add attribute directive inside directive in angular

I'm creating a validation directive in angular and I need to add a tooltip to the element the directive is bound to.
Reading thru the web I found this solution setting a high priority and terminal to the directive, but since I'm using ngModel this doesn't work for me. This is what I'm doing right now:
return {
restrict: 'A',
require: 'ngModel',
replace: false,
terminal: true,
priority: 1000,
scope: {
model: '=ngModel',
initialValidity: '=initialValidity',
validCallback: '&',
invalidCallback: '&'
},
compile: function compile(element, attrs) {
element.attr('tooltip', '{{validationMessage}');
element.removeAttr("validator");
return {
post: function postLink(scope, element) {
$compile(element)(scope);
}
};
},
}
But it's not working for me. It throws the following error:
Error: [$compile:ctreq] Controller 'ngModel', required by directive 'validator', can't be found!
This is the HTML where I'm using the directive:
<input id="username" name="username" data-ng-model="user.username" type="text" class="form-control" validator="required, backendWatchUsername" placeholder="johndoe" tabindex="1" >
Any ideas on how can I solve this?
Thanks.
The reason is because of the combination of your directive priority with terminal option. It means that ngModel directive will not render at all. Since your directive priority (1000) is greater than ng-model's(0) and presence of terminal option will not render any other directive with lower priority (than 1000). So some possible options are :
remove the terminal option from your directive or
reduce the priority of your directive to 0 or -1 (to be less than or equal to ngModel) or
remove ng-model requirement from the directive and possibly use a 2-way binding say ngModel:"=" (based on what suits your requirement).
Instead of adding tooltip attribute and recompiling the element, you could use transclusion in your directive and have a directive template.
terminal - If set to true then the current priority will be the last set of directives which will execute (any directives at the current priority will still execute as the order of execution on same priority is undefined). Note that expressions and other directives used in the directive's template will also be excluded from execution.
demo
angular.module('app', []).directive('validator', function($compile) {
return {
restrict: 'A',
require: 'ngModel',
replace: false,
terminal: true,
scope: {
model: '=ngModel',
initialValidity: '=initialValidity',
validCallback: '&',
invalidCallback: '&'
},
compile: function compile(element, attrs) {
element.attr('tooltip', '{{validationMessage}');
element.removeAttr("validator");
return {
post: function postLink(scope, element) {
$compile(element)(scope);
}
};
},
}
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<input validator ng-model="test">
</div>
As explained in my comments you do not need to recompile the element and all these stuffs, just set up an element and append it after the target element (in your specific case, the input).
Here is a modified version of validation directive (i have not implemented any validation specifics which i believe you should be able to wire up easily).
So what you need is to set up custom trigger for tooltip which you can do by using the $tooltipprovider. So set up an event pair when you want to show/hide tooltip.
.config(function($tooltipProvider){
$tooltipProvider.setTriggers({'show-validation':'hide-validation'});
});
And now in your directive just set up your tooltip element as you like with tooltip attributes on it. compile only the tooltip element and append it after the target element (you can manage positioning with css ofcourse). And when you have validation failure, just get the tooltip element reference (which is reference to the tooltip element, instead of copying the reference you could as well select every time using the selector) and do $tooltipEl.triggerHandler('show-validation') and to hide it $tooltipEl.triggerHandler('show-validation').
Sample Implementation which shows the tooltip after 2 sec and hides it after 5 sec (since validation is not in the scope of this question you should be able to wire it up):
.directive('validator', function($compile, $timeout){
var tooltiptemplate = '<span class="validation" tooltip="{{validationMessage}}" tooltip-trigger="show-validation" tooltip-placement="bottom"></span>';
var tooltipEvents = {true:'show-validation', false:'hide-validation'};
return {
restrict: 'A',
require: 'ngModel',
replace: false,
priority: 1000,
scope: {
model: '=ngModel',
initialValidity: '=initialValidity',
validCallback: '&',
invalidCallback: '&'
},
compile: function compile(element, attrs) {
return {
post: function postLink(scope, element) {
var $tooltipEl= getTooltip();
init();
function init(){
scope.$on('$destroy', destroy);
scope.validationMessage ="Whoops!!!";
$timeout(function(){
toggleValidationMessage(true);
},2000);
$timeout(function(){
toggleValidationMessage(false);
},5000);
}
function toggleValidationMessage(show){
$tooltipEl.triggerHandler(tooltipEvents[show]);
}
function getTooltip(){
var elm = $compile(angular.element(tooltiptemplate))(scope);
element.after(elm);
return elm;
}
function destroy(){
$tooltipEl= null;
}
}
};
},
}
});
Plnkr
Inline Demo
var app = angular.module('plunker', ['ui.bootstrap']);
app.controller('MainCtrl', function($scope) {
$scope.user = {
username: 'jack'
};
}).directive('validator', function($compile, $timeout) {
var tooltiptemplate = '<span class="validation" tooltip="{{model}}" tooltip-trigger="show-validation" tooltip-placement="bottom"></span>';
var tooltipEvents = {
true: 'show-validation',
false: 'hide-validation'
};
return {
restrict: 'A',
require: 'ngModel',
replace: false,
priority: 1000,
scope: {
model: '=ngModel',
initialValidity: '=initialValidity',
validCallback: '&',
invalidCallback: '&'
},
compile: function compile(element, attrs) {
return {
post: function postLink(scope, element) {
var $tooltipEl = getTooltip();
init();
function init() {
scope.$on('$destroy', destroy);
scope.validationMessage = "Whoops!!!";
$timeout(function() {
toggleValidationMessage(true);
}, 2000);
$timeout(function() {
toggleValidationMessage(false);
}, 5000);
}
function toggleValidationMessage(show) {
$tooltipEl.triggerHandler(tooltipEvents[show]);
}
function getTooltip() {
var elm = $compile(angular.element(tooltiptemplate))(scope);
element.after(elm);
return elm;
}
function destroy() {
elm = null;
}
}
};
},
}
}).config(function($tooltipProvider) {
$tooltipProvider.setTriggers({
'show-validation': 'hide-validation'
});
});
/* Put your css in here */
.validation {
display: block;
}
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<link data-require="bootstrap-css#3.1.*" data-semver="3.1.1" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
<script>
document.write('<base href="' + document.location + '" />');
</script>
<script data-require="angular.js#1.3.x" src="https://code.angularjs.org/1.3.12/angular.js" data-semver="1.3.12"></script>
<script data-require="ui-bootstrap#*" data-semver="0.12.0" src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.12.0.min.js"></script>
</head>
<body ng-controller="MainCtrl">
<br/>
<br/>{{user.username}}
<input id="username" name="username" data-ng-model="user.username" type="text" class="form-control" validator="required, backendWatchUsername" placeholder="johndoe" tabindex="1">
</body>
</html>
You should not create a new isolated scope in your directive: this will mess up with the others directives (and in this case will not share ngModel).
return {
restrict: 'A',
require: 'ngModel',
compile: function compile(element, attrs) {
element.attr('tooltip', '{{validationMessage}');
element.removeAttr("validator");
return {
post: function postLink(scope, element) {
$compile(element)(scope);
}
};
},
}
I invite you to check the Angular-UI library and especially how they have implemented their ui.validate directive: http://angular-ui.github.io/ui-utils/

AngularJS - How do I change an element within a template that contains a data-binding?

What is the most Angular recommended way to use a dynamic tag name in a template?
I have a drop-down containing h1-h6 tags. A user can choose any of these and the content will change to being wrapped by the chosen header tag (which is stored on the $scope). The content is bound to the model i.e. within {{ }}.
To persist the binding I can change the markup and use $compile. However, this does not work because it gets appended (obviously) before Angular replaces the {{ }} with model values. It's h3 on page load.
Example:
<div id="root">
<h3 id="elementToReplace">{{ modelData }}</h3>
</div>
When re-compiling I have tried using a string as follows:
<{{ tag }} id="elementToReplace">{{ modelData }}</{{ tag }}>
Any ideas?
Demo Plunker Here
Define a scope variable named 'tag' and bind it to both your select list and custom directive.
HTML:
<select ng-model="tag" ng-init="tag='H1'">
<option ng-value="H1">H1</option>
<option ng-value="H2">H2</option>
<option ng-value="H3">H3</option>
<option ng-value="H4">H4</option>
<option ng-value="H5">H5</option>
</select>
<tag tag-name="tag">Hey There</tag>
Next, pass the tag scope model into your directive using two-way model binding:
var app = angular.module('app',[]);
app.directive('tag', function($interpolate) {
return {
restrict: 'E',
scope: {
tagName: '='
},
link: function($scope, $element, $attr) {
var content = $element.html();
$scope.$watch('tagName', function(newVal) {
$element.contents().remove();
var tag = $interpolate('<{{tagName}}>{{content}}</{{tagName}}>')
({tagName: $scope.tagName, content: content});
var e = angular.element(tag);
$element.append(e);
});
}
}
});
Notice that in the custom directive, we are using the $interpolate service to generate the HTML element based on the Tag that was selected in the select list. A $watch function is used to watch for changes to the tag model, and when it changes, the new element is appended to the DOM.
Here is one I knocked up, even though you said you didn't want it ;)
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="../lib/jquery.js"></script>
<script src="../lib/angular.js"></script>
<script>
var app = angular.module('app', []);
app.controller('ctrl', ['$scope', function ($scope) {
$scope.modelData = "<h1>Model Data</h1>" +
"<p id='replace'>This is the text inside the changing tags!</p>";
$scope.tags = ["h1", "h2", "h3", "h4", "p"];
$scope.selectedTag = "p";
}]);
app.directive("tagSelector", function(){
return {
resrict: 'A',
scope: {
modelData: '#',
selectedTag: '#'
},
link: function(scope, el, attrs){
scope.$watch("selectedTag", updateText);
el.prepend(scope.modelData);
function updateText(){
var tagStart = "<" + scope.selectedTag + " id='replace'>";
var tagEnd = "</" + scope.selectedTag + ">";
$("#replace").replaceWith(tagStart + $("#replace").html() + tagEnd);
}
}
}
});
</script>
</head>
<body ng-app="app">
<div ng-controller="ctrl">
<select ng-model="selectedTag" ng-options="tag for tag in tags"></select>
<div tag-selector selected-tag="{{selectedTag}}" model-data="{{modelData}}"></div>
</div>
</body>
</html>
More kosher version. Work good with ng-repeat and nested directives.
Working example here.
angular.module('myApp', [])
.directive('tag', function($interpolate, $compile) {
return {
priority: 500,
restrict: 'AE',
terminal: true,
scope: {
tagName: '='
},
link: function($scope, $element) {
$scope.$on('$destroy', function(){
$scope.$destroy();
$element.empty();
});
$scope.$parent.$watch($scope.tagName, function(value) {
$compile($element.contents())($scope.$parent, function(compiled) {
$element.contents().detach();
var tagName = value || 'div';
var root = angular.element(
$element[0].outerHTML
.replace(/^<\w+/, '<' + tagName)
.replace(/\w+>$/, tagName + '>'));
root.append(compiled);
$element.replaceWith(root);
$element = root;
});
});
}
}
})
.controller('MyCtrl', function($scope) {
$scope.items = [{
name: 'One',
tagName: 'a'
}, {
name: 'Two',
tagName: 'span'
}, {
name: 'Three',
}, {
name: 'Four',
}];
});
Usages:
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<tag class="item" tag-name="'item.tagName'" ng-repeat="item in items">
{{item.name}}
</tag>
</div>
</div>

Angular Directive refresh on parameter change

I have an angular directive which is initialized like so:
<conversation style="height:300px" type="convo" type-id="{{some_prop}}"></conversation>
I'd like it to be smart enough to refresh the directive when $scope.some_prop changes, as that implies it should show completely different content.
I have tested it as it is and nothing happens, the linking function doesn't even get called when $scope.some_prop changes. Is there a way to make this happen ?
Link function only gets called once, so it would not directly do what you are expecting. You need to use angular $watch to watch a model variable.
This watch needs to be setup in the link function.
If you use isolated scope for directive then the scope would be
scope :{typeId:'#' }
In your link function then you add a watch like
link: function(scope, element, attrs) {
scope.$watch("typeId",function(newValue,oldValue) {
//This gets called when data changes.
});
}
If you are not using isolated scope use watch on some_prop
What you're trying to do is to monitor the property of attribute in directive. You can watch the property of attribute changes using $observe() as follows:
angular.module('myApp').directive('conversation', function() {
return {
restrict: 'E',
replace: true,
compile: function(tElement, attr) {
attr.$observe('typeId', function(data) {
console.log("Updated data ", data);
}, true);
}
};
});
Keep in mind that I used the 'compile' function in the directive here because you haven't mentioned if you have any models and whether this is performance sensitive.
If you have models, you need to change the 'compile' function to 'link' or use 'controller' and to monitor the property of a model changes, you should use $watch(), and take of the angular {{}} brackets from the property, example:
<conversation style="height:300px" type="convo" type-id="some_prop"></conversation>
And in the directive:
angular.module('myApp').directive('conversation', function() {
return {
scope: {
typeId: '=',
},
link: function(scope, elm, attr) {
scope.$watch('typeId', function(newValue, oldValue) {
if (newValue !== oldValue) {
// You actions here
console.log("I got the new value! ", newValue);
}
}, true);
}
};
});
I hope this will help reloading/refreshing directive on value from parent scope
<html>
<head>
<!-- version 1.4.5 -->
<script src="angular.js"></script>
</head>
<body ng-app="app" ng-controller="Ctrl">
<my-test reload-on="update"></my-test><br>
<button ng-click="update = update+1;">update {{update}}</button>
</body>
<script>
var app = angular.module('app', [])
app.controller('Ctrl', function($scope) {
$scope.update = 0;
});
app.directive('myTest', function() {
return {
restrict: 'AE',
scope: {
reloadOn: '='
},
controller: function($scope) {
$scope.$watch('reloadOn', function(newVal, oldVal) {
// all directive code here
console.log("Reloaded successfully......" + $scope.reloadOn);
});
},
template: '<span> {{reloadOn}} </span>'
}
});
</script>
</html>
angular.module('app').directive('conversation', function() {
return {
restrict: 'E',
link: function ($scope, $elm, $attr) {
$scope.$watch("some_prop", function (newValue, oldValue) {
var typeId = $attr.type-id;
// Your logic.
});
}
};
}
If You're under AngularJS 1.5.3 or newer, You should consider to move to components instead of directives.
Those works very similar to directives but with some very useful additional feautures, such as $onChanges(changesObj), one of the lifecycle hook, that will be called whenever one-way bindings are updated.
app.component('conversation ', {
bindings: {
type: '#',
typeId: '='
},
controller: function() {
this.$onChanges = function(changes) {
// check if your specific property has changed
// that because $onChanges is fired whenever each property is changed from you parent ctrl
if(!!changes.typeId){
refreshYourComponent();
}
};
},
templateUrl: 'conversation .html'
});
Here's the docs for deepen into components.

Categories

Resources