Custom angular directive : how to watch for scope changes - angularjs

I am writing a custom directive with a single field in its scope. This field is a dictionary with arrays in values.
I want the directive to react to any change made on this field : new entry, additional value in list, etc...
I'm just trying to figure out why :
my directive does not react when I change values in the dictionary.
directive is not even initialized with the initial dictionary.
Here is a simplified version of my script, where I only perform some logging in the sub-directive.Nothing happens when the button is clicked on the dictionary is modified :
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Test</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
angular.module("myApp", [])
.controller("myCtrl", function($scope) {
$scope.dico = {};
$scope.dico["Paris"] = [];
$scope.dico["Paris"].push("Tour Eiffel");
$scope.dico["Paris"].push("Champs Elysees");
$scope.dico["London"] = [];
$scope.dico["London"].push("British Museum");
$scope.addPOI = function() {
$scope.dico["Wellington"] = [];
$scope.dico["Wellington"].push("Botanic Garden");
console.log($scope.dico);
};
})
.directive('subdirective', function() {
return {
restrict: 'E',
template: '<div><span ng-repeat="key in myDico">{{key}}</span></div>',
link: function(scope, element, iAttrs) {
console.log("test");
scope.$watch("myDico", function(newVal, oldVal) {
console.log("watch!");
console.log(newVal);
//update values...
}, true);
},
scope: {
myDico: '='
}
};
});
</script>
</head>
<body ng-app="myApp">
<div ng-controller="myCtrl">
<button ng-click="addPOI()">
Add POI
</button>
<div>
<subdirective myDico="dico"></subdirective>
</div>
</div>
</body>
</html>
I have tried to use $watch, $watchCollection, deep watch, but it does not seem to do the job.

You are missing scope binding definition in your Directive Definition Object.
scope: {
myDico: '='
}

Related

angularjs: any way to use directive's nested DOM elements as the template?

Please refer to code below.
I'd like to avoid using ng-trasclude, as it's extra wrapping elements, + ng-transclude make own scope. So my goal is to render <div foo title="FOO!">FOO!</div> in the end.
$compile(el.html())(scope) breaks, since again, it needs a wrapping element.
template: "<div ng-transclude></div>" will fail to acces scope.title.
Thanks
EDIT
Added plunker link: https://plnkr.co/edit/R1CAc5pksOVMJoFLhsTu?p=preview
And snippet
angular.module('app', []).directive('foo', function() {
return {
restrict: 'A',
scope: {
title: '#'
}
}
});
<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js"></script>
</head>
<body>
<div foo title="FOO">{{title}}</div>
<span>expecting "FOO!" above this line, but, sigh...</span>
</body>
</html>
EDIT 2
I'd like to keep isolate scope, so that attributes (<div foo title="FOO!">{{title}}</div> are then applied via scope: {title:'#'}.
EDIT 3
Updated the snippet.
You assigned isolated scope for the directive. if you want access it make it as local by given scope : true.
angular.module('app', []).directive('foo', function() {
return {
restrict: 'A',
scope: {
title : '='
},
template : '{{title}}'
}
});
<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js"></script>
</head>
<body>
<div foo title="'Foo!'"></div>
<span>expecting "FOO!" above this line, but, sigh...</span>
</body>
</html>
you need to have a template to render scope.foo so in your directive you need to specify a template. also if you aren't going to have any values coming from the parent you don't have to put scope: {} as a scope will be create for you already
angular.module('app', []).directive('foo', function() {
return {
restrict: 'A',
template: '<span ng-bind="foo"></span>', // <-- add this line
link: function(scope, el) {
scope.foo = 'FOO!';
}
}
});
try with it
link: function(scope, el,attr) {
scope.$parent.foo = "foo"
scope.foo = 'FOO!';
}

Directive inside directive and ngModel

I'm trying to create a directive that uses another directive.
The main directive, slipt a string to edit each item separately.
The problem is that the main directive doesn't receibe the ng-model changes from the inner directive.
Use the example below:
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.2/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.0/ui-bootstrap-tpls.min.js"></script>
</head>
<body ng-app="app" ng-controller="Ctrl">
<input type="text" ng-model="values">
<editors model="values">
<script>
var app = angular.module('app', ['ui.bootstrap']);
app.controller('Ctrl', ['$scope', function($scope) {
$scope.values = '1 2';
}]);
app.directive('editors', function() {
return {
restrict: 'E',
template: '<accordion><accordion-group heading="Editor 1 - {{field1}}"><editor model="field1"></accordion-group><accordion-group heading="Editor 2 - {{field2}}"><editor model="field2"></accordion-group>',
scope: {
model: '=model'
},
controller: ['$scope', function($scope) {
$scope.$watch('model', function() {
var values = $scope.model.split(' ');
$scope.field1 = values[0];
$scope.field2 = values[1];
$('body').append($scope.field1 + ' - ');
$('body').append($scope.field2+ '<br>');
});
}]
};
});
app.directive('editor', function() {
return {
restrict: 'E',
template: '<input type="text" ng-model="model"> {{model}}',
scope: {
model: '=model'
}
};
});
</script>
</body>
</html>
Image 1 - Changing the value in the field1 (Editor 1) doesn't affect the accordion title.
Iamge 2 - Changing the root value (input outside accordion) updates the fields (field1 and field2) and accordion heading.
How can I get it working, when I change the Editor 1 value to update accordion heading ?
NEVER EVER EVER EVER EVER EVER use the prefix "ng" in your own custom directives!!!!
ng-model is a directive developped and maintained by angular. It will create its own scope.
It is supposed to receive a string and nothing else.
Just bind it that way :
scope: {
model: '=model'
},

AngularJS : Data binding in directive template doesn't work with ng-repeat

I created this simple plunker to demonstrate the problem:
http://plnkr.co/edit/xzgzsAy9eJCAJR7oWm74?p=preview
var app = angular.module('app',[]);
app.controller('ctrl',function($scope){
$scope.items = {};
})
app.directive('myDirective',function(){
return {
restrict: 'E',
scope: {
item: "=item"
},
template: "<h2>ng-repeat: </h2>" +
"<button ng-repeat='i in [1,2,3]' ng-click='item = true'>Set to true</button>" +
"<h2>no ng-repeat: </h2>" +
"<button ng-click='item = false'>Set to false</button>"
}
})
<body ng-controller='ctrl'>
<h1>Item: {{items.someItem}}</h1>
<my-directive item='items.someItem'></my-directive>
</body>
Two way data binding works when I pass a model to the directive, unless it is accessed from inside of ng-repeat.
Why is this happening and how to solve this problem?
You find answer here. Briefly, ng-repeat create a new scope, a primitive data type (boolean, integer, ...) copied by value in the new scope. But objects ({}, []) copied by pointer (not value) and it be same in the new scope and parents scope.
Edited:
I solved your case plnkr
Html:
<!DOCTYPE html>
<html ng-app='app'>
<head>
<script data-require="angular.js#*" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
<script src="script.js"></script>
</head>
<body ng-controller='ctrl'>
<h1>Item: {{items.someItem}}</h1>
<my-directive item='items.someItem'></my-directive>
</body>
</html>
JavaScript:
var app = angular.module('app', []);
app.controller('ctrl', function($scope) {
$scope.items = {};
})
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
extItem: "=item"
},
template: "<h2>ng-repeat: </h2>" +
"<button ng-repeat='i in [1,2,3]' ng-click='intItem.val = true'>Set to true</button>" +
"<h2>no ng-repeat: </h2>" +
"<button ng-click='intItem.val = false'>Set to false</button>",
link: function(scope, element, attrs) {
scope.intItem = {
val: scope.extItem
};
scope.$watch('intItem.val', function(){scope.extItem = scope.intItem.val})
}
}
})
In this solution I'm create internal object intItem with Boolean property val, which passed into ng-repeat and added $watch for intItem.val.

angularjs adding my own function to the existing ng-click

I have ng-click="foo()" which alert "foo"
In my own directive, if ng-click is found, I want to add another function to alert "bar"
I tried this
DEMO: http://plnkr.co/edit/1zYl0mSxeLoMU3yjoGBV?p=preview
and it did not work
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<script src="http://code.angularjs.org/1.2.7/angular.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('MyCtrl', function($scope) {
$scope.foo = function() { alert('foo'); }
$scope.bar = function() { alert('bar'); }
});
app.directive("myAttr", function() {
return {
link: function(scope, el, attrs) {
el.attr('ng-click', attrs.ngClick+';bar()');
}
}
})
</script>
</head>
<body ng-controller="MyCtrl">
<a my-attr ng-click="foo()" href="">click here!</a>
</body>
</html>
I was also not able to another ng-* directive to this to make it work, i.e. el.attr('ng-focus', 'bar()');. It seems that I cannot change or add ng-* directive once it is rendered.
How can I achieve this, and what was I doing wrong?
app.directive("myAttr", function() {
return {
priority: 1,
compile: function(el, attrs) {
attrs.ngClick += ';bar()';
}
}
})
First of all you want a compile function, for when link is called, the ng-click directive is already set up.
The second important thing is to change the priority. You want to ensure that your directive is called before ng-click. ng-click has the default priority 0, so 1 is enough.
The last and important thing, which is not obvious, is that you don't want to change the element, but attrs itself. It is created only once per element. So when ng-click accesses it it would still contain the same value, if you changed the attribute on the element directly.
I think you can do what you want with ngTransclude.
app.directive("myAttr", function() {
return {
transclude:true,
template: '<span ng-click="bar()" ng-transclude></span>',
link: function(scope, el, attrs) {
}
}
});
Does that work?
EDIT
Okay what about this one?
app.directive("myAttr", function($compile) {
return {
link: function(scope, el, attrs) {
el.attr('ng-click', 'bar()');
el.removeAttr('my-attr');
$compile(el)(scope);
}
}
});
While this could be done with compile as outlined above, that approach doesn't guarantee the order in which the ng-click items would be added to a DOM node (as you have already discovered), and is inherently slow (as has been pointed out by Words Like Jared.
Personally, I would just do something like this:
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<link rel="stylesheet" href="style.css">
<script src="http://code.angularjs.org/1.2.7/angular.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('MyCtrl', function($scope) {
$scope.foo = function() { alert('foo'); }
$scope.bar = function() { alert('bar'); }
});
app.directive('myAttr', function() {
return {
scope: true,
link: function(scope, el, attrs) {
if(attrs.hasOwnProperty('ngClick')){
scope.foo = function(){
scope.$parent.foo();
scope.$parent.bar();
}
}
}
};
});
</script>
</head>
<body ng-controller="MyCtrl">
<a my-attr ng-click="foo()" href="">click here!</a>
</body>
</html>
Whats going on:
scope: true: By default directives do not create new scopes, simply sharing their parent scope. By setting scope: true, every instance of this directive will create a child scope, that will prototypically inherit from the parent scope.
Then you can simply override the method desired (foo()) and voila
Live demo:
http://plnkr.co/edit/8A8y96wAhqGEowFaRQUH?p=preview
I freely admit I may entirely misunderstand what you are trying to do. However, given the example you provided, I think you might be better served by separating concerns a little more.
It seems from your example that you are trying to trigger foo and bar together whenever your directive is present. If both foo and bar are concerns of the controller, then why not wrap them both up in another function and assign that function to the ng-click of your element. If foo is a concern of the controller, but bar is a concern of the directive, why not trigger the bar functionality directly from the directive code?
If the functionality wrapped up in 'foo' and 'bar' is suppose to be defined by the controller creator...
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<link rel="stylesheet" href="style.css">
<script src="http://code.angularjs.org/1.2.7/angular.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('MyCtrl', function($scope) {
$scope.foo = function() { alert('foo'); }
$scope.bar = function() { alert('bar'); }
$scope.pak = function() {
$scope.foo();
$scope.bar();
}
});
</script>
</head>
<body ng-controller="MyCtrl">
<a ng-click="pak()" href="">click here!</a>
</body>
</html>
Or, if the functionality wrapped up in 'foo' is suppose to be defined by the controller creator, but the functionality wrapped up in 'bar' is suppose to be defined by the directive creator...
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<link rel="stylesheet" href="style.css">
<script src="http://code.angularjs.org/1.2.7/angular.js"></script>
<script>
var app = angular.module('myApp', []);
app.controller('MyCtrl', function($scope) {
$scope.foo = function() { alert('foo'); }
});
app.directive('myAttr', function(){
return {
link: function(scope, element, attrs){
element.click(function(){
alert('bar');
});
}
}
});
</script>
</head>
<body ng-controller="MyCtrl">
<a ng-click="foo()" href="">click here!</a>
</body>
</html>

AngularJS and contentEditable two way binding doesn't work as expected

Why in the following example the initial rendered value is {{ person.name }} rather than David? How would you fix this?
Live example here
HTML:
<body ng-controller="MyCtrl">
<div contenteditable="true" ng-model="person.name">{{ person.name }}</div>
<pre ng-bind="person.name"></pre>
</body>
JS:
app.controller('MyCtrl', function($scope) {
$scope.person = {name: 'David'};
});
app.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
// view -> model
element.bind('blur', function() {
scope.$apply(function() {
ctrl.$setViewValue(element.html());
});
});
// model -> view
ctrl.$render = function() {
element.html(ctrl.$viewValue);
};
// load init value from DOM
ctrl.$setViewValue(element.html());
}
};
});
The problem is that you are updating the view value when the interpolation is not finished yet.
So removing
// load init value from DOM
ctrl.$setViewValue(element.html());
or replacing it with
ctrl.$render();
will resolve the issue.
Short answer
You're initializing the model from the DOM using this line:
ctrl.$setViewValue(element.html());
You obviously don't need to initialize it from the DOM, since you're setting the value in the controller. Just remove this initialization line.
Long answer (and probably to the different question)
This is actually a known issue: https://github.com/angular/angular.js/issues/528
See an official docs example here
Html:
<!doctype html>
<html ng-app="customControl">
<head>
<script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<form name="myForm">
<div contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>Change me!</div>
<span ng-show="myForm.myWidget.$error.required">Required!</span>
<hr>
<textarea ng-model="userContent"></textarea>
</form>
</body>
</html>
JavaScript:
angular.module('customControl', []).
directive('contenteditable', function() {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if(!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html(ngModel.$viewValue || '');
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$apply(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
});
Plunkr
Here is my understanding of Custom directives.
The code below is basic overview of two way binding.
you can see it working here as well.
http://plnkr.co/edit/8dhZw5W1JyPFUiY7sXjo
<!doctype html>
<html ng-app="customCtrl">
<head>
<script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script>
angular.module("customCtrl", []) //[] for setter
.directive("contenteditable", function () {
return {
restrict: "A", //A for Attribute, E for Element, C for Class & M for comment
require: "ngModel", //requiring ngModel to bind 2 ways.
link: linkFunc
}
//----------------------------------------------------------------------//
function linkFunc(scope, element, attributes,ngModelController) {
// From Html to View Model
// Attaching an event handler to trigger the View Model Update.
// Using scope.$apply to update View Model with a function as an
// argument that takes Value from the Html Page and update it on View Model
element.on("keyup blur change", function () {
scope.$apply(updateViewModel)
})
function updateViewModel() {
var htmlValue = element.text()
ngModelController.$setViewValue(htmlValue)
}
// from View Model to Html
// render method of Model Controller takes a function defining how
// to update the Html. Function gets the current value in the View Model
// with $viewValue property of Model Controller and I used element text method
// to update the Html just as we do in normal jQuery.
ngModelController.$render = updateHtml
function updateHtml() {
var viewModelValue = ngModelController.$viewValue
// if viewModelValue is change internally, and if it is
// undefined, it won't update the html. That's why "" is used.
viewModelValue = viewModelValue ? viewModelValue : ""
element.text(viewModelValue)
}
// General Notes:- ngModelController is a connection between backend View Model and the
// front end Html. So we can use $viewValue and $setViewValue property to view backend
// value and set backend value. For taking and setting Frontend Html Value, Element would suffice.
}
})
</script>
</head>
<body>
<form name="myForm">
<label>Enter some text!!</label>
<div contenteditable
name="myWidget" ng-model="userContent"
style="border: 1px solid lightgrey"></div>
<hr>
<textarea placeholder="Enter some text!!" ng-model="userContent"></textarea>
</form>
</body>
</html>
Hope, it helps someone out there.!!
Check this angularjs directive
https://github.com/clofus/angular-inputnlabel
http://clofus.com/viewarticles/109
You may run into issues using #Vanaun's code if a scope.$apply is already in progress. In this case I use $timeout instead which resolves the issue:
Html:
<!doctype html>
<html ng-app="customControl">
<head>
<script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<form name="myForm">
<div contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>Change me!</div>
<span ng-show="myForm.myWidget.$error.required">Required!</span>
<hr>
<textarea ng-model="userContent"></textarea>
</form>
</body>
</html>
JavaScript:
angular.module('customControl', []).
directive('contenteditable', function($timeout) {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if(!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html(ngModel.$viewValue || '');
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
$timeout(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
});
Working Example: Plunkr

Resources