Decoupling UI and Controllers in a nested custom directive - angularjs

What I think I want to do is completely isolate each step of a wizard into a custom element directive.
I think this would allow me to completely encapsulate the detail of each page of the wizard. For example:
<custom-wizard-tag>
<enter-name-page page="1" name-placeholder="name"/>
<enter-address-page page="2" name-placeholder="name" address-placeholder="address" last-page/>
</custom-wizard-tag>
So far, so good. Each of the elements above has its own directive, and each of these specifies a templateUrl and a controller (templateUrl could be supplied as an attribute, of course).
I want each page of the wizard to 'inherit' some behaviour. The UI components would contain the buttons, which would need to query the outer scope, for example to determine whether it is possible to move forward, backward and so on. We would also need to call member functions on the parent scope in order to actually move the wizard forwards and backwards, and to check whether the current page number matches 'ours'.
I'm new to this so bear with me...
I read the documentation on directive, and thought I could use scope: { onNext: '&onNext' } in order to 'inherit' the onNext function from the previous scope (which is assumed to be one which is 'wizard-like'). However, this is not what angular seems to do. It seems to want map the inner scope's onNext via an attribute called on-next, thus breaking encapsulation, because now the UI elements must reference functions in the parent scope - which is exactly what I wanted to avoid.
Am I barking up the wrong tree, or is there an idiomatic way to do this. A day of web searching has not got me far, but I could be using the wrong search terms.
Thanks for your patience.

scope: { onNext: '&onNext' }
won't do any inherintance, you would have to define onNext in the template (the template scope) the same way you do with the page property: <enter-name-page page="1"
If you have a function onNext defined in you customWizardTag directive either in link function or in its controller, you'll have to put it in the controller, because the controller can be passed to the child directive. Then you'll be able to pass the parent directive's controller in the link functions of somethingPage directives.
.directive('parentDirective, function() {
return {
controller: someControllerThatHasOnNext,
}
})
.directive('childDirective', function() {
return {
require: '^^parentDirective',
link: function(scope, element, attrs, theParentDirectivesController){
theParentDirectivesController.onNext();
}
}
})
If this is what you wanted

Related

AngularJS: Should I convert directive's linking function to a controller?

I heard it's a good practice to use the controllerAs syntax along with bindToController: true in directives that use an isolate scope. References: one, two
Suppose, I have a directive like this:
angular.module('MyModule').directive('MyDirective', function(User) {
return {
scope: {
name: '='
},
templateUrl: 'my-template.html',
link: function(scope) {
scope.User = User;
scope.doSomething = function() {
// Do something cool
};
}
};
});
<!-- my-template.html -->
<div>
User Id: {{ User.id }}
Name: {{ name }}
<button ng-click="doSomething()">Do it</button>
</div>
As you can see, there is no controller in this directive. But, to be able to leverage controllerAs and bindToController: true I have to have a controller.
Is the best practice to convert the linking function to a controller?
angular.module('MyModule').directive('MyDirective', function(User) {
return {
scope: {
name: '='
},
templateUrl: 'my-template.html',
bindToController: true,
controllerAs: 'myCtrl',
controller: function() {
this.User = User;
this.doSomething = function() {
// Do something cool
};
}
};
});
<!-- my-template.html -->
<div>
User Id: {{ myCtrl.User.id }}
Name: {{ myCtrl.name }}
<button ng-click="myCtrl.doSomething()">Do it</button>
</div>
My understanding is that directive's controller should be used as a mechanism to expose directive's API for a directive-to-directive communication.
Could anyone shed light on what's the best practice these days, having Angular 2.0 in mind?
I consider it best practice to move initialization code and/or exposing API functions inside of a directive's controller, because it serves two purposes:
1. Intialization of $scope
2. Exposing an API for communication between directives
Initialization of Scope
Suppose your directive defines a child scope (or inherits scope). If you initialize scope inside of your link function, then child scopes will not be able to access any scope variables defined here through scope inheritance. This is because the parent link function is always executed after the child link function. For this reason, the proper place for scope initialization is inside of the controller function.
Exposing a Controller API
Child directives can access the parent directive's controller through the 'require' property on the directive definition object. This allows directives to communicate. In order for this to work, the parent controller must be fully defined, so that it can be accessed from the child directive's link function. The best place to implement this is in the definition of the controller function itself. Parent controller functions are always called before child controller functions.
Final Thoughts
It is important to understand that the link function and the controller function serves two very different purposes. The controller function was designed for initialization and directive communication, and the linker function was designed for run-time behavior. Based on the intent of your code, you should be able to decide whether it belongs in the controller, or it belongs in the linker.
Should you move any code that initializes scope from the link function to the controller function?
Yes, that is one of the primary reasons that the controller function exists: to initialize scope, and allow its scope to participate in prototypical scope inheritance.
Should you move $watch handlers from the link function to the controller function?
No. The purpose of the link function is to hookup behavior and potentially manipulate the DOM. In the link function, all directives have been compiled, and all child link functions have already executed. This makes it an ideal place to hookup behavior because it is as close DOM ready as it can be (it is not truly DOM ready until after the Render phase).
I will start with your last sentence. It's all about how you want to write your angular code. If you want to stick with the guideline for writing good code for angular 1.x then don't even bother thinking too much about what is ideal. However, if you want to prepare for the next version of Angular, as well as, the upcoming web technologies, I would suggest that you start adopting the new concepts and adjust them to the way you write your code today. Bare in mind there is no right or wrong in this case.
Speaking about angular 2.0 and ES6, I would like to stress out that the notion of directives will be more in align with the Web Components technology.
In Angular 2.0 (according to the current design) will get rid of the complex way of defining directives; That is no more DDO. Thus I think it would be better if you start thinking in that way. A component will just have a View and a controller.
For example,
#ComponentDirective({
selector:'carousel',
directives:[NgRepeat]
})
export class Carousel{
constructor(panes:Query<CarouselItem>) {
this.items= panes;
}
select(selectedCarouselItem:CarouselItem) { ... }
}
The above code is written in AtScript (a superset of typescript and ES6), but you will be able to express the same thing in ES5, as well. You can see how simpler things will be. There in np such notion like link function or compile etc.
In addition, the view of the above component will be directly bound to the above class; So you can already find a similarity to the controllerAs syntax.
So in essence, I would suggest that you first look at the general idea behind Web Components, and how the future of the Web Developments might be, and then I think you would start writing Angular 1.x code with that in mind.
In summary, try to code in a way that favours the current version of Angular, but if you believe that there are some parts of your code that can embrace some concepts of the next version, then do it. I don't believe it will harm you. Try to keep it simple as the new version of Angular will be simpler.
I would suggest that you read the following posts:
https://www.airpair.com/angularjs/posts/component-based-angularjs-directives
http://eisenbergeffect.bluespire.com/all-about-angular-2-0/
https://www.airpair.com/angularjs/posts/preparing-for-the-future-of-angularjs
http://teropa.info/blog/2014/10/24/how-ive-improved-my-angular-apps-by-banning-ng-controller.html
UPDATE
(at the bottom I added a code/plnkr that shows the approach)
Apart from the article you mentioned: https://www.airpair.com/angularjs/posts/preparing-for-the-future-of-angularjs#3-3-match-controllers-with-directives, which basically not only advocates the pattern you are asking for, but component based front-end in general, I have found: http://joelhooks.com/blog/2014/02/11/lets-make-full-ass-angularjs-directives/ (it advocates Minimal use of the link function and use ui-bootstrap as an example where such a pattern has been used). I cannot agree more with both these articles.
Another thing about Angular2.0: no more $scope in angular2.0 -- https://www.youtube.com/watch?v=gNmWybAyBHI&t=12m14s, so surely if you can get rid of $scope as much as possible, then the transition should be smoother.
I made a small mistake as well:
Still, I prefer to define all functions in controller and just call
them via link's scope. Ideally it is just one call:
scope.init ctrl.init(/*args*/) (where ctrl is
directive's controller).
To some degree it is a matter of taste, but there are some valid reasons to keep the link function as thin as possible:
The logic in link function is not easily testable. Sure, you can compile the directive in your unit tests and test its behaviour, but the link function itself is a black box.
If you have to use controller (let say to inter directive communication), then you end up with two places where to put your code. It is confusing, but if you decide to have the link function thin, then everything that can be put in controller should be put in controller.
You cannot inject additional dependencies directly to the link function (you can still use those injected to the main directive function). There is no such a problem in case of controller's approach. Why it matters:
it keeps better structure of the code, by having the dependencies closer to the context where they are needed
people coming to angular with non-JS backgrounds have still problems how functional closure works in JS
So what has to be put in the link function:
Everything that needs to be run after the element has been inserted into DOM. If $element exposed $on('linked') event than basically this point is not valid.
Grabbing references to controllers require:ed. Again, if it was possible to inject them into the controller directly...
Still, I prefer to define all functions in controller and just call them via link's scope. Ideally it is just one call: scope.init.
Misko Hevery told a couple of times that DDO is far from being perfect and easy to understand and it evolved to what it is right now. I am pretty sure, that if the design decisions were made upfront then there would a single place to put the logic of the directive - as it will be in angular2.0.
Now answering your question if you should convert link function to a controller. It really depends on a number of criteria, but if the code is actively developed then probably it is worth to consider. My experience (and couple of people I talked about it) can be illustrated by this image:
About angular2.0 -- it is going to be a tectonic shift, so from that perspective it should not matter much, but still the controller's approach seems to be closer to the way directives/components are going to be declared in v2.0 via ES6 classes.
And as the last thing: To some degree it is a matter of taste, but there are some valid reasons to keep the CONTROLLER function thin as well (by delegating logic to services).
UPDATE -- PLNKR
PLNKR exemplifying the approach:
html
<input ng-model="data.name"/>
<top-directive>
<my-directive my-config="data">
</my-directive>
</top-directive>
js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.data = { name : 'Hello, World'};
});
app.controller('MyCtrl', function($scope){
var self = this;
this.init = function(top){
this.topCtrl = top;
this.getTopName = top.getName.bind(top);
this.getConfigName = function(){return this.config.name};
console.log('initilizing', this, $scope, this.getConfigName, this.getTopName());
}
// if you want to $watch you have to inject $scope
// you have access to the controller via name defined
// in contollerAs
$scope.$watch('myCtrl.config', function(){
console.log('config changed', self.getConfigName());
}, true);
});
app.directive('topDirective', function(){
return {
controller : function(){
this.name = "Hello, Top World";
this.getName = function(){return this.name};
}
}
});
app.directive('myDirective', function(){
return {
require: ['myDirective', '^topDirective'],
controller : 'MyCtrl',
bindToController: true,
controllerAs: 'myCtrl',
template : '{{myCtrl.getConfigName() + " --- " + myCtrl.getTopName()}} ',
scope : {
config : "=myConfig",
},
link : function(scope, element, attrs, Ctrls){
Ctrls[0].init(Ctrls[1]);
}
}
});
As per the latest documentation this is still the recommended practice "use controller when you want to expose an API to other directives. Otherwise use link." I would like to hear from other people also and the approach they are using.
sharing the contents from here, (I dont have enough reputations to put it as comments)
“where do I put code, in ‘controller’ or ‘link’?”
Before compilation? – Controller
After compilation? – Link
Couple of things to note:
controller ‘$scope’ and link ‘scope’ are the same thing. The difference is paramaters sent to the controller get there through Dependency Injection (so calling it ‘$scope’ is required), where parameters sent to link are standard order based funcitons. All of the angular examples will use ‘scope’ when in the context, but I usually call it $scope for sanity reasons: http://plnkr.co/edit/lqcoJj?p=preview
the $scope/scope in this example is simply the one passed in from the parent controller.
‘link’ in directives are actually the ‘post-link’ function (see rendering pipeline below). Since pre-link is rarely used, the ‘link’ option is just a shortcut to setting up a ‘post-link’ function.
So, whats a real world example? Well, when I’m deciding, I go by this:
“Am I just doing template and scope things?” – goes into controller
“Am I adding some coolbeans jquery library?” – goes in link
Credit for the answer goes to jasonmore

AngularJS : Directive shell that allows user defined content with directive scope

Here's a plunker example you can see: http://plnkr.co/edit/NQT8oUv9iunz2hD2pf8H
I have a directive that I would like to turn into a web component. I've thought of several ways as to how I can achieve that with AngularJS but am having difficulty with a piece of it. I'm hoping someone can explain my misstep rather than tell me a different way to do it.
Imagine you have a directive component that sets up some shell with css classes maybe some sub components, etc.. but lets the user define the main content of the component. Something like the following:
<my-list items="ctrl.stuff">
<div>List Item: {{ item.name }}</div>
</my-list>
The HTML for the list directive could be something like the following (with OOCSS):
<ul class="mas pam bas border--color-2">
<li ng-repeat="items in item track by item.id" ng-transclude></li>
</ul>
Normally this can be solved in the link function by linking the directives scope to the new content. And it does work for other components. However introducing the ng-repeat seems to break that portion of the control. From what I can tell, the appropriate place might be the compile function but the documentation says the transcludeFn parameter will be deprecated so I'm not sure how to proceed.
I should also note that when using the beta AngularJS, there is either a bug or a new paradigm coming, because this is no longer a problem. It seems like the transcluded content always gets access to the directives scope as well as the outer controllers scope.
I really appreciate any enlightenment on this.
It's by design that content added via ng-transclude will bind with an outer controller scope, not a scope of the current element that ng-transclude is on.
You could solve the problem by copy the ng-transclude's code and modify it a bit to give a correct scope:
.directive('myTransclude', function () {
return {
restrict: 'EAC',
link: function(scope, element, attrs, controllers, transcludeFn) {
transcludeFn(scope, function(nodes) {
element.empty();
element.append(nodes);
});
}
};
});
And replace the ng-transclude with my-transclude in your directive template.
Example Plunker: http://plnkr.co/edit/i7ohGeRiO3m5kfxOehC1?p=preview

Exposing directive controller to parent controller

I want to expose some of my directive's functionality through its controller (think a public API for this directive).
return {
restrict: 'E',
scope: {},
controller: function($scope) {
this.method1 = ...;
this.method2 = ...;
},
controllerAs: 'dir',
link: function (scope, element, attrs) { ... }
}
Then in my parent controller or template call dir.method1 to get stuff accomplished inside the directive. Any ideas if this is possible as of Angular 1.3?
I'd like to refrain from event passing or even function passing, I have heard this is possible although I have never seen an implementation of this.
It is possible, but your issue isn't to figure out how to get the API out. It's how to get TO it from the parent. You're creating an isolate scope through your use of the 'scope' option. You're also making an element-type directive, so I'm guessing you're doing something like this:
<my-parent>
<my-child></my-child>
</my-parent>
where <my-parent> is the parent directive, and <my-child> is the directive with the API you want to expose.
The real question is what you're trying to achieve here. There is totally a way to do what you're asking. Just because the scope is isolated doesn't mean you can't get to it. You can just iterate through the parent $scope's $$childHead/etc list to find the child whose API you want to access. Anything you define in the child like this:
$scope.myApiFunction = function() {
};
will be visible here. (Things you put into 'this' will not - use the $scope storage bucket instead.)
That means if you only had ONE child you could do something like this from the parent controller:
$scope.$$childHead.myApiFunction();
Simple. Also, very crude. There are lots of problems here: what if you have many children? What if this child with its API ends up one level down? Etc. It's breaking all kinds of OO patterns and it's going to get messy, fast.
Your question is very abstract - it might be good if you updated it with an exact example. Without that, let me guess at your goal. There are two ways to do something "like this" that are encouraged within Angular:
Services. Whenever you say "API", think Service first. A service is a singleton (automatically) so it's tailor-made for creating APIs. And services can use the Factory pattern to return objects of a type, so THOSE are tailor made for doing things like having a manager service handle, say, a buddy list in an IM client, with API methods for creating, removing, and finding buddies.
Items that add "optional" functionality to their parents when they're defined. Let's say we have three possible types of tooltips: tooltips that have a hover effect, those that have a click effect, and those that are triggered by a "walkthrough" system in some order. For this kind of thing, the easy thing to do is just reverse the API, like this:
Parent Controller:
$scope.tooltipHandler = {
showTooltip: function() {},
hideTooltip: function() {}
};
Child Controller:
$scope.$parent.tooltipHandler = {
showTooltip: function() {
// Do some real work
},
hideTooltip: function() {
// Do some real work
},
}
What happens here is if there's no tooltip defined, when the parent runs its walkthrough, nothing happens. If you add the blue tooltip display module, when the parent runs its walkthrough now, it's going to show blue tooltips.
Make sense?
I arrived here looking for a similar response. So far the best that I can figure is to do what Angular does with ngForm.
In the documentation clearly states
If the name attribute is specified, the form controller is published onto the current scope under this name.
This basically makes the form controller accessible from anywhere.
If you have the following DOM
<div ng-controller="MyCtrl as parentCtrl">
<form name="parentCtrl.frmCtrl">
<my-child-directive>
</form>
</div>
You can use require: 'ngForm' in my-child-directive to get access from an inside directive. If you are in the parent controller you can access it trough the frmCtrl variable.
Not sure if this is best practice. In ngForm the name attribute works well, but I don't even know how to call such an attribute for a custom directive.
Thats why I arrived here, I wanted to know if this is "The Angular way" and what types of convetions are on the subject.
Hope it helps!

Angular directive link function called twice

In my angular app, directives are working fine during the first visit, but once a page been visited twice, all the directive link function gets called twice too. Say I am on page A, click a link to go to page B and then back to page A, all directives on page A will execute the their link function twice. if I refresh the browser it will become normal again.
Here is an example where the console.log will output twice when the second visit.
#app.directive 'testChart', ["SalesOrder", (SalesOrder) ->
return {
scope: {options: '='}
link: (scope, elem, attrs) ->
console.log("............checking")
SalesOrder.chart_data (data) ->
Morris.Line
element: "dash-sales"
data: data
xkey: 'purchased_at'
ykeys: ['total']
labels: ['Series a']
}
]
Any idea?
Update
My Route
when("/dash", {
templateUrl: "<%= asset_path('app/views/pages/dash.html') %>",
controller: DashCtrl
}).
so my chart is duplicated
also make sure you are not including your directive in your index.html TWICE!
I had this exact same problem.
After a loooooong time digging around I found that I hadn't used the correct closing tag which resulted in the chart being called twice.
I had
<line-chart><line-chart>
instead of
<line-chart></line-chart>
The link() function is called every time the element is to be bound to data in the $scope object.
Please check if you are fetching data multiple times , via GET call. You can monitor the resource fetching via Network tab , of chrome debugger.
A directive configures an HTML element and then updates that HTML subsequently whenever the $scope object changes.
A better name for the link() function would have been something like bind() or render(), which signals that this function is called whenever the directive needs to bind data to it, or to re-render it.
Maybe this will help somebody...
I had a problem with directive transclude, I used a transclude function which was adding child elements and also at the same time I forgot ng-transclude in directive template. Child elements were also directives and their link function was called twice!
Spent some time on this one..
More in details:
I had a "main" directive and "child" directives, idea was to use one inside another, something like that:
main
child
child
So problem was that link of "child" directive was called twice, and I didn't understand why,
Turned out I had ng-transclude in "main" directive template (I am posting it as it is in PUG format, sorry for that):
md-card(layout-fill)
md-card-content(flex)
.map-base(id="{{::config.id}}", layout-fill)
ng-transclude
and also in link function of "main" directive I called transclude function:
link: function($scope, $element, $attrs, controller, transcludeFn) {
$element.append(transcludeFn());
}
I think I just tried different combinations and forgot about that, visually everything was ok, but link was called twice and code was running twice and logic was broken..
So problem is that you can't have both and you have to choose one of the ways.
Hopefully now it is more clearer.
In my case I had a main-nav and sub-nav that both called a directive by its name attribute. Since the first instance already set the scope needed the second sub-nav the 2nd call wasn't needed. Incase anyone has a similar issue.

AngularJS - Hook into Angular UI Bootstrap - Carousel Custom Next()?

I'm trying to implement a Angular UI Bootstrap carousel, but I'm using it for a Quiz. Therefore, I don't need normal Prev() and Next() buttons.
Rather, I need a custom Next() button that makes sure they've selected an answer before continuing on to next "slide" of question/answers.
How do I hook into the carousel directive functions to run my code and then use the carousel.next() function?
Thanks,
Scott
There is no official possibility to achieve this. but this can be hacked, if you want. But i think it is better grab the bootstrap original one, have a look the at angular bootstrap ui sources (carousel) and write your own wrapper.
Here comes the hack:
The first problem we have to solve is, how to access the CarouselController. There is no API that exposes this and the carousel directive creates an isolated scope. To get access to this scope wie need the element that represents the carousel after the directive has been instantiated by angular. To achieve this we may use a directive like this one, that must be put at the same element as our ng-controller:
app.directive('carouselControllerProvider', function($timeout){
return {
link:function(scope, elem, attr){
$timeout(function(){
var carousel = elem.find('div')[1];
var carouselCtrl = angular.element(carousel).isolateScope();
var origNext = carouselCtrl.next;
carouselCtrl.next = function(){
if(elem.scope().interceptNext()){
origNext();
}
};
});
}
};
});
We must wrap our code in a $timeout call to wait until angular has created the isolated scope (this is our first hack - if we don't want this, we had to place our directive under the carousel. but this is not possible, because the content will be replaced). The next step is to find the element for the carousel after the replacement. By using the function isolateScope we have access to the isolated Scope - e.g. to the CarouselController.
The next hack is, we must replace the original next function of the CarouselController with our implementation. But to call the original function later we have to keep this function for later use. Now we can replace the next function. In this case we call the function interceptNext of our own controller. We may access this function through the scope of the element that represents our controller. If the interceptNext returns true we call the original next function of the carousel. For sure you can expose the complete original next function to our controller - but for demonstration purposes this is sufficient. And we define our interceptNext function like this:
$scope.intercept = false;
$scope.interceptNext = function(){
console.log('intercept next');
return !$scope.intercept;
}
We can now control the next function of the carousel by a checkbox, that is bound to $scope.intercept. A PLUNKR demonstrates this.
I knew this is not exactly what you want, but how you can do this is demonstrated.
That hack is neat michael, I started working on something similar for my needs. But then realized I might as well finally dip my toe into contributing to the open source community.
I just submitted a pull request to update the library so the index of the current slide is exposed to the Carousel scope.
https://github.com/angular-ui/bootstrap/pull/2089
This change allows you to have per-slide behavior in the carousel template.
This change allowed me to override the base carousel template so that for instance on the first slide the "prev" button would not show or the "next" button would not show for the final slide.
You can add more complex logic for your own personal needs, but exposing the current index in this manner to the $scope is part of making this part of the framework more flexible.
EDIT
I made more changes for my personal use, but don't want quite yet to contribute this change which is closer to what you are needing.
I modified the carousel directive, adding the "finish" property to scope.
.directive('carousel', [function () {
return {
restrict: 'EA',
transclude: true,
replace: true,
controller: 'CarouselController',
require: 'carousel',
templateUrl: 'template/carousel/carousel.html',
scope: {
interval: '=',
noTransition: '=',
noPause: '=',
finish: '='
}
};
}])
Then, when I declare the carousel, I can pass in a method to that directive attribute which is a method in the scope of the controller containing the carousel.
<carousel interval="-1" finish="onFinish">
...
</carousel>
This allows me to modify my template to have a button that looks like this:
<button ng-hide="slides().length-1 != currentIndex" ng-click="finish()" class="btn next-btn">finish<span class="glyphicon glyphicon-stats"></span></button>
So it only shows conditionally on the correct slide and with ng-click it is calling the carousel's $scope.finish() which is a pointer to a method in the controller I created for this application.
Make sense?
edit: This only works if you don't use sort functionality with ng-repeat. There is a bug which breaks the indexing of the slides for this kind of functionality.

Resources