Changing property of controller from directive - angularjs

EDIT: see jsfiddle
I have a list of items, that I show as text via a directive (all is simplified here, it's more comlicated than just text, but it's the same in principle). Like so:
<body ng:controller="BaseController">
...
<div ng:controller="Controller">
<itemdir ng:repeat="item in items" item="item"></item>
</div>
...
<input ng:model="currentItem" />
</body>
When I click it, it should show the content of the clicked item in an input.
items array as well as currentItem belong to BaseController scope.
The directive produces a template (see below) with ng:click which should change BaseController scope's property (called currentItem). However it does not do anything to it (input value is not changed to the new current item). In Batarang for Chrome I can see that the currentItem property is visible and changed in the scope of the directive but not of the BaseController.
module.directive 'itemdir', () ->
restrict: 'E'
replace: true
template: '<div ng:click="show(item)"></div>'
controller: 'EditorController'
scope:
item: '=item'
link: ($scope, $element, $attrs) ->
update = ->
$element.html($scope.item)
$scope.$watch('item', update)
For changing the property I tried a method show(item) which is defined in the BaseController's scope which only assigns the item parameter to $scope.currentItem.
It doesn't work even when I change the ng:click value from show(item) to currentItem = item
I know this is some scope issue, but it seems I still don't grasp all the details of it.

So, looking at the provided jsFiddle we can see that the BaseController is being used both in a directive and in top div. This introduced a subtle issue since it was possible to invoke the show(item) method from top-buttons and HTML produced by directives, but those methods were invoked on different controllers and writing to different scopes.
Now, it is hard to deduce from your question if the use of BaseController in a directive was intentional or not (in the question the directive has the EditorController) but assuming that this was by accident and you want to keep BaseController for a div and still invoke methods on it from a directive you need to take special care when creating isolated scopes (as the name implies those are really isolated so not inheriting from a parent scope). Basically you need to make sure that the show method is available in an isolated scope and points to the right method in the parent scope.
Taking your example you would define your directive like this (please note show : '&ngClick'):
module.directive('itemdir', function () {
return {
restrict:'E',
replace:true,
template:'<div ng:click="show(item)" class="clickable"></div>',
scope : {item : '=', show : '&ngClick'},
link:function ($scope, $element, $attrs) {
$element.html($scope.item)
}
}
});
Here is the working jsFiddle: http://jsfiddle.net/pkozlowski_opensource/M9B93/
In the future you might find AngularJS Batarang extension for Chrome (http://blog.angularjs.org/2012/07/introducing-angularjs-batarang.html) useful as it allows to visualize scopes and their content.

Related

Directive with it's own controller placed within ngAnimateSwap results in new controller being initialized on every 'swap'

I have created a directive (ParentDir) that has it's own controller and whose template binds to this controller. It communicates with another directive (Child1) that has it's own controller which 'requires' the first parent directive. Below is a simplified example:
Module.directive("ParentDir", function () {
return {
templateUrl: '../ParentTemplate',
restrict: 'AEC',
scope: {
},
controllerAs: 'parCtrl',
bindToController: true,
controller: ['$scope', function ($scope) {
parCtrl= this;
parCtrl.title = "PARENT 1 TITLE";
}]}
Module.directive("Child1", function () {
return {
templateUrl: '../Child1Template',
restrict: 'AEC',
require: '^^ParentDir',
scope: {},
controllerAs: 'ch1Ctrl',
bindToController: true,
link: function ($scope, element, attrs, parCtrl) {
$scope.parCtrl= parCtrl;
},
controller: ['$scope', function ($scope) {
ch1Ctrl= this;
ch1Ctrl.title = "CHILD 1 TITLE";
}]}
ParentDir html:
<child1> </child1>
Child1 html:
{{parCtrl.title}}
{{ch1Ctrl.title}}
Finally my ParentDirective is initialized in something like this:
<div ng-animate-swap="trigger" class="swapclass">
<parent-dir></parent-dir>
</div>
I need the entire parent directive's template to slide in certain cases. I also use the directive in other places where I don't need this and I can use it as is. In the cases where I do need the slide animation, I place it inside an ng-animate-swap as shown above. The problem is that every time the swap trigger changes, a new parCtrl is initialized causing everything to be reset!
How can I use animate swap with a directive that has isolate scope and it's own controller, without reinitializing the controller everytime a swap occurs?
Directives, as we know, are high-level markers that tell Angular's compiler to attach a specified behavior to that HTML element. When a directive is put on the DOM, Angular's $compile service matches directive names with their code, normalizes it and executes it.
However, ng-animate-swap removes its element from the DOM before appending the new one.
This means your directives are being recompiled for each swap, and new isolate scopes are created each time the animation happens.
The solution to this depends on the functionality of your app, how large your templates are, and how often you need to do the animation (or what the animation entails):
One solution is to create another directive outside of the swap animation that holds parCtrl.title and ch1Ctrl.title (or whatever other variables you have) and then is able to pass that information down to child scopes through prototypical inheritance:
<swap-dir>
<div ng-animate-swap="trigger" class="swapclass">
<parent-dir></parent-dir>
</div>
<swap-dir>
This could also be done with a controller, perhaps much more easily. What you choose to do would depend on where you're getting your scope variables from and how many different elements you have on the page.
However, ng-animate-swap creates its own scope, so while I believe this would work, the ever-fun JavaScript inheritance shenanigans could cause an issue here as well.
Another solution would be to skip ng-animate-swap altogether and just animate the template element with regular old CSS transitions, although this depends on what you're doing and how you want it to look.

How to refactor directive and change functionality by checking parent scopes?

I have a form with a ton of duplicate functionality in 2 different Controllers, there are slight differences and some major ones in both.
The form sits at the top of a products view controller, but also inside of the products modal controller.
Test plunker: http://plnkr.co/edit/EIW6xoBzQpD26Wwqwwap?p=preview
^ how would you change the string in the console.log and the color of the button based on parent scope?
At first I was going to create a new Controller just for the form, but also the HTML was being duplicated, so decided to put that into a Directive, and just add the Controller code there.
My question now is this: How would I determine which parent scope the form-directive is currently being viewed in? Because depending on the parent scope the functions/methods behave differently.
So far I've come up with this:
.directive('productForm', function() {
return {
templateUrl: "views/products/productForm.html",
restrict: "E",
controller: function($scope) {
console.log('controller for productForm');
console.log($scope);
console.log($scope.$parent);
/*
If parent scope is the page, then this...
If parent scope is the modal then this instead...
*/
}
}
});
However it's giving me back $parent id's that look like 002 or 00p. Not very easy to put in if / else statements based on that information.
Have you guys run into this issue before?
You can define 'saveThis' in your controller and pass it to directive using '&'
scope: {
user: '=',
saveThis : '&'
},
please see demo here http://plnkr.co/edit/sOY8XZtEXLORLmelWssS?p=preview
That gives you more flexibility, in future if you want to use saveThis in another controller you can define it inside controller instead adding additional if statement to directive.
You could add two way binding variables in the directive scope, this allows you to specify which Ctrl variable gets bound to which directive variable
<my-directive shared="scopeVariable">
this way you achieve two way binding of the scopeVariable with the shared directive variable
you can learn more here
I advice against this practice and suggest you to isolate common logics and behaviours in services or factories rather than in directives
This is an example of a directive that has isolated scope and shares the 'title' variable with the outer scope.
You could declare this directive this way:
now inside the directive you can discriminate the location where the directive is defined; just replace the title variable with a location variable and chose better names.
.directive('myPane', function() {
return {
restrict: 'E',
scope: {
title: '#'
},
link: function(scope, element, attrs, tabsCtrl) {
},
templateUrl: 'my-pane.html'
};
});

Common directive ng-click guidance needed

I have a directive which consists of a form text element and a continue button along with the associated controller etc. This directive is going to be used in about 5 different pages, but on each page it is used the continue button will do something different.
My question is where can/should I put the code for the continue button if it does different things for each page?
Since its a directive I cant simply pass a different function into ng-click depending on what page im on (ie, if i simply replicated the code on each page it is used I could simply change the function called on ng-click and have that function in each of the page controllers.
Hopefully Im not being too vague with my question and you can make sense of what im asking. If not just say so and ill try to explain in more detail.
I would really appreciate some guidance on this matter.
Thanks.
There are two ways that you can do it. If you are creating your directive as a true component you can use isolated scope with & binding that binds to an expression.
Assume your directive looks like
<div do-work on-click="save()"></div>
and the generated html
<div>
<input ...>
<button ng-click="doAction()"><button>
</div>
The directive scope will be defined
scope:{
onClick:'&'
}
In your directive controller or link function you need to implement the button doAction, which in turns evaluates the onClick action
scope.doAction=function() {
scope.onClick({//if params are required});
}
Now you have linked the parent through the direct onClick reference. One thing to remember here is that this creates a directive with isolated scope.
In case you do not want isolated scope created you need to use
scope.$eval(attr.onClick); // this evaluates the expression on the current scope.
Hope this helps.
Ideally you should not create directives which are not re-usable.
In your case, you may do it like following -
create an isolated scope in the directive
add a function to be called and pass the page/ page id as parameter
call functions in controller based on parameter
Directive
myApp.directive('someDirecive', function () {
return {
// restrict options are EACM. we want to use it like an attribute
restrict: 'A',
// template : <inline template string>
// templateUrl = path to directive template.
// templateUrl: '',
scope: {
onButtonClick : '&'
},
controller: function ($scope, $element, $attrs, $transclude) {
$scope.onButtonClick = function(pageId) {
if (pageId == 1) {
// do something
}
else if (pageId == 2) {
// do something
}
}
},
//link: function (scope, iElement, iAttrs) {
//}
};
});
HTML
<div some-directive on-button-click="DoSomething(1)" />

How do I assign an attribute to ng-controller in a directive's template in AngularJS?

I have a custom attribute directive (i.e., restrict: "A") and I want to pass two expressions (using {{...}}) into the directive as attributes. I want to pass these attributes into the directive's template, which I use to render two nested div tags -- the outer one containing ng-controller and the inner containing ng-include. The ng-controller will define the controller exclusively used for the template, and the ng-include will render the template's HTML.
An example showing the relevant snippets is below.
HTML:
<div ng-controller="appController">
<custom-directive ctrl="templateController" tmpl="template.html"></custom-directive>
</div>
JS:
function appController($scope) {
// Main application controller
}
function templateController($scope) {
// Controller (separate from main controller) for exclusive use with template
}
app.directive('customDirective', function() {
return {
restrict: 'A',
scope: {
ctrl: '#',
tmpl: '#'
},
// This will work, but not what I want
// Assigning controller explicitly
template: '<div ng-controller="templateController">\
<div ng-include="tmpl"></div>\
</div>'
// This is what I want, but won't work
// Assigning controller via isolate scope variable from attribute
/*template: '<div ng-controller="ctrl">\
<div ng-include="tmpl"></div>\
</div>'*/
};
});
It appears that explicitly assigning the controller works. However, I want to assign the controller via an isolate scope variable that I obtain from an attribute located inside my custom directive in the HTML.
I've fleshed out the above example a little more in the Plunker below, which names the relevant directive contentDisplay (instead of customDirective from above). Let me know in the comments if this example needs more commented clarification:
Plunker
Using an explicit controller assignment (uncommented template code), I achieve the desired functionality. However, when trying to assign the controller via an isolate scope variable (commented template code), it no longer works, throwing an error saying 'ctrl' is not a function, got string.
The reason why I want to vary the controller (instead of just throwing all the controllers into one "master controller" as I've done in the Plunker) is because I want to make my code more organized to maintain readability.
The following ideas may be relevant:
Placing the ng-controller tags inside the template instead of wrapping it around ng-include.
Using one-way binding ('&') to execute functions instead of text binding ('#').
Using a link function instead of / in addition to an isolate scope.
Using an element/class directive instead of attribute directive.
The priority level of ng-controller is lower than that of ng-include.
The order in which the directives are compiled / instantiated may not be correct.
While I'm looking for direct solutions to this issue, I'm also willing to accept workarounds that accomplish the same functionality and are relatively simple.
I don't think you can dynamically write a template key using scope, but you certainly do so within the link function. You can imitate that quite succinctly with a series of built-in Angular functions: $http, $controller, $compile, $templateCache.
Plunker
Relevant code:
link: function( scope, element, attrs )
{
$http.get( scope.tmpl, { cache: $templateCache } )
.then( function( response ) {
templateScope = scope.$new();
templateCtrl = $controller( scope.ctrl, { $scope: templateScope } );
element.html( response.data );
element.children().data('$ngControllerController', templateCtrl);
$compile( element.contents() )( templateScope );
});
}
Inspired strongly by this similar answer.

How to create this custom control with AngularJS directive?

I'm a bit new to AngularJS and am trying to write a custom select control based on Zurb Foundation's custom select(see here: http://foundation.zurb.com/docs/components/custom-forms.html)
I know I need to use a directive for this but am not sure how to accomplish this.
It's going to have to be reusable and allow for the iterating of whatever array is passed in to it. A callback when the user selects the item from the dropdown list is probably needed.
Here is the markup for the custom Foundation dropdown list:
<select name="selectedUIC" style="display:none;"></select>
<div class="custom dropdown medium" style="background-color:red;">
Please select item
<ul ng-repeat="uic in uics">
<li class="custom-select" ng-click="selectUIC(uic.Name)">{{uic.Name}}</li>
</ul>
</div>
This works for now. I am able to populate the control from this page's Ctrl. However, as you can see, I'd have to do this every time I wanted to use a custom dropdown control.
Any ideas as to how I can turn this baby into a reusable directive?
Thanks for any help!
Chris
If you want to make your directives reusable not just on the same page, but across multiple AngularJS apps, then it's pretty handy to set them up in their own module and import that module as a dependency in your app.
I took Cuong Vo's plnkr above (so initial credit goes to him) and separated it out with this approach. Now this means that if you want to create a new directive, simply add it to reusableDirectives.js and all apps that already have ['reusableDirectives'] as a dependency, will be able to use that new directive without needing to add any extra js to that particular app.
I also moved the markup for the directive into it's own html template, as it's much easy to read, edit and maintain than having it directly inside the directive as a string.
Plnkr Demo
html
<zurb-select data-label="{{'Select an option'}}" data-options="names"
data-change-callback="callback(value)"></zurb-select>
app.js
// Add reusableDirectives as a dependency in your app
angular.module('angularjs-starter', ['reusableDirectives'])
.controller('MainCtrl', ['$scope', function($scope) {
$scope.names = [{name: 'Gavin'}, {name: 'Joseph'}, {name: 'Ken'}];
$scope.callback = function(name) {
alert(name);
};
}]);
reusableDirectives.js
angular.module('reusableDirectives', [])
.directive('zurbSelect', [function(){
return {
scope: {
label: '#', // optional
changeCallback: '&',
options: '='
},
restrict: 'E',
replace: true, // optional
templateUrl: 'zurb-select.html',
link: function(scope, element, attr) { }
};
}]);
zurb-select.html
<div class="row">
<div class="large-12 columns">
<label>{{label || 'Please select'}}</label>
<select data-ng-model="zurbOptions.name" data-ng-change="changeCallback({value: zurbOptions.name})"
data-ng-options="o.name as o.name for o in options">
</select>
</div>
</div>
Is something like this what you're looking for?
http://plnkr.co/edit/wUHmLP
In the above example you can pass in two attribute parameters to your custom zurbSelect directive. Options is a list of select option objects with a name attribute and clickCallback is the function available on the controller's scope that you want the directive to invoke when a user clicks on a section.
Notice there's no code in the link function (this is where the logic for your directive would generally go). All we're doing is wrapping a template so that it's reusable and accepts some parameters.
We created an isolated scope so the directive doesn't need to depend on parent scopes. We binded the isolated scope to the attribute parameters passed in. The '&' means bind to the expression on the parent scope calling this (in our case the callback function available in our controller) and the '=' means create a two way binding between the options attribute so when it changes in the outter scope, the change is reflected here and vice versa.
We're also restricting the usage of this directive to only elements (). You can set this to class, attributes, etc..
For more details the AngularJs directives guide is really good:
http://docs.angularjs.org/guide/directive
Hope this helps.

Resources