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
Related
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.
I'm trying to use Angular in a more web-component style. So I have created an http-request directive that has a url and a response attribute. It works quite well but my directive is reliant on a template and I would like to remove that as it is hacky and the directive doesn't need a template. Here is my code
<div>
<http-request url="http://jsonplaceholder.typicode.com/posts" response="items"></http-request>
<ul>
<li ng-repeat="item in items">{{ item.id }}</li>
</ul>
</div>
var myApp = angular.module('myApp', []);
myApp.directive('httpRequest', ['$http', function ($http) {
return {
restrict: 'E',
replace: true,
scope: {
response: '='
},
template: '<input type="text" ng-model="response" style="display:none"/>',
link: function (scope, element, attributes) {
$http.get(attributes.url)
.then(function (response) {
scope.response = response.data;
});
}
}
}]);
Fiddle: http://jsfiddle.net/HB7LU/9558/
Update your directive to the following:
myApp.directive('httpRequest', ['$http', function ($http) {
return {
restrict: 'E',
replace: true,
scope: {
response: '='
},
link: function (scope, element, attributes) {
//create response object if it doesn't exist
scope.response = scope.response || {};
$http.get(attributes.url)
.then(function (response) {
//write to items property of response object
scope.response.items = response.data;
});
}
}
}]);
Then loop over your response.items where you use the directive:
<http-request url="http://jsonplaceholder.typicode.com/posts" response="response">
</http-request>
<ul>
<li ng-repeat="item in response.items">{{ item.id }}</li>
</ul>
Updated fiddle.
The way you were doing it (with the template inside the directive) was reassigning the reference inside the isolate scope to be the $http data. This was then being bound to the ng-model="response" (through the watch) and published back out through the two way binding. You are also using an old version of angular. Newer versions look like you don't need to do this work around, just remove the template.
Newer angular fiddle.
Edit:
Since you said you didn't like binding to an items property. You can change your directive to look like this (uses $parse service to set the value on scope). This works with the older version of angular too:
myApp.directive('httpRequest', ['$http', '$parse', function ($http, $parse) {
return {
restrict: 'E',
replace: true,
link: function (scope, element, attributes) {
//use $parse to assign this value to scope
var responseSetter = $parse(attributes.response).assign;
$http.get(attributes.url)
.then(function (response) {
//call the "setter" against scope with the data
responseSetter(scope, response.data);
});
}
}
}]);
Demo.
Your directive doesn't have to have a template, since there's nothing you need to render visually. All you're doing is setting scope variables to encapsulate the state of the request as it's being made and then reacting to the response status and data.
Have a look at https://github.com/coding-js/directives/tree/solutions/datasource from a recent JS meetup I helped run.
I have an infinitely nested data structure, where there is a top level object that has a collection of objects, and each of these objects can also have a collection of objects.
I need to iterate through this tree, which I am currently doing like so:
collection.js
app.directive('collection', function() {
return {
restrict: 'E',
replace: true,
scope: {
collection: '='
},
templateUrl: 'collection.html'
};
});
collection.html
<ul>
<member ng-repeat="member in collection" member="member"></member>
</ul>
member.js
app.directive('member', function($compile) {
return {
restrict: 'E',
replace: true,
scope: {
member: '='
},
templateUrl: 'member.html',
link: function(scope, element, attrs) {
var collection = '<collection collection="member.children"></collection>';
if (scope.member.children) {
$compile(collection)(scope, function(cloned, scope) {
element.append(cloned);
});
}
}
};
});
member.html
<li>
{{ member }}
</li>
index.html
<div data-ng-controller="collectionController">
<collection collection="collection"></collection>
</div>
I need to be able to click on a member, no matter how nested it is, and set the controller's selectedMember property as that member.
so something like this:
app.controller('collectionController', function($scope, collection) {
collection.getCollection().then(function(collection) {
$scope.collection = collection;
});
$scope.selectMember = function(member) {
$scope.selectedMember = member;
};
});
Since I'm calling a function defined in the parent scope (the controller), I think I need to pass down the selectMember function like this:
index.html
...
<collection collection="collection" select-member="selectMember"></collection>
...
collection.html
<member ng-repeat="member in collection" member="member"
select-member="selectMember()" ng-click="selectMember(member)">
</member>
collection.js
...
scope: {
collection: '=',
selectMember: '&selectMember'
}
...
member.js
...
scope: {
member: '=',
selectMember: '='
}
...
I just can't seem to get the function to trigger correctly and set the controller scope's selectedMember property. The parameter passed to the selectMember function is undefined.
I think it's obvious that I'm misunderstanding something about scopes, but the nested nature of the problem I have to solve isn't making things easier.
Any ideas?
Edit:
Heres a plunker: http://plnkr.co/edit/JfxpoLLgpADs9RXSMife
Yes, I think the approach you're taking is correct - i.e. passing the click handler from the outer scope. There is just some minor confusion with how to pass the handler. I wish you created a plunker, but I'll try to go blind. :)
index.html
<collection collection="collection" select-member="selectMember(member)"></collection>
collection.html template
<member ng-repeat="item in collection"
member="item"
select-member="selectMember({member: member})"></member>
collection.js
...
scope: {
collection: '=',
selectMember: '&'
}
...
member.html template
<li ng-click="selectMember({member: member})>{{ member }}</li>
In addition, when you add <collection> for member.children:
<collection collection="member.children"
select-member="selectMember({member: member})"></collection>
member.js
...
scope: {
member: '=',
selectMember: '&'
}
...
EDIT:
Ok, this wasn't trivial :) But it was fun.
A few modifications:
select-member shouldn't just "pass a function" as I incorrectly suggested.
ng-click wasn't properly firing when it was declared on a member - it was firing for both child and parent. I moved it to member.html template.
For clarity, I used item with ng-repeat: ng-repeat="item in collection"
I'm correcting the above code. I also created a fork of you plunker.
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>
I need some help with getting AngularJS to maintain my non-string values in directive attributes.
I was looking for a way to render a tree structure in HTML from a piece of JSON, and I found this code: http://jsfiddle.net/n8dPm/
I've been trying to adapt that for my project, as shown in the code below.
My controller/directive is shown here:
cxpControllers.controller("ProductTocCtrl", ["$scope", "$http", "$routeParams",
function ProductTocController($scope, $http, $routeParams) {
$scope.typeOf = typeOf;
//test value
$scope.contents = {
"id": 1,
"name": "Test",
subsections: [
{
id: 2,
name: "Test1.1",
link: "test11.xml",
test: 34
},
{
id: 3,
name: "Test1.2",
link: "test12.xml",
test: 95
}
]
}
}]);
cxpControllers.directive('tree', function($compile) {
return {
restrict: 'E',
scope: {key: "=", content: "="},
templateUrl: "tree_renderer.html",
compile: function(tElement, tAttr) {
var contents = tElement.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
if(!compiledContents) {
compiledContents = $compile(contents);
}
compiledContents(scope, function(clone, scope) {
iElement.append(clone);
});
};
}
};
});
And then this is my template:
<script type="text/ng-template" id="tree_renderer.html">
{{key}}:
<ul ng-if="typeOf(content) == 'object' && content != null">
<li ng-repeat="(key, content) in content">
<tree key="key" content="content"></tree>
</li>
</ul>
<span ng-if="typeOf(content) != 'object'">
"{{content}}"
</span>
</script>
<ul>
<li ng-repeat="(key, content) in contents">
<tree key="key" content="content"></tree>
</li>
</ul>
This would work, except for one problem. Angular is turning the value of "content" into a string, preventing the recursion from working because it can't iterate over a string.
I have seen other questions like this, for example here, but their problem is that they used "#" in the directive scope, which converts to a string. But since I'm using "=", it should maintain the type.
Here's the output I'm seeing with the test data shown in the code above:
I would appreciate any help you can give. If you need more information I'll be happy to supply it.
The problem is with the typeOf function in your template. The compiled template doesn't find this function so it is never equal to 'object'. Add a controller to your directive to define it.
I took the plunkr and added this:
controller: function($scope) {
$scope.typeOf = function(val) {
return typeof val;
};
},
It does recognize it as an object. Check out the updated plunkr here.