What can controllers do that directives can't? [duplicate] - javascript

This question already has an answer here:
Difference between and when to use controllers vs directives?
(1 answer)
Closed 4 years ago.
I'm just beginning AngularJS. When I first started reading about it, it seemed from the beginner tutorials that the controller is the basic building block of an angular app. However, since learning about directives, I've been creating my own little Angular app with only directives, literally not a single controller. I can't see why I would ever need a controller.
The only thing I've ever seen done with a controller is add variables to scope:
angular.controller("myController",
function($scope)
{
$scope.x = 5;
$scope.y = 6;
}
)
But I can do that with a directive too, by using the scope argument passed to the link function.
Is there something else that can be done with controllers, which can't be done with directives? Or at least something which is easier to do with controllers than with directives?
For example, if I just needed to populate scope with some variables x an y, I can just do:
angular.directive(
"myDirective",
function()
{
return {
link: function(scope, element, attributes)
{
scope.x = 5;
scope.y = 6;
}
};
}
);

You can probably write pretty much everything your app needs in a link callback, sure. Note that I'm not even calling it a directive, I'm saying a link callback. A directive is something that defines a custom HTML tag and its associated functionality, a link callback is merely a specific part of that.
The thing is that this is little more than working with jQuery, or using addEventListener to attach behaviour to HTML elements. On the other hand, you can write controllers as classes instead of procedural code manipulating the scope object. Here's my preferred style to write angularjs in typescript:
export default class WidgetController {
error: string;
static $inject = ['$state', 'FooService', 'BarService'];
constructor(
protected $state: angular.ui.IStateService,
protected foo: FooService,
protected bar: BarService
) {}
get fooValue() {
return this.foo.baz;
}
doSomething() {
this.error = null;
this.bar.getSomething().then(data => {
if (data.error) {
this.error = data.error;
} else {
this.$state.go('success');
}
});
}
}
A template for this might look like this:
<h1>{{ $ctrl.fooValue }}</h1>
<button ng-click="$ctrl.doSomething()">Do!</button>
<p>{{ $ctrl.error }}</p>
The controller may be attached to the template using the ui-router:
import WidgetController from 'widget-controller';
module.config(['$stateProvider', ($state: angular.ui.IStateProvider) => {
$state.state('widget', {
controller: WidgetController,
controllerAs: '$ctrl',
templateUrl: '/templates/widget.html',
});
}]);
Or as a component:
module.component('widget', {
templateUrl: '/templates/widget.html',
controller: WidgetController,
bindings: {
error: '#'
}
});
Or using ng-controller or in a number of other ways.
It gives you more flexibility. It allows you to test the controller pretty easily in isolation, since it's just a regular class. It allows you to reuse the controller for different templates, and the same template for different controllers (yes, this can actually be really useful). It's IMO more readable and easier to understand. Specifically using $ctrl. in the template prevents you from building too interdependent nested scopes and explicitly binds the template to use only its controller, instead of some implicit scope.
There are many ways to do things, but the one thing I have figured out over time is that dealing with the scope object is both verbose and annoying, and can easily lead to spaghetti code. So, moving away from that, you're soon arriving at controllers as objects.

Related

Angular 1.6 component transclusion scope

I'm trying to figure out how to get data into a component transclusion in Angular 1.6.4. The scenario has a component, a directive (not re-written as a component yet) and a service for inter-component communication.
angular.module('app')
.service('svc', function() {
this.connector = {};
})
.directive('first', ['svc', function($svc) { return {
restrict: 'E',
scope: { 'id': '#' },
template: '<button ng-click="GetData()">get data</button>',
controller: ['$scope', 'svc', function($scope, $svc) {
$scope.connector = { data: [] };
$svc.connector[$scope.id] = $scope.connector;
$scope.GetData = function() {
// This is a mock-up; I'm really doing a REST call.
$scope.connector.data = [
{id: 0, name: 'one'},
{id: 1, name: 'two'}
];
};
}]
}; }])
.component('second', {
bindings: { parent: '#firstid' },
transclude: true,
template: '<ng-transclude></ng-transclude>',
controller: ['svc', function($svc) {
this.data = $svc.connector[this.parent];
// Not sure what to do here
}]
})
;
My HTML looks something like this:
<first id="first-thing"></first>
<second firstid="first-thing">
Where I expect my data to be: {{$ctrl | json}}<br/>
... but maybe here: {{$ctrl.$parent | json}}<br/>
... or even here: {{$parent | json}}<br/>
<div ng-repeat="item in $ctrl.data">
<p>Output: {{item.id}}/{{item.name}}</p>
</div>
</second>
These may not be nested with a require, which is why I'm using a service to store my data; <first><second></second></first> is not an option. I can manage getting data from the service inside the component controller using some $onInit workarounds where necessary. I've checked and the service contains the correct data at the correct times. In the interest of component reuse, I need the controller to transclude content.
Batarang lists all my scopes. The directive has a scope, $id 6 (there are other things on the page), as expected. The component has a scope, $id 7, as expected. These scopes contain the correct data based on what I've put in them and what I'd expect.
My problem is that I have an additional scope, $id 8. It appears to be the transcluded scope and it is a sibling of 6 and 7 (these are peers on $id 5, my page controller). As noted in my HTML snark, I expected the component transclusion to live in 7. I would be fine if 8 was a child scope of 7, but it's a disconnected sibling. I tried additional bindings but I can't get them to populate so they just throw. I'm clearly doing something wrong because what I'm getting is the model that pre-1.3 used for transclusion scope inheritance.
Can someone tell me where I've gone astray or at least point me towards the correct solution?
I've figured it out. In passing, I should note that according to the literature on the Internet, I'm doing something that I probably shouldn't do. I understand where the authors of Angular are coming from with trying to isolate scopes down the chain but I don't agree with that model, at least for transclusion.
angular.module('app')
.service('svc', function() {
this.connector = {};
})
.directive('first', ['svc', function($svc) { return {
restrict: 'E',
scope: { 'id': '#' },
template: '<button ng-click="GetData()">get data</button>',
controller: ['$scope', 'svc', function($scope, $svc) {
$scope.connector = { data: [] };
$svc.connector[$scope.id] = $scope.connector;
$scope.GetData = function() {
// This is a mock-up; I'm really doing a REST call.
$scope.connector.data = [
{id: 0, name: 'one'},
{id: 1, name: 'two'}
];
$scope.connector.data.Update($scope.connector.data);
};
}]
}; }])
.component('second', {
bindings: { parent: '#firstid' },
transclude: true,
template: '<ng-transclude></ng-transclude>',
controller: ['$element', '$transclude', '$compile', 'svc', function($element, $transclude, $compile, $svc) {
this.$onInit = () => { angular.extend(this, $svc.connector[this.parent]; };
var parentid = $element.attr('firstid');
$transclude((clone, scope) => {
$svc.connector[parentid].Update = (data) => {
angular.extend(scope, data);
$element.append($compile(clone)(scope));
};
});
}]
})
;
How it works
This is essentially manual transclusion. There are too many examples on the Internet about manual transclusion where people modify the DOM manually. I don't completely understand why some people think this is a good idea. We jump through so many hoops to separate our markup (HTML) from our formatting (CSS) from our code (Angular directives/components) from our business logic (Angular services/factories/providers), so I'm not going to go back to putting markup inside my code.
I found this article and a comment on an Angular issue by Gustavo Henke that used the scope inside $transclude to register a callback. With that key bit of information, I figured I could do much more scope manipulation.
The code in $transclude seems to be outside the digest cycle. This means that anything touched inside it will not receive automatic updates. Luckily, I have control of my data's change events so I pushed through this callback. On the callback, the data are changed and the element is recompiled. The key to locate the callback in the service hasn't been bound from the controller tag yet so it has to be retrieved from the attributes manually.
Why this is bad
Components are not supposed to modify data outside their own scope. I am specifically doing exactly not-that. Angular doesn't seem to have a more appropriate primitive for doing this without breaking some other concern that's more important to leave intact, in my mind.
I think there's a, "memory leak," in this, which is to say that my element and scope aren't being disposed of correctly with each update cycle. Mine uses fairly little data, it is updated only directly by the user with a throttle and it's on an administration interface; I'm okay with leaking a little memory and I don't expect the user will stay on the page long enough for it to make a difference.
My code all expects things to be in the right place and named the right things in the markup. My real code has about four times as many lines as this and I'm checking for errors or omissions. This is not the Angular way which means I'm probably doing something wrong.
Credits
Without the Telerik article, I would have been sitting next to an even bloodier mark on my wall right now.
Thanks to Ben Lesh for his comprehensive post about $compile with appropriate disclaimers about how one shouldn't use it.
Todd Motto helped a bunch with how to write a decent Angular 1.x component in his post on upgrading to 1.6. As one may expect, the Angular documentation on components doesn't offer much more than specific pointers to exactly what things are called.
There's a little information at the bottom of AngularJS issue 7842 that does something similar and may even have a better method for managing scoped data more appropriately than I did.

Method polymorphism / inheritance in AngularJS 1.6

I am facing problem with controllers overriding in AngularJS.
Here comes the problem:
function UiCoreController($scope, ...) {
'use strict';
angular.extend(vm, {
validatePremium: validatePremium
}
function validatePremium() {
console.log('validate premium in core controller');
// validation logic
vm.calculatePremium();
}
function calculatePremium() { ... }
}
function UiAskController($rootScope, $scope, $controller) {
'use strict';
var vm = this;
var core = $controller('UiCoreController', { $scope: $scope });
angular.extend(this, core);
angular.extend(vm, {
...
}
function calculatePremium() { ... }
}
When I call validatePremium() method from template (by ng-change directive) - it is called from UiCoreController, but calculatePremium() is not called by more descriptive UiAskController::calculatePremium().
That works only if copy-paste whole validatePremium() method to UiAskController but it seems to me like code duplication.
Would anyone help me achieving the calls to calculatePremium() in Java-like code?
Javascript is not OOP like Java and therefore there is no polymorphism and inheritance. So short answer is no. You cannot achieve what you want by using controllers only. The function calculatePremium is bind to controller scope and for sure you don't want to share scopes between controllers.
You can write this method to $rootScope but its not good practice
I would put vm.calculatePremium() logic into service. Its a right place to make this kind of stuff.
Keep in mind that main purpose of controllers is to bind your data to DOM a.e. render view by using scope.
Since Service is a singleton, all data manipulations you can do in services to avoid code duplicate and make code maintenance easy

Are variable bound / 1st class functions preferable over private method naming? How is hoisting affected?

A few questions regarding structuring Angular code and the behavior of JavaScript when using variable bound vs private method naming function conventions. Is there a performance or stylistic reason for using variable bound functions / first class functions in AngularJS over private method naming? How is hoisting affected in each method? Would the second method below reduce the amount of hoisting performed and would this have a noticeable affect on application performance?
An example of private method naming. Is this a recommended way to structure Angular code?
(function () {
'use strict'
function modalController(dataItemsService, $scope) {
var vm = this;
function _getSelectedItems() {
return dataItemsService.SelectedItems();
}
function _deleteSelectedItems() {
dataItemService.DeleteItem();
$("#existConfirmDialog").modal('hide');
}
vm.SelectedItems = _getSelectedItems;
vm.deleteItemRecord = _deleteItemRecord;
}
angular.module('app').controller('modalController', ['dataItemService', '$scope', modalController]
})();
An example of variable bound functions. What about this method of structuring angular code within the controller - is there any disadvantage or advantage in terms of performance/style?
angular.module('appname').controller("NameCtrl", ["$scope", "$log", "$window", "$http", "$timeout", "SomeService",
function ($scope, $log, $window, $http, $timeout, TabService) {
//your controller code
$scope.tab = 0;
$scope.changeTab = function(newTab){
$scope.tab = newTab;
};
$scope.isActiveTab = function(tab){
return $scope.tab === tab;
};
}
]);
The first method, using "private" methods and exposing them via public aliases, is referred to as the Revealing Module Pattern, although in the example the methods aren't actually private.
The latter is a pretty standard Constructor Pattern, using $scope as context.
Is there a performance or stylistic reason for using variable bound functions / first class functions in AngularJS over private method naming?
Is [there] a recommended way to structure Angular code?
TL;DR
Fundamentally, there isn't much difference between the two styles
above. One uses $scope, the other this. One Constructor function is defined in a closure, one is defined inline.
There are scenarios where you may want a private method or value.
There are also stylistic and (probably insignificant) performance
reasons for using the variable this/vm over $scope. These are not mutually exclusive.
You'll probably want to use a
basic, bare bones, old school Constructor Pattern, and a lot of
people are exposing state and behavior via this instead of $scope.
You can allow yourself data privacy in the Controller, but most of
the time this should be leveraged by a Service/Factory. The main exception is data representative of the state of the View.
Don't use jQuery in your Controller, please.
References:
AngularJS Style Guide by Todd Motto.
AngularJS Up & Running
To answer your question thoroughly, I think it important to understand the responsibility of the Controller. Every controller's job is to expose a strict set of state and behavior to a View. Put simply, only assign to this or $scope the things you don't mind your user seeing or playing with in your View.
The variable in question (vm, $scope) is the context (this) of the instance being created by the Controller function.
$scope is Angular's "special" context; it has some behaviors already defined on it for you to use (e.g. $scope.$watch). $scopes also follow an inheritance chain, i.e. a $scope inherits the state and behaviors assigned to its parent $scope.
Take these two controllers:
angular.module("Module")
.controller("Controller", ["$scope", function($scope) {
$scope.tab = 0;
$scope.incrementTab = function() {
$scope.tab++;
};
}])
.controller("OtherController", ["$scope", function($scope) {
// nothing
}]);
And a view
<div ng-controller="Controller">
<p>{{ tab }}</p>
<button ng-click="incrementTab();">Increment</button>
<div ng-controller="OtherController">
<p>{{ tab }}</p>
<button ng-click="incrementTab();">Increment</button>
</div>
</div>
Example here
What you'll notice is that even though we didn't define $scope.tab in OtherController, it inherits it from Controller because Controller is it's parent in the DOM. In both places where tab is displayed, you should see "0". This may be the "hoisting" you're referring to, although that is an entirely different concept.
What's going to happen when you click on the first button? In both places we've exposed "tab", they will now display "1". Both will also update and increment when you press the second button.
Of course, I may very well not want my child tab to be the same tab value as the parent. If you change OtherController to this:
.controller("OtherController", ["$scope", function($scope) {
$scope.tab = 42;
}]);
You'll notice that this behavior has changed - the values for tab are no longer in sync.
But now it's confusing: I have two things called "tab" that aren't the same. Someone else may write some code later down the line using "tab" and break my code inadvertently.
We used to resolve this by using a namespace on the $scope, e.g. $scope.vm and assign everything to the namespace: $scope.vm.tab = 0;
<div ng-controller="OtherController">
<p>{{ vm.tab }}</p>
<button ng-click="vm.incrementTab();">Increment</button>
</div>
Another approach is to use the simplicity and brevity of this and take advantage of the controllerAs syntax.
.controller("OtherController", function() {
this.tab = 0;
});
<div ng-controller="OtherController as oc">
<p>{{ oc.tab }}</p>
</div>
This may be more comfortable for people who are used to using plain JS, and it's also easier to avoid conflicts with other Angular sources this way. You can always change the namespace on the fly. It's also a bit "lighter" on performance since you're not creating a new $scope instance, but I'm not sure there's much gain.
In order to achieve privacy, I would recommend encapsulating your data in a Service or Factory. Remember, Controllers aren't always singletons; there is a 1:1 relationship between a View and a Controller and you may instantiate the same controller more than once! Factories and Service objects are, however, singletons. They're really good at storing shared data.
Let all Controllers get a copy of the state from the singleton, and make sure all Controllers are modifying the singleton state using behaviors defined on the Service/Factory.
function modalController(dataItemsService) {
var vm = this;
vm.selectedItems = dataItemsService.SelectedItems(); // get a copy of my data
vm.updateItem = dataItemService.UpdateItem; // update the source
}
But wait, how do I know when another part of my app has changed my private data? How do I know when to get a new copy of SelectedItems? This is where $scope.$watch comes into play:
function modalController(dataItemsService, $scope) {
var vm = this;
vm.updateItem = dataItemService.UpdateItem; // update the source
// periodically check the selectedItems and get a fresh copy.
$scope.$watch(dataItemsService.SelectedItems, function(items) {
vm.items = items;
});
// thanks $scope!
}
If your data is not shared, or if your private data is representative of the View layer and not the Model layer, then it's totally OK to keep that in the controller.
function Controller() {
var buttonClicked = false;
this.click = function() {
buttonClicked = true; // User can not lie and say they didn't.
};
}
Lastly, DO NOT USE JQUERY IN YOUR CONTROLLER, as your reference did!
$("#existConfirmDialog").modal('hide');
This example might not be purely evil, but avoid accessing and modifying the DOM outside a Directive, you don't want to break other parts of your app by modifying the DOM underneath it.

Is there a way to get access to parent scope from an Angular UI nested named view?

I have several screens in my application that contain nested views. I have found it easier to make modal dialog boxes and panels that slide in and out to be actual nested states with actual urls rather than be some arbitrary javascript magic embedded within the html. I found this approach to be good for reuse and making new pages that require dialogs and panels. It simplifies everything a great deal.
For example, here is what modal and right panel child states might look like when defined as Angular UI states:
.state('courses.detail.courseVersion.page', {
url: '/sections/:sectionId/pages/:pageId',
templateUrl: 'app/courses/courses.detail.courseVersion.page.html',
resolve: {
page: function(Model, $stateParams) {
{not relevant}
}
},
controller: 'PageDetailController'
})
.state('courses.detail.courseVersion.page.edit', {
url: '/edit',
views: {
'rightPanel#': {
templateUrl: 'app/courses/courses.detail.courseVersion.page.edit.html',
resolve: {
{not relevant}
},
controller: 'PageFormController'
}
}
})
.state('courses.detail.courseVersion.page.delete', {
url: '/delete',
views: {
'modal#': {
templateUrl: 'app/courses/courses.detail.courseVersion.deletePage.html',
resolve: {
{not relevant}
},
controller: 'DeletePageController'
}
}
})
In DeletePageController or PageFormController, what I'd like to do is access the scope in PageDetailController. For example, if the Page's title was edited, I'd like to update the parent scope with that information rather than reload the page entirely (which is what I'm currently doing, however this defeats the entire point of using ajax).
My first instinct was to look at the value for $scope.$parent, however, it is actually null. This is different than when using regular nested states that don't have named views.
How can I reliably access the parent scope without using the root scope or a custom service to solve this problem? I simply want to call a function on the parent scope to update the values - this will solve my problem nicely, but it doesn't look possible without using the root scope.
Although already answered by Mathew Berg that you should use a Service to share data between controllers you CAN share/inherit scope variables (though probably not encouraged).
<div ng-controller="BaseCtrl">
<div ng-controller="InnerCtrl">
...
</div>
</div>
function BaseCtrl($scope) {
$scope.shared = {
fun : true
};
}
function InnerCtrl($scope) {
console.log('fun', $scope.shared.fun);
$scope.shared.lol = 'always';
}
I was using a simple (working) syntax for the example. Just remember to place properties ON the shared object - do NOT overwrite it like this:
function InnerCtrl($scope) {
$scope.shared = {
lol: 'always'
};
}
That will break the link to the object the BaseCtrl is holding. Sometimes you need to apply several properties and typing/copying them manually can be hard. You can do this instead:
function InnerCtrl($scope) {
var model = {
lol : 'always',
shout: function () { console.log('yay!'); }
};
angular.copy($scope.shared, model); // shared now also holds lol and shout.
}
You should be using a service that all of them share that can be accessed.

How to trigger methods on my custom directive?

I have a custom directive that I'm using in my templates. It does a bit of DOM work for me. I would like the host view/controller that I'm using the directive in to be able to run methods on my directive (and it's controller). But I'm not sure how best to call into the directives scope.
Example Fiddle
My view code:
<div ng-app="app">
<div ng-controller="MainCtrl">
<h3>Test App</h3>
<button ng-click="scopeClear()">Parent Clear</button>
<div my-directive string="myString"></div>
</div>
</div>
Here is the custom directive:
angular.module('components', []).directive('myDirective', function() {
function link(scope, element, attrs) {
scope.string = "";
scope.$watch(attrs.string, function(value) {
scope.string = value;
});
}
return {
controller: function($scope, $element) {
$scope.reset = function() {
$scope.string = "Hello";
}
$scope.clear = function() {
$scope.string = "";
}
},
template:
"<button ng-click='reset()'>Directive Reset</button>" +
"<button ng-click='clear()'>Directive Clear</button><br/>" +
"<input type='text' ng-model='string'>",
link: link
}
});
And controller:
angular.module('app', ['components']).controller('MainCtrl', function($scope) {
$scope.myString = "Hello";
$scope.scopeClear = function() {
// How do I get this to call the clear() method on myDirective
}
});
The workaround I found is jQuery('#my_directive').scope().myMethod(); But this seems wrong, like I'm missing some better part of angular to do this.
It also seems like and $emit isn't right here since I want a targeted method so it won't trigger on additional instances of the directive I have on the same page.
How would I access the directives methods from my parent controller?
I'm not sure I fully understand your objective here, and it's possible you could find a better pattern completely. Typically, directives display the state of the scope which is either an isolate scope (if they are self-sufficient) or a shared scope. Since you are not creating an isolate scope then they inherit the scope from the controller. If they are displaying data inherited from the controller then you don't want your controller calling into the directive, rather the directive will simply "redraw" itself whenever the properties in the controller change.
If you, instead, are looking to recalculate some stuff in your directives based on events from outside the directive you don't want any tight coupling - especially if building an entirely separate module. In that case, you might simply want to use $broadcast from the $scope within MainCtrl to broadcast an event that you may care about, and then your directive can provide the $on('eventName') handler. This way it's portable to any controller/scope that will fire such an event.
If you find yourself needing to know the exact properties in the controller or the exact functions within the directive then I would suggest that you have too-tightly coupled these pieces and they don't belong in separate modules since they could never be reused. Angular directives and controllers are not objects with functions, but objects that create scope and update frequently via $digest calls whenever properties in that scope change. So you may be able to find a way to better model the data, objects, and properties you are displaying. But I can't say without greater context.

Categories

Resources