angularjs nested ng-repeat scopes with custom directives - javascript

I have an angular project using angularjs 1.3.7 and cannot figure out why an object I'm passing with an attribute from a parent to a child (each with their own isolated scopes), will not pass properly. Logging to the console from the child directive will show this object as undefined whereas logging to the console from the parent will show the object as intended. Below is a simplified version of what I'm currently working with.
Main View Template:
<parent-directive>
Parent Directive:
HTML:
<div>
<div ng-repeat="foo in parentCtrl.foos">
<child-directive foo="foo" bar="parentCtrl.bar"></child-directive>
</div>
</div>
javascript:
angular
.module('parentDirectiveModule', [])
.directive('parentDirective', function() {
var parentDirectiveCtrl = ['$scope', function($scope){
var parentCtrl = this;
parentCtrl.foos = [
{'name': 'foo1', 'id': '1'},
{'name': 'foo2', 'id': '2'}
];
parentCtrl.bar = {'property': 'name'};
return {
scope: {},
templateUrl: '../parentDirective.html',
restrict: 'E',
replace: false,
controller: parentDirectiveCtrl,
controllerAs: 'parentCtrl',
bindToController: true
}
}];
Child Directive:
HTML:
<div>
<span>{{childCtrl.foo}}</span>
<button ng-click="childCtrl.editBar()">{{childCtrl.bar.property}}</button>
</div>
javascript:
angular
.module('childDirectiveModule', [])
.directive('childDirective', function() {
var childDirectiveCtrl = ['$scope', function($scope){
var childCtrl = this;
console.log(childCtrl.foo);
console.log(childCtrl.bar);
childCtrl.editBar = function() {
// update bar and reflect in the parent controller
};
return {
scope: {
foo: '=',
bar: '='
},
templateUrl: '../childDirective.html',
restrict: 'E',
replace: false,
controller: childDirectiveCtrl,
controllerAs: 'childCtrl',
bindToController: true
}
}];
Using the above code, the console log on childCtrl.foo returns the foo object as expected, but the childCtrl.bar returns as undefined.
Thanks in advance
EDIT: fixed a spelling
EDIT EDIT: closed an open " and changed bar to parentCtrl.bar on

Try changing the bar="bar" assignment to this:
<div>
<div ng-repeat="foo in parentCtrl.foos">
<child-directive foo="foo" bar="parentCtrl.bar"></child-directive>
</div>
</div>
At least with a quick glance that might be the cause for your problem, bar being genuinely undefined.

Thanks everyone for your help. So it looks like my issue is actually related to the way angular handles camel-case conversions when copying an attribute into an isolated scope. On the child-directive, I have the property bar="parentCtrl.bar". In actuality, I have a camel case name for this attribute which is something more akin to fooBar="parentCtrl.bar". In the declaration of the isolated scope, angular will pass this attibute as foobar and not as the fooBar I was expecting. As a result, every time I console logged childCtrl.fooBar, I would get undefined.
To fix this problem, I changed the attribute name to foo-bar. Angular did not need to do any more conversions and the object passed through as expected.

Related

Angular two-way data binding isolate scope directive but property is undefined?

Hello I think I don't understand what two-way data binding is. First the code:
.directive('mupStageButtons', function() {
return {
transclude: true,
template: '<span ng-transclude></span>',
replace: true,
scope: {
property: "=",
action: "="
},
controller: function($scope) {
console.log($scope); //I can see the property of $scope defined in console
console.log($scope.property); //undefined
this.property = $scope.property;
this.changeStage = $scope.action; //anyway this is ok
},
};
})
.directive('mupStageButton', function() {
return {
transclude: true,
templateUrl: '/static/templates/directives/StageButton.html',
require: '^^mupStageButtons',
scope: {
value: "=",
btnClass: "#",
},
link: function(scope, element, attrs, mupStageButtonsCtrl, transclude) {
scope.property = mupStageButtonsCtrl.property;
scope.changeStage = mupStageButtonsCtrl.changeStage;
}
};
})
//html
<mup-stage-buttons property="company.stage" action="setStage">
<mup-stage-button value="0" btn-class="btn-default-grey">
</mup-stage-button>
</mup-stage-buttons>
//controller for that html ^^^
.controller('CompanyDetailController', function($scope, $stateParams, Company){
Company.query ({
id : $stateParams.companyId
}, function (data) {
$scope.company = new Company(data);
});
}
//template for <mup-stage-button>
<label ng-class="property === value ? 'active' : 'btn-on-hover' " class="btn {{btnClass}}" ng-click="changeStage(value)">
<div ng-transclude></div>
</label>
Does the "=" mean, that the change in outside scope will propagate thanks to data binding? Or not? Because I fetch a $resource and it is of course defined after the time it is fetched, but the "property" remains undefined. So what is wrong?
EDIT: desired behavior is that the ng-class in the template for <mup-stage-button> works
EDIT: plunker: https://plnkr.co/edit/drXxyMpd2IOhXMWFj8LP?p=preview
You are missing an important thing about the transclude option: the wrapped content is bound to the OUTER scope rather than the directive's scope.
So, here how the scope bindings will look in your case after compilation:
<div ng-controller="CompanyDetailController">
<mup-stage-buttons property="company.stage" action="setStage"> <-- even though the 'property' is bound correctly, it is not available below due to transclusion -->
<span ng-transclude>
{{company.stage}} <!-- CompanyDetailController $scope available here due to transclusion, 'property' is not available! -->
<mup-stage-button property="company.stage" value="0">
<!-- directive's scope here, binding to the outer scope's 'company.stage' can be used here -->
{{property}} - {{value}} <!-- this will work -->
<label ng-class="property === value ? 'active' : 'btn-on-hover' " class="btn {{btnClass}}" ng-click="changeStage(value)">
<div ng-transclude>
<!-- transcluded content here, bound to the CompanyDetailController $scope -->
not working ng-class 0
</div>
</label>
</mup-stage-button>
</span>
</mup-stage-buttons>
</div>
So, to make your code work (Plunk) it would be enough to map the property to the company.stage on the child directive only.
UPDATE
To avoid repetition of the property="company.stage" binding on the child directives and pass the data through the controller and link function of the parent and child directives respectively, you should use the wrapping object for you scope properties, so that you could pass the reference to that object through. Any changes to this object will be available to the child scopes as they will have a reference to that object, this is called the dot notation:
CompanyDetailController:
$scope.vars = {};
this.getCompany = function () {
$scope.vars.company = $scope.company = {stage: 0};
};
then bind the vars property to the parent directive's scope:
// ...
scope: {
vars: '=',
},
controller: function($scope) {
this.vars = $scope.vars;
}
// ...
then put the reference of vars to the child directive's scope:
// ...
link: function(scope, element, attrs, mupStageButtonsCtrl, transclude) {
scope.vars = mupStageButtonsCtrl.vars;
}
// ...
and finally have access to it in the child directive's view:
<label ng-class="vars.company.stage === value ? 'active' : 'btn-on-hover'">...</label>
This way there is no need to repeat the bindings on the child directive instances.
Plunk is updated.
In javascript
Primitives are passed by value, Objects are passed by "copy of a
reference".
good explanation stackoverflow.com/questions
Solution using $watch:
.directive('mupStageButtons', function() {
return {
transclude: true,
template: '<span ng-transclude></span>',
replace: true,
scope: {
property: "=",
action: "="
},
controller: function($scope) {
that = this;
$scope.$watch('property', function(newValue){
that.property = newValue;
/***Refresh this.property (normal assignment would only copy value,
it would not behave as a reference to desired transcluded property)***/
});
this.changeStage = $scope.action;
},
};
})
.directive('mupStageButton', function() {
return {
transclude: true,
templateUrl: '/static/templates/directives/StageButton.html',
require: '^^mupStageButtons',
scope: {
value: "=",
btnClass: "#",
},
link: function(scope, element, attrs, mupStageButtonsCtrl, transclude) {
scope.btnCtrl = mupStageButtonsCtrl;
scope.changeStage = mupStageButtonsCtrl.changeStage;
}
};
})
An important part besided the $watch is also this in link function:
scope.btnCtrl = mupStageButtonsCtrl;
We could not do
scope.property = mupStageButtonsCtrl.property;
because it would just copy the value, and when it changed in the ctrl, it wouldn't change here in the child directive.
So we assign ctrl reference to scope.btnCtrl and it works.
Template for child directive:
<label ng-class="btnCtrl.property === value ? 'active' : 'btn-on-hover' " class="btn {{btnClass}}" ng-click="changeStage(value)">
<div ng-transclude></div>
</label>
Now I can use the directives generically as I need - pass just the property like company.stage, so that the directive doesn't need to know the property name (stage).
<mup-stage-buttons property="company.stage" action="setStage">
<mup-stage-button value="0" btn-class="btn-default-grey">
Stage 0
</mup-stage-button>
</mup-stage-buttons>

Pass directive attribute into linked controller?

I have a directive i'm using to do the same search filtering across multiple pages. So the directive will be using a service and get pretty hefty with code. Because of that I want to link to a controller instead of have the controller inside the directive like this:
.directive('searchDirective', function($rootScope) {
return {
restrict: 'E',
templateUrl:'searchtemplate.html',
controller: 'searchCtrl',
controllerAs: 'search'
};
});
I also want access to parent scope data inside the template, so I don't want to use a isolated scope.
Anyway here's what i'm not sure how to do. My directive looks like this:
<search-directive filter="foo"/>
How do I pass in the value in the filter attribute so that I can access it in my controller using $scope.filter or this.filter?
If I were using an isolated scope it'd be simple. If i had the controller in the same page I could use $attrs. But since i'm using a controller from another spot and don't want an isolated scope i'm not sure how to get the attrs values into the controller.
Any suggestions?
What about using the link function and passing the value to the scope?
return {
restrict: 'E',
templateUrl:'searchtemplate.html',
controller: 'searchCtrl',
controllerAs: 'search',
link: function (scope, element, attr) {
scope.filter = attr.filter;
}
};
searchDirective.js
angular
.module('searchDirective', []).controller('SearchCtrl', SearchCtrl)
.directive('SearchDirective', directive);
function directive () {
var directive = {
templateUrl:'searchtemplate.html',
restrict: "E",
replace: true,
bindToController: true,
controller: 'searchCtrl as search',
link: link,
scope: { filter:'=' } // <-- like so here
};
return directive;
function link(scope, element, attrs) {}
}
SearchCtrl.$inject = [
'$scope',
'$filter'];
function SearchCtrl(
$scope,
$filter) {
/** Init SearchCtrl scope */
/** ----------------------------------------------------------------- */
var vs = $scope;
// ....
Also I highly recommend checking out this AngularJS style guide, how you are writing your directive above is how I use to do it too. John Papa shows some way better ways: https://github.com/johnpapa/angular-styleguide
Directives:
https://github.com/johnpapa/angular-styleguide#directives
Controllers:
https://github.com/johnpapa/angular-styleguide#controllers
Flip the values of bindToController and scope around.
{
....
scope: true,
bindToController: { filter:'=' }
...
}
I have just hit the same issue over the weekend, and made a simple complete example here: bindToController Not Working? Here’s the right way to use it! (Angular 1.4+)

Appending to an array that's tied to an Angular.JS directive

In one of my Angular.JS controllers, I have the following:
app.controller("MyController", ["$scope", function($scope){
$scope.messages = [
new Message(1),
new Message(2)
];
$scope.addMessage = function(x) { $scope.messages.push(new Message(x)); }
}]);
Then in my main HTML page, I have
<message message="message" ng-repeat="message in messages">
This is bound to a directive:
app.directive("message", function() {
return {
restrict: "E",
scope: {
message: "="
},
templateUrl: "js/Directives/message.html"
};
});
The template file is:
<li class="message">{{message.msg}} </li>
However, when I call addMessage on the controller, while it does add to $scope.messsages, it doesn't actually refresh the ng-repeat and display the new message. How can I do this?
I would suggest some structural changes in your directive.
First of all, why not refer the original array itself instead of referring value at each iteration ??
<message messages="messages">
Then you can actually move ng-repeat part in your directive template, [You must note that since you're using = in message: "=", = binds a local/directive scope property to a parent scope property. So with =, you use the parent model/scope property name as the value of the DOM attribute. ].
Hence your directive will look like :
app.directive("message", function() {
return {
restrict: "E",
scope: {
messages: "="
},
templateUrl: "js/Directives/message.html"
};
});
and the subsequent template will look something like this :
<ul>
<li ng-repeat="message in messages" class="message">{{message.msg}} </li>
</ul>
You can find a demo plunker here

Angular, adding scope is breaking my directive

I am having trouble getting this directive to work properly, It was working fine until I tried adding a scope to link a function. Here's what I have so far
The directive (in requirejs format hence the module/component name stuff) :
angular.module(metadata.moduleName, [
filterFieldControl.moduleName
]).directive(metadata.componentName,
function(scope) {
return {
controller: filterFieldControl.componentName,
restrict: 'C',
replace: true,
scope: {
filterFn: '='
},
template: builderResultFiltersHTML
};
}
);
The controller
angular.module(metadata.moduleName, []).controller(metadata.componentName, [
'$scope',
function($scope) {
$scope.filterChange = function() {
$scope.filterFn();
};
}
]);
The template:
<div>
<ul>{{filter.name}}
<li ng-repeat="value in filter.values">
<input type="checkbox" ng-model="filterObject[filter.name][value]" ng-change="filterChange()">{{value}}
</li>
</ul>
Where it is being used (and maybe some of the issue is it's being repeated?)
<div ng-repeat="result in searchResults.results" class="builder-search-result" filter-fn="filterClicked" ></div>
So if I take out the scope from the directive it works fine. If i add it in, there are no errors in console but nohting shows up.
If I remove this, it works :
scope: {
filterFn: '='
},
Can't seem to figure out why. Could use some ideas.
Edit: added fiddle here https://jsfiddle.net/vt1uasw7/31/ - you will notice if you remove the scope part of the directive, everything shows up fine.
When you do scope: { ... } in a directive, you're telling Angular to create an isolate scope, which is a scope that doesn't prototypally inherit from any other scope.
Now, take a look at your directive's template:
<div>in
<ul>{{filter.name}}
<li ng-repeat="value in filter.values">
<input type="checkbox" ng-change="filterChange()">{{value}}
</li>
</ul>
</div>
Both filter and filterChange() are defined in the controller's scope, and your directive can't access them because it has its own private scope.
You can fix your code by doing one of two things:
Use $parent.filter and $parent.filterChange();
Add filter and filterChange to the directive's isolate scope:
scope: {
filter: '=',
filterChange: '&'
}
I suggest #2. Here's your updated fiddle.

$scope.item in directive is undefined

I have a problem with my directive and controller. The variable item in scope is undefined in directive, even though I passed it in html. This is my code:
app.js:
var app = angular.module("app", ["ngRoute"]);
app.config(["$routeProvider", function($routeProvider) {
$routeProvider
.when("/", {
templateUrl: "views/main.html",
controller: "MainCtrl"
});
}]);
controller.js:
app.controller("MainCtrl", function($scope) {
$scope.item = "x";
});
directive.js:
app.directive("exampleDirective", function() {
return {
restrict: "A",
scope: {
item: "="
},
templateUrl: "views/item.html"
};
});
index.html:
<div ng-view></div>
main.html:
<div example-directive item="item"></div>
item.html:
<div>{{ item }}</div>
UPDATE
I changed my code to:
app.directive("exampleDirective", function() {
return {
restrict: "A",
scope: true,
templateUrl: "views/item.html"
};
});
and now there is "x" in scope.$parent.item. But why it isn't present inside directive?
Although it seems to work just fine with latest Angular stable http://plnkr.co/edit/6oXDIF6P04FXZB335voR?p=preview maybe you are trying to use templateUrl thats pointing to somewhere it doesn't exist.
Another thing, use primitives only when strictly needed. In case you ever need to modify value of item, you won't be able to do so since you are using a primitive. Plus, if you need "more info" to go inside your isolated scopes, and to avoid attribute soup (attr-this="that", attr-that="boop", my-otherstuff="anotheritem.member", etc) you can pass the two-way bind of an object that handle more data.
Or if you need to share state through multiple controllers, directives, etc, use a service instead and use dependency injection, and there's no need to pass in objects/primitives to your isolated scope, and you can assure state, and that's best practice "the Angular way".
This fiddle works: http://jsfiddle.net/HB7LU/2844/ which is essentially the same thing just without the route information.
var myApp = angular.module('myApp',[]);
myApp.directive("exampleDirective", function() {
return {
restrict: "A",
scope: {
item: "="
},
template: "<div>{{ item }}</div>"
};
});
function MyCtrl($scope) {
$scope.item = 'Superhero';
}
With the view:
<div ng-controller="MyCtrl">
<div example-directive item="item"></div>
</div>
This leads me to believe it could be a scope issue. So try encapsulating the controller scope variable in a container:
$scope.cont = { item: 'x' };
And in the view
<div example-directive item="cont.item"></div>

Categories

Resources