I have created a directive as a wrapper for md-autocomplete so that it's easier to re-use. In the parent controller, I have an object. I want to pass the keys of the object to my custom directive, but I'm having trouble. Simplified code, without md-autocomplete:
Here's the script
var app = angular.module('myApp',[])
.controller('parentController', function(){
var parent = this;
parent.things = {item1: {color: "blue"}, item2: {color: "red"}};
})
.directive('childDirective',function(){
return {
scope: {},
bindToController: {
items:'&'
},
controller: childController,
controllerAs: 'child',
template: '<pre>{{child.items | JSON}}<pre>' //should be [item1,item1]
}
function childController(){
//Just a dummy controller for now
}
})
HTML
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="Object.keys(parent.things)">
</my-directive>
</div>
TL;DR: How do I pass the keys of an object defined in the parent controller to a child directive? I need to pass just the keys, not the object itself, because my directive is designed to deal with an array of strings.
Try using a directive with local scope from user attribute (=)
app.directive('childDirective', function() {
return {
replace: true,
restrict: 'E',
scope: {
items: '='
},
template: '<pre>{{items | JSON}}<pre>'
};
});
Using the directive, object in attribute "items" is passed "as is" , as a scope variable "items"
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="getKeys(parent.things)">
</my-directive>
</div>
Using Object.keys(obj) as source will cause an infinite loop digest (the function is always returning a new different object). You need a function to save the result to a local updatable object, like in this example:
https://jsfiddle.net/FranIg/3ut4h5qm/3/
$scope.getKeys=function(obj){
//initialize result
this.result?this.result.length=0:this.result=[];
//fill result
var result=this.result;
Object.keys(obj).forEach(function(item){
result.push(item);
})
return result;
}
I'm marking #Igor's answer as correct, because ultimately it led me to the right place. However, I wanted to provide my final solution, which is too big for a comment.
The search for the answer to this question led me to create a directive that is more flexible, and can take several different types of input.
The real key (and my actual answer to the original question) was to bind the items parameter to a proxy getter/setter object in the directive. The basic setup is:
app.directive('myDirective',function(){
return {
...
controller: localControl,
bindToController: {
items: '<' //note one-way binding
}
...
}
function localControl(){
var child = this;
child._items = [],
Object.defineProperties(child,{
items: {
get: function(){return child._items},
set: function(x){child._items = Object.keys(x)}
}
});
}
});
HTML
<my-directive items="parent.items">
<!-- where parent.items is {item1:{}, item2:{}...} -->
</my-directive>
Ultimately, I decided I wanted my directive to be able to accept a variety of formats, and came up with this plunk as a demonstration.
Please feel free to offer comments/suggestions on improving my code. Thanks!
Related
I want to create a view in angular.js where I add a dynamic set of templates, each wrapped up in a directive. The directive names correspond to some string property from a set of objects. I need a way add the directives without knowing in advance which ones will be needed.
This project uses Angular 1.5 with webpack.
Here's a boiled down version of the code:
set of objects:
$scope.items = [
{ name: "a", id: 1 },
{ name: "b", id: 2 }
]
directives:
angular.module('myAmazingModule')
.directive('aDetails', () => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./a.html")
}))
.directive('bDetails',() => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./b.html")
}));
view:
<li ng-repeat="item in items">
<div>
<{{item.name}}-details/>
</div>
</li>
so that eventually the rendered view will look like this:
<li ng-repeat="item in items">
<div>
<a-details/>
</div>
<div>
<b-details/>
</div>
</li>
How do I do this?
I do not mind other approaches, as long as I can inline the details templates, rather then separately fetching them over http.
Use ng-include:
<li ng-repeat="item in items">
<div ng-controller="myRavishingController"
ng-include="'./'+item.name+'.html'">
</div>
</li>
I want to inline it to avoid the http call.
Avoid http calls by loading templates directly into the template cache with one of two ways:
in a script tag,
or by consuming the $templateCache service directly.
For more information, see
AngularJS $templateCache Service API Reference
You can add any html with directives like this:
const el = $compile(myHtmlWithDirectives)($scope);
$element.append(el);
But usually this is not the best way, I will just give a bit more detailed answer with use of ng-include (which actully calls $compile for you):
Add templates e.g. in module.run: [You can also add templates in html, but when they are required in multiple places, i prefer add them directly]
app.module('myModule').run($templateCache => {
$templateCache.put('tplA', '<a-details></a-details>'); // or webpack require
$templateCache.put('tplB', '<b-details></b-details>');
$templateCache.put('anotherTemplate', '<input ng-model="item.x">');
})
Your model now is:
$scope.items = [
{ name: "a", template: 'tplA' },
{ name: "b", template: 'tplB' },
{ name: "c", template: 'anotherTemplate', x: 'editableField' }
]
And html:
<li ng-repeat="item in items">
<div ng-include="item.template">
</div>
</li>
In order to use dynamic directives, you can create a custom directive like I did in this plunkr:
https://plnkr.co/edit/n9c0ws?p=preview
Here is the code of the desired directive:
app.directive('myProxy', function($compile) {
return {
template: '<div>Never Shown</div>',
scope: {
type: '=',
arg1: '=',
arg2: '='
},
replace: true,
controllerAs: '$ctrl',
link: function($scope, element, attrs, controller, transcludeFn) {
var childScope = null;
$scope.disable = () => {
// remove the inside
$scope.changeView('<div></div>');
};
$scope.changeView = function(html) {
// if we already had instanciated a directive
// then remove it, will trigger all $destroy of children
// directives and remove
// the $watch bindings
if(childScope)
childScope.$destroy();
console.log(html);
// create a new scope for the new directive
childScope = $scope.$new();
element.html(html);
$compile(element.contents())(childScope);
};
$scope.disable();
},
// controller is called first
controller: function($scope) {
var refreshData = () => {
this.arg1 = $scope.arg1;
this.arg2 = $scope.arg2;
};
// if the target-directive type is changed, then we have to
// change the template
$scope.$watch('type', function() {
this.type = $scope.type;
refreshData();
var html = "<div " + this.type + " ";
html += 'data-arg1="$ctrl.arg1" ';
html += 'data-arg2="$ctrl.arg2"';
html += "></div>";
$scope.changeView(html);
});
// if one of the argument of the target-directive is changed, just change
// the value of $ctrl.argX, they will be updated via $digest
$scope.$watchGroup(['arg1', 'arg2'], function() {
refreshData();
});
}
};
});
The general idea is:
we want data-type to be able to specify the name of the directive to display
the other declared arguments will be passed to the targeted directives.
firstly in the link, we declare a function able to create a subdirective via $compile . 'link' is called after controller, so in controller you have to call it in an async way (in the $watch)
secondly, in the controller:
if the type of the directive changes, we rewrite the html to invoke the target-directive
if the other arguments are updated, we just update $ctrl.argX and angularjs will trigger $watch in the children and update the views correctly.
This implementation is OK if your target directives all share the same arguments. I didn't go further.
If you want to make a more dynamic version of it, I think you could set scope: true and have to use the attrs to find the arguments to pass to the target-directive.
Plus, you should use templates like https://www.npmjs.com/package/gulp-angular-templatecache to transform your templates in code that you can concatenate into your javascript application. It will be way faster.
I'm trying to pass an array from a controller to a directive and for some (probably obvious to you lot!) reason when the array values are updated in the controller it does not reflect in the directive. The controller obtains data from a service into an array and I want to pass that array to the directive to create a bar graph. I've put the key parts of the code below.
Here is my top level HTML
<div dash-progress
graph-data="{{dashCtrl.myProgress}}">
</div>
<div>
Other Stuff
</div>
My template HTML for the directive:
<div class="boxcontent" ng-show="dashCtrl.showProgress">
<div class="chart-holder-lg">
<canvas tc-chartjs-bar
chart-data="progress"
chart-options="options"
height="200"
auto-legend>
</canvas>
</div>
</div>
Controller:
angular
.module('myApp')
.controller('dashCtrl',['mySvc',
function(mySvc) {
var self = this;
this.myProgress = [];
this.getProgress = function() {
//logic must be in the service !
mySvc.getProgress().then(function(success) {
self.myProgress = mySvc.progress;
});
};
}]);
and the directive:
angular
.module('myApp')
.directive('dashProgress', [function() {
return {
restrict: 'AE',
templateUrl: 'components/dashboard/progress.html',
scope: {
graphData: '#'
},
link: function(scope,el,attrs) {
scope.progress = {
labels: ['Duration','Percent'],
datasets: [
{
label: 'Duration',
data: [scope.graphData.duration]
},
{
label: 'Percent',
data: [scope.graphData.percent]
}
]
};
scope.options = { };
}
}
}]);
If I set an initial values of the myProgress object in the controller then these do get reflected in the directive, but I don't get the real values that I need when they are returned to the controller from the service.
In your directive's scope, instead of this:
scope: {
graphData: '#'
}
try using this:
scope: {
graphData: '='
}
Don't use {{ }} when passing array to the directive with =. It will render the array in the view instead of passing a reference to directive's scope.
As far as I know, # is not only one-way binding, but also one-time binding and should be used mostly for string values (e.g. setting an html attribute while initializing directive). If you'd like to use #, you should firstly convert data to JSON, then pass it to directive with {{ }}, then parse it again in directive and after any change - manually recompile the directive. But it would be a little overkill, wouldn't it?
Conclusion
Just remove the curly brackets from the view and use = to bind value to directive's scope.
View
<div dash-progress
graph-data="dashCtrl.myProgress">
</div>
Directive
scope: {
graphData: '='
},
Update
Try one more thing. In dashCtrl, wrap myProgress with an object (you can change names to be more self-explaining - this is just an example):
this.graphData = {
myProgress: []
}
this.getProgress = function() {
mySvc.getProgress().then(function(success) {
self.graphData.myProgress = mySvc.progress;
});
}
Then, pass graphData to directive:
<div dash-progress
graph-data="dashCtrl.graphData">
</div>
Finally, substitute every scope.graphData with scope.graphData.myProgress. This way you make sure that scope.graphData.myProgress always refers to the same data because it's a property of an object.
If this still doesn't work, you will probably have to use a watcher and update properties of scope.progress manually.
Spent a few hours on this already, sifted through numerous stack posts and blogs but can't seem to get this to make my model update. More specifically, I am trying to update an array item (ng-repeat). In the simple case below, I iterate over venues list, and upon toggling a "like" button, I update the server appropriately, and reflect the change on the venues item on the $scope.
in my search.html I have a directive:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
and search controller I have:
app.controller('bleh', function(Service) {
...
$scope.venues = [{ id: 1, name: 'Venue1', like: false },{ id: 2, name: 'Venue2', like: false }];
...
});
Nothing unusual there. Then in my venues-list directive:
app.directive('venues-list', function() {
function venueListController($scope, Service) {
$scope.likeToggle = function(venue, $index) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venues[$index].like= !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueListController,
scope: {
venues: '='
}
}
});
then in my venue.html I have:
<div ng-repeat="venue in venues">
<p>{{venue.name}}</p>
<button ng-click="likeToggle(venue, $index)">Like</button>
</div>
I have tried many different options:
$scope.$apply() // after updating the $scope or updating scope within apply's callback function;
$scope.$digest()
$timeout(function() { // $scope.venues[$index] .... }, 0);
safe(s,f){(s.$$phase||s.$root.$$phase)?f():s.$apply(f);}
so
safe($scope, function() { $scope.venues[$index].like = !venue.like });
I haven't yet used the link within the directive, but my venues.html template is obviously a little more elaborate than presented here.
EDIT:
Just to keep the discussion relevant, perhaps I should have mentioned - the data is coming back from the server with no issues, I am handling errors and I am definitely hitting the portion of the code where the $scope is to be updated. NOTE: the above code is a small representation of the full app, but all the fundamental pieces are articulated above.
Search Controller
Venues Service
venue-list directive and venue.html template to accompany the directive
directive controller
EDIT #2
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
Just to keep it even simpler, the above doesn't work - so really, the bottom line is my items within an array are not reflecting the updates ...
EDIT #3
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
My apologies - just to re-iterate what I was trying to say above - the above is changing the scope, however, the changes are not being reflected on the view.
Perhaps the issue is with your service and promise resolution.. Can you put a console.log there and see if the promise resolution is working fine? or Can you share that code bit. Also where are you checking for scope update? within directive or outside
OK after some refactoring I finally got it working.
The "fix" (if you want to call it that) to my specific problem was:
instead of passing an array of venues, I was iterating over the array on the parent controller, passing in a venue as an element attribute that would bind (two-way) on the isolated scope of the directive.
so, instead of:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
I now have:
<ion-content>
<venues-list ng-repeat="venue in venues" venue="venue"></venues-list>
</ion-content>
and my directive now looks like:
app.directive('venues-list', function() {
function venueController($scope, Service) {
$scope.likeToggle = function(venue) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venue.like = !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueController,
scope: {
venue: '='
}
}
});
This did the trick!
I have something like this:
.controller('contr',['$scope', '$http',function($scope, $http){
$http.post(...).success(function(){
$scope.myTextVar = "some text here";
$scope.completed == true;
});
}]);
an HTML snippet like so:
<div class="myClass" ng-if="completed == true" manipulate-header>
<p>{{myTextVar}}</p>
</div>
and the directive, for simplicity sake let's say it looks like this:
.directive('manipulateHeader',function(){
return{
restrict: 'A',
link: function(scope, elem){
console.log(angular.element(elem).find('p'));
}
}
});
The manipulate-header directive is supposed to do some manipulation of the text inside the <p></p> tag, however, it runs before {{myTextVar}} gets replaced and hence it outputs {{myTextVar}} instead of some text here.
How may i get around this problem? (i can pass the variable inside the directive scope, but i'm thinking there must be another way).
EDIT: the controller is there and working as intended. Issue is not related to it. I didn't include it to shorten the post.
If it MUST be a directive
If you're trying to do string manipulation in your link function, you're going to have a bad time. The link function is executed before the directive is compiled (that's the idea of the link function), so any bindings (ng-bind or otherwise) will not have been compiled inside of link functions.
To execute code after the compilation stage, you should use a controller. However, you cannot access the DOM in controllers (or rather, you shouldn't). So the logical solution is to instead modify the scope argument instead. I propose something like this:
angular.directive('manipulateHeader', function() {
return {
scope: {
myTextVar: '='
},
controller: function($scope, myFilter) {
// you can't use bindToController here because bindToController executes *after*
// this function
this.modifiedText = myFilter($scope.myTextVar);
},
controllerAs: 'ctrl',
// display the modified text in a template
template: '<span ng-bind="ctrl.modifiedText"></span>'
};
})
.filter('myFilter', function() {
return function(inputText) {
// do some text manipulation here
};
});
Usage:
<manipulate-header myTextVar='myTextVar'></manipulate-header>
Or:
<p>{{ myTextVar | myFilter }}</p>
You could, of course, make this an attribute instead, but best practice indicates that directives that have a template should be an element instead.
The above is only if you need this to be a directive. Otherwise, it should almost definitely be a filter.
If you need to change the $scope variable from your controller you need to isolate scope ,
scope:{
myattr='#', // this will provide one way communication , you can define in your template as <p myattr="hello"><p>
message:'&', //This allows you to invoke or evaluate an expression on the parent scope of whatever the directive is inside
message:'=' // sets up a two-way binding expression between the directive's isolate scope and the parent scope.
}
refer https://docs.angularjs.org/guide/directive
As suggested by #DanPantry - you most likely want a filter not a directive
Read this guide about using filters
https://docs.angularjs.org/guide/filter
Here is an example of such a filter (from documentation)
angular.module('myStatefulFilterApp', [])
.filter('decorate', ['decoration', function(decoration) {
function decorateFilter(input) {
//This is the actual modification of text
//That's what you are looking for
return decoration.symbol + input + decoration.symbol;
}
decorateFilter.$stateful = true;
return decorateFilter;
}])
.controller('MyController', ['$scope', 'decoration', function($scope, decoration) {
$scope.greeting = 'hello';
$scope.decoration = decoration;
}])
.value('decoration', {symbol: '*'});
I am not sure whether you defined $scope.myTextVar
in correct scope. Like, if you defined it in any controller, then directive should be under the controller scope.
Here is the updated HTML
<div ng-controller ="MainController">
<div class="myClass" manipulate-header>
<p>{{myTextVar}}</p>
</div>
</div>
JS :
app.controller('MainController', ['$scope', function($scope) {
$scope.myTextVar = "some text here";
}]);
app.directive('manipulateHerader',function(){
return{
restrict: 'A',
link: function(scope, elem){
console.log(angular.element(elem).find('p'));
}
}
});
Here is the plunker
I want to create a "Header" service to handle the title, buttons, and color of it.
The main idea is to be able to customize this header with a single line in my controllers like this:
function HomeCtrl($scope, Header) {
Header.config('Header title', 'red', {'left': 'backBtn', 'right': 'menuBtn'});
}
So I created a service (for now I'm only focussing on the title):
app.service('Header', function() {
this.config = function(title, color, buttons) {
this.title = title;
}
});
...And a directive:
app.directive('header', ['Header', function(Header) {
return {
restrict: 'E',
replace: true,
template: '<div class="header">{{title}}</div>',
controller: function($scope, $element, $attrs) {
$scope.$watch(function() { return Header.title }, function() {
$scope.title = Header.title;
});
}
};
}]);
So, this actually works but I'm wondering if there are no better way to do it.
Especially the $watch on the Header.title property. Doesn't seem really clean to me.
Any idea on how to optimize this ?
Edit: My header is not in my view. So I can't directly change the $scope value from my controller.
Edit2: Here is some of my markup
<div class="app-container">
<header></header>
<div class="content" ng-view></div>
<footer></footer>
</div>
(Not sure this piece of html will help but I don't know which part would actually...)
Thanks.
If you are using title in your view, why use scope to hold the object, rather than the service? This way you would not need a directive to update scope.header, as the binding would update it if this object changes
function HomeCtrl($scope, Header) {
$scope.header = Header.config('Header title', 'red', {'left': 'backBtn', 'right': 'menuBtn'});
}
and refer to title as
<h1>{{header.title}}</h1>
Update
Put this in a controller that encapsulates the tags to bind to the header:
$scope.$on("$routeChangeSuccess", function($currentRoute, $previousRoute) {
//assume you can set this based on your $routeParams
$scope.header = Header.config($routeParams);
});
Simple solution may be to just add to rootScope. I always do this with a few truly global variables that every controller will need, mainly user login data etc.
app.run(function($rootScope){
$rootScope.appData={
"header" : {"title" : "foo"},
"user" :{}
};
});
.. then inject $rootScope into your controllers as warranted.