I'm building an Angular library that provides a bunch of components that should make it easier to build SPA application on top of a certain API. For some components we're using the multi-slot transclusions feature. Multi-slot transclusions and components were introduces in the AngularJS 1.5 release.
I really like both features, but I don't understand why components always have an isolated scope. I would like to control how variables are accessible in my transcluded template. But now I can't, because I can't control the scope. This basically means I have to tell the users of my library to reference the parent scope first in order to get to the data they need.
Does anyone know a workaround?, or maybe I'm doing it wrong. Then please tell me :-)
So this is my component:
export const ProductsListComponent =
{
transclude: {
'template' : '?productListItemTemplate'
},
templateUrl: 'app/components/products-list/products-list.html',
controller: ProductsListComponentController,
bindings: {
list: '='
}
}
...
angular.module('MyWebApplication', ['ngMessages', 'ui.router' ])
.component( 'productList', ProductsListComponent )
...
Here's the template HTML:
<div class="product-list-wrapper" ng-repeat="$product in $ctrl.list">
<ng-transclude ng-transclude="template">
<product-list-item product="$product"></product-list-item>
</ng-transclude>
</div>
And this is how it can be used. And you see my problem. The expression must begin with $parent.$product.title, because the component scope is contained.
<product-list list="search.products">
<product-list-item-template>
<h2>{{$parent.$product.title}}</h2>
</product-list-item-template>
</product-list>
The simple answer is, that using parent scope is an issue when it comes to maintainability and reusability.
Angular 2 promotes the component pattern which requires to make all outer dependencies explicit. The angular 1 component function is the way to structure your application for easier migration to angular 2.
This post is a great reference if you are interested in details about the reasons: http://teropa.info/blog/2015/10/18/refactoring-angular-apps-to-components.html#replace-ng-controller-with-component-directive
Related
Started learning AngularJS today, using webpack and ES6. I'm kind of stuck on a simple problem that I cannot figure out. I'm building a really simple SPA app, like a shopping cart app, which should have the "header" component with a couple of links to other pages and the sum and total of shopping cart.
One of these other controllers could be just a random dummy page, one could be the shopping cart summary, and the main controller would be where the user can actually add items to the shopping cart.
What I have thus far is just the following routes:
export default function routes($stateProvider) {
$stateProvider
.state('main', {
url: '/',
template: require('./main.html'),
controller: 'MainController',
controllerAs: 'main'
})
.state('other', {
url:'/other',
template: require('./other.html'),
controller: 'OtherController',
controllerAs: 'other',
})
}
index.js:
export default angular.module('app.main', [uirouter, productService])
.config(routing)
.controller('MainController', MainController)
.controller('OtherController', OtherController)
.name;
productService isn't relevant here, so I exclude that code.
Just as a test I tried to set the OtherController as a separate view in index.html:
<body>
<div ui-view="other"></div>
<div class="container" ui-view></div>
</body>
Kind of like suggested here: How to update partial of html using ES6 syntax with angular-webpack-seed
Also tried to use ng-controller="other" instead of ui-view="other", but that throws error:
The controller with the name 'other' is not registered
And I don't know where I would then register it...
Some places I read that you could use a directive for making a "header"/navbar, but that doesn't seem correct to me at all. Also tried that though, in this way: https://stackoverflow.com/a/33714913/6294072 Read about ng-include, but that wouldn't work, since I need the service for the shopping cart.
I am missing something fundamental here, but can't figure it out. My brain is also (unfortunately) wired on Angular (4), since I have some experience with that :)
Sorry for the noob question, I feel ashamed, but I have been struggling for this for hours now and therefore going crazy. Help is very much appreciated!
Use components instead of controllers with controllerAs. You'll find it much more similar to Angular 4. Then depending on how you want to do it, you can use either bindings or requires to create relationships between your components.
Since you have tight control over this, I would go with requires. Create a component with your navbar and stuff in it, then create child components and require the parent in them. We have a generic in-house framework for this stuff. The outer component we call the site, so we get a reference to it in our child components like this:
require: { site: '^f1Site' }
If you're not already doing so, use ui-router 1.0.x and use route to component to handle navigation & state.
Index.html just has <f1-site>Loading</f1-site> in it's body.
The f1-site template has basic layout stuff in it - including navbars and such that we've moved off into their own components - and then the ui-view directive is on the tag where we want to load all of dynamic components.
I have a ng-repeat-end-watch directive
.....
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
scope.$evalAsync(attr.ngRepeatEndWatch);
}
}
}
on my markup i have
<div ng-repeat="(i, tile) in tiles" class="w3-col tile tile-{{tile.number}}" ng-repeat-end-watch="ctlr.resize()" ng-model="tile">
and on my controller I have
rowsColumns = Math.round(Math.sqrt($scope.tiles.length)), widthHeight = $window.screen.availHeight/rowsColumns;
ctlr.resize = function(){
angular.element('.w3-col').width(widthHeight).height(widthHeight);
};
If in the developer's guide for AngularJS it recommends that you shouldn't use controllers to manipulate DOM elements
What would the recommended way be to apply the above "resize" if its not a good idea to use controllers to manipulate the DOM?
Angular is mostly for functionality, not direct DOM manipulation. You probably noticed that you access elements based on their model not id. You don't need any id in an an Angular app which makes it easier if certain html bits change. As long as the model stays the same you can do what you want with the html.
This being said if you do need to access the DOM you can still do it. You could have the code which does that outside of the controller in its own js file for example. Then you can inject it as an Angular service and reuse it in whichever controller you want.
I would try to keep things nicely separated and the controllers very thin. If you code it this way then it's easier to test things as you don't have to worry about controllers anymore. Hope this makes sense.
The best way to deal with it is writing your own directive. In fact, directives are one of the most powerful concepts in Angular.js.
Think of it as reusable components: not only for jQuery plugins, but also for any of your (sub) controllers that get used in multiple places. It’s kinda like shadow DOM. And the best part is, your controller does not need to know the existence of directives: communications are achieved through scope sharing and $scope events.
I was doing some reading about directives and was wondering what the distinction was between a directive and a component, when I found that there are lots of components in AngularJS.
There is a function component, type component, service component, filter component, provider component, etc... Then to top it off I found that a module component is a component consisting of directives, services, filters, providers, templates, global API’s, and testing mocks. That tended to make things more confusing. I couldn't find a definition of a "component" in the Angular documentation that would explain the distinctions between the types of components listed.
So what exactly is a "component" in AngularJS? Is it something as simple as reusable blocks of code?
By the way, I'm using Angular version 1.4.2 currently.
Angular components were introduced in version 1.5.
A component is a simplified version of a directive. It cannot do dom manipulation (not link or compile methods) and "replace" is gone too.
Components are "restrict: E" and they are configured using an object (not a function).
An example:
app.component('onOffToggle', {
bindings: {
value: '=',
disabled: '='
},
transclude: true,
template: '<form class="form-inline">\
<span ng-transclude></span>\
<switch class="small" ng-model="vm.value" ng-disabled="vm.disabled"/>\
</form>',
controllerAs: 'vm',
controller: ['$scope', function($scope) {
var vm = this;
$scope.$watch("vm.disabled", function (val) {
if (!val) {
vm.value = undefined;
}
})
}]
});
Further reading:
https://toddmotto.com/exploring-the-angular-1-5-component-method/
Coming from an OOP Java oriented background, I was trying to distinguish between the various Angularjs components, including modules. I think the best answer I found about modules was 13 Steps to Angularjs Modularization
In an AngularJS context, modularization is organization by function
instead of type. To compare, given arrays time = [60, 60, 24, 365] and
money = [1, 5, 10, 25, 50], both are of the same type, but their
functions are completely different.
That means your components (controllers, filters, directives) will
live in modules instead of wherever they live now.
So yes, for our 1.4x code, components are blocks of resusable code, but in our version 1.4x context, I see the Module Pattern as a recurring structure to these blocks of code in Angularjs, though not considered true components until version 1.5. The way these modules are implemented gives you the type of component, that is, a controllers implementation structure will distinguish it from a service or a provider, if that makes sense. I also think the Angularjs documents should have addressed this.
Here is the basic pattern I see repeated in the Angularjs code:
(function () {
// ... all vars and functions are in this scope only
// still maintains access to all globals
}());
Here is an excellent article on the Javascript Module Pattern in depth.
A component is the building block of an Angular 2 application. In Angular 2 applications everything is a component.
They are a special type of directive which are always "Restrict:E" type.
It has majorly two parts. One is the selector and the other is tempate/templateUrl:
#Component({
selector: "sample-ui",
templateUrl: "../UI/sample.html"
})
export class CustomerComponent {
/* Component logic */
}
When working with the $ionicModal in Ionic Framework, I noticed a lot of people instantiate the Modal inside the controller and pass the controller scope to the Modal.
Like so,
$ionicModal.fromTemplateUrl("views/call_options_view.html", function ($ionicModal) {
$scope.menu = $ionicModal;
}, {
scope: $scope,
animation: "slide-in-up"
});
Doing this allows the modal to invoke methods in the controller scope. Is there some way we can give a separate controller to the Modal?
Right now, using the controller scope, isn't there a MVC violation? The controller owns two views. Suppose I want the same modal available on another controller, then I would have to duplicate my functionality for the modal on both the controllers. MVC is supposed improve code reuse. So essentially, I want to re-enforce MVC by giving my modal a separate controller.
One way I thought of fixing this is by putting the modal in the Root Controller. Doing so, will make it accessible from all the child controllers and the functionality for the modal will only be available in the root controller. I still don't like this fix, cause i don't want to clutter my root controller with too much logic.
Any other suggestions?
I stumbled on your question while trying to come up with a solution similar to your concern.
Because I had a problem regarding navigation in my routes, I decided to use $ionicModal to show a view of another state in a modal view. I came up with a solution I crafted there (but I did not implement it for my working context yet) that should work in my case, while I'm not really satisfied with it.
To summarize, all my states are nested under tabs; when I am in the tabs.home state, I want to directly show the tabs.settings.sub state.
However, tabs.settings.sub relies on data populated by its parent state tabs.settings. Hence my problem with giving the scope of my current state (tabs.home) to tabs.settings.sub.
My modal uses a template that will include the template of my view:
<script id="templates/modal.html" type="text/ng-template">
<ion-modal-view>
<ng-include src="templateUrl" ng-controller="controller"></ng-include>
</ion-modal-view>
</script>
I can then reuse the view from the state. Regarding the scope, I used $scope.new(true) to isolate it, and populated it with data required by my modal template:
var subState = $state.get ('tabs.settings.sub');
var subScope = $scope.$new (true); // true: isolate
subScope.title = 'Sub';
subScope.templateUrl = subState.templateUrl;
subScope.controller = function () {
if (subState.controller)
return $controller (subState.controller, {$scope:subScope});
return null;
};
The modal is instantiated using this scope (that's one problem to my opinion: mixing the scope of the modal and the scope of the controller).
The controller has to be a function that returns the appropriate controller.
$ionicModal.fromTemplateUrl ('templates/modal.html', {
scope: subScope
}).then (function (modal) {
modal.show ();
});
The major problem with my solution is to transit data up to the controller of the view to show (in this case SubCtrl). But it is more narrowed to my specific context: my modal is not aware of the chain of inheritance of controllers adn states, because this is handled by UI router.
I don't know if it is possible to access the state associated to a controller (the usual pattern seems to be to use $state.parent, but this cannot be used here, as mentioned by the UI router wiki).
The workaround I use here (this is the part I am not satisfied with) is to federate data through the states:
.state ('tabs.settings', {
data: { status: 'valid' }
}
I have access to it when creating my modal:
subScope.status = subState.data.status;
And I have access to it from the parent controller:
$scope.status = $state.current.data.status;
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