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

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.

Related

Cannot get '&' prefix working when defining isolated scope in basic angular directive

I'm boning up on Angular directives and having trouble with the isolated scope. I'm making a VERY basic directive/controller and simply trying to throw an alert on an ng-click from within a directive by setting a scope attribute to the '&' prefix. I've been staring at this for 45 minutes now and can't see what I'm doing wrong, but nothing is happening when clicking the button (the other 2 prefixes are working though). Here is the code for the directive/controller:
(function() {
'use strict';
angular
.module('mePracticing')
.directive('practiceDirective', practiceDirective);
function practiceDirective() {
var directive = {
restrict: 'AE',
scope: {
movie: '=',
rating: '#',
display: '&'
},
template: '<div>Movie: {{movie}}</div>' +
"Enter a movie: <input type ='text' ng-model='movie'>" +
"<div>Rating: {{rating}}</div>" +
"<div><button ng-click='displayMovie(movie)'>CLICK ME</button></div>"
};
return directive;
}
})();
(function() {
'use strict';
angular
.module('mePracticing')
.controller('practiceController', practiceController);
function practiceController($scope) {
$scope.movie = 'The Big Lebowski';
$scope.rating = 1000101010;
$scope.displayMovie = function(movie) {
alert("Movie :" + movie);
}
}
})();
And here is the HTML:
<html>
<head>
<title>Directive Practice</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body ng-app="mePracticing">
<h1>HEADER</h1>
<div ng-controller="practiceController">
<practice-directive movie="movie" display="display(movie)" rating="{{rating}}"></practice-directive>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
<script src="app.js"></script>
<script src="myDirective.js"></script>
</body>
</html>
The app.js is just one line where I create my module:
angular.module('mePracticing', []);
If anyone can give me any tips on why that '&' isn't working I'd really appreciate it. Thank you!
EDIT: I made a JSFiddle for it: https://jsfiddle.net/hozxz5n4/
It occurs because when you are using the directive there is not a function called "display", you need to pass the declared function:
<practice-directive movie="movie" display="displayMovie(movie)" rating="{{rating}}"></practice-directive>
and finally in the directive just call display prop function:
(function() {
'use strict';
angular
.module('mePracticing')
.directive('practiceDirective', practiceDirective);
function practiceDirective() {
var directive = {
restrict: 'AE',
scope: {
movie: '=',
rating: '#',
display: '&'
},
template: '<div>Movie: {{movie}}</div>' +
"Enter a movie: <input type ='text' ng-model='movie'>" +
"<div>Rating: {{rating}}</div>" +
"<div><button ng-click='display()'>CLICK ME</button></div>"
};
return directive;
}
})();
The directive has its own isolated scope, so your displayMovie() function introduced in the practiceController never get's called. Move the function to your directive like so:
function practiceDirective() {
var directive = {
restrict: 'AE',
scope: {
movie: '=',
rating: '#',
display: '&'
},
link: function(scope, element, attrs) {
scope.displayMovie = function(movie) {
alert("Movie :" + movie);
}
},
template: '<div>Movie: {{movie}}</div>' +
"Enter a movie: <input type ='text' ng-model='movie'>" +
"<div>Rating: {{rating}}</div>" +
"<div><button ng-click='displayMovie(movie)'>CLICK ME</button></div>"
};
return directive;
}
Working fiddle:
https://jsfiddle.net/hozxz5n4/1/
Also take a look at this great answer.

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

AngularJS - binding/linking two directives together

What is the preferred way to link/bind two directives together? I have a controller with two directives, first directive is a select element, after selecting option, second directive should process selected item value.
App code:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function() {
var sharedData = { selectedId: '' };
var vm = this;
vm.sharedData = sharedData;
});
app.directive('directiveA', ['$compile', function($compile) {
return {
restrict: 'E',
scope: {
selectedId: '='
},
template: '<select data-ng-model="vm.sharedData.selectedId" data-ng-options="currentSelect.Id as currentSelect.Name for currentSelect in vm.sharedData.availableSelects track by currentSelect.Id"><option value="">Select option</option></select><p>Directive A, selected ID: {{vm.sharedData.selectedId}}</p>',
bindToController: true,
controllerAs: 'vm',
controller: function() {
vm = this;
vm.sharedData = {
availableSelects: [
{Id:1, Name: 'Option 1'},
{Id:2, Name: 'Option 2'},
{Id:3, Name: 'Option 3'},
{Id:4, Name: 'Option 4'}
]
}
vm.logMessage = logMessage;
function logMessage(selectedId) {
console.log('directiveA: ' + selectedId);
}
},
link: function($scope, elem, attr, ctrl) {
attr.$observe('selectedId', function(selectedId) {
ctrl.logMessage(selectedId);
});
}
};
}]);
app.directive('directiveB', ['$compile', function($compile) {
return {
restrict: 'E',
scope: {
selectedId: '='
},
template: '<p>Directive B, selected ID: {{vm.sharedData.selectedId}}</p>',
bindToController: true,
controllerAs: 'vm',
controller: function() {
vm = this;
vm.logMessage = logMessage;
function logMessage(selectedId) {
console.log('directiveB: ' + selectedId);
}
},
link: function($scope, elem, attr, ctrl) {
attr.$observe('selectedId', function(selectedId) {
ctrl.logMessage(selectedId);
});
}
};
}]);
HTML code:
<!DOCTYPE html>
<html data-ng-app="plunker" data-ng-strict-di>
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link href="style.css" rel="stylesheet" />
<script data-semver="1.4.1" src="https://code.angularjs.org/1.4.1/angular.js" data-require="angular.js#1.4.x"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl as vm">
<p>MainCtrl, selected ID: {{vm.sharedData.selectedId}}</p>
<directive-a data-selected-id="vm.sharedData.selectedId"></directive-a>
<directive-b data-selected-id="vm.sharedData.selectedId"></directive-b>
</body>
</html>
Here is a Plunker example:
http://plnkr.co/edit/KVMGb8uAjUwD9eOsv72z?p=preview
What I'm doing wrong?
Best Regards,
The key issue revolves around your use of isolated scopes:
scope: {
selectedId: '='
},
With controllerAs binding:
controllerAs: 'vm',
What this essentially does, to put it basically, is it places the view model onto the directives scope, accessed through the alias you assign in the controllerAs. So basically in your html when you go:
<directive-a data-selected-id="vm.sharedData.selectedId"></directive-a>
You are actually accessing the directive-a view model, NOT the MainCtrl view model. BECAUSE you set directive-a as having an isolate scope... which is a new scope, isolated from the MainCtrl.
What you need to do is more along the following lines:
http://plnkr.co/edit/wU709MPdqn5m2fF8gX23?p=preview
EDIT
TLDR: I would recommend having unique view model aliases (controllerAs) when working with isolated scopes to properly reflect the fact that they are not the same view model.

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>

AngularJs Attribute Directive 2 Way Binding

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) {
}
};
});

Categories

Resources