Using $onChanges vs $onInit in angular component - javascript

Is there a difference between using Controller1 vs Controller2?
angular.module('app', [])
.component('foo', {
templateUrl: 'foo.html',
bindings: {
user: '<',
},
controller: Controller1, //Or Controller2
});
function Controller1(){
this.$onInit = function(){
this.user = angular.copy(this.user);
};
this.$onChanges = function(changes){
if(changes.user && !changes.user.isFirstChange()){
this.user = angular.copy(changes.user.currentValue);
}
};
}
function Controller2(){
this.$onChanges = function(changes){
if(changes.user){
this.user = angular.copy(changes.user.currentValue);
}
};
}
Why should I bother with $onInit when I can just do the same this in $onChanges and save some rows?
Is this type of initialization better in $onChanges and $onInit better for some other kind of initialization?

$onInit
Called on each controller after all the controllers on an element have been constructed and had their bindings initialized (and before the pre & post linking functions for the directives on this element). This is a good place to put initialization code for your controller.
$onChanges
The $onChanges lifecycle hook gets called for a few reasons. The first, is on component initialisation, it passes down that initial changes Object at runtime, so we can grab our data straight away. The second reason it gets called is only when changes occur to '<' (one-way databinding) and '#' (for evaluated DOM attribute values) that are being bound to from the parent component. Once $onChanges gets called, you’ll get a special changes Object back that you can hook into, which we’ll explore in the upcoming sections.
The primary practical difference is $onInit will be called only on the directive initialisation but $onChanges will be called during initialisation and when < and # variables changes.

Related

AngularJS 1.5 component won't invoke $onChanges upon change

I have an AngularJS 1.5 component inside an html page (not parent component)
and the component won't invoke $onChanges upon change.
HTML
<my-comp standards-data="standardsData"></my-comp>
Component
angular.module("bla").component('myComp', {
templateUrl: '/my-comp.tpl.html',
controller: myController,
bindings: {
standardsData: '<'
}
});
function myController() {
var self = this;
self.$onInit = function () {
self.standardsData = {};
}
self.$onChanges = function (changes) {
self.standardsData.something = changes.standardsData.currentValue.something;
};
}
When I fetch new data in the ctrl of the html that contains my component,
it won't influence the component. I get only into the $onInit of the component
and after $scope.standardsData changes (in the containing html/ctrl),
the $OnChanges of my component won't invoke.
Hope I described the problem correctly,
Thanks !
Your component binding to standardsData is a binding to an object reference, which does not change even when one of its properties is modified. As a result, $onChanges is not triggered. This is due to the fact that $onChanges uses a shallow $watch.
In other words, $onChanges is triggered only if we use primitives (i.e. non-objects) or change the reference of the javascript object bound into the component
So, you need to either: 1) Bind manually to the property/properties you want, rather than the object reference, or 2) Change the entire standardsData object when its data changes. You could also 3) Rewrite $onChanges functionality, I suppose (not recommended).
Option 1: Bind only to the property
Use if the parent controller/component merely changes a property or properties.
<my-comp standards-data-something="standardsData.something"></my-comp>
and
bindings: {
standardsDataSomething: '<'
}
Option 2: Change the reference
Use if the parent controller/component obtains completely new standardsData. You would keep your current HTML for this option and set:
standardsData = {newData}; //Reset the new data from http or wherever
to change the reference.
Some further reading you may find interesting:
http://blog.kwintenp.com/the-onchanges-lifecycle-hook/
http://www.codelord.net/2016/12/20/replacing-angulars-deep-watches-with-the-%24docheck-lifecycle-hook/

Accessing bindings variables in component controller

I am trying to console.log myobject but I keep getting undefined. I tried using $scope.watch and $attrs.$observe. Can someone tell me what I'm doing wrong.
here is the html
<my-component some-data="$ctrl.data"></my-component>
here is the component and controller
module.component('myComponent', {
bindings: {
needThisStuff: '<someData'
},
controller: Ctrl,
templateUrl:
requirejs.toUrl('path/to/templ.html')
});
function Ctrl(){
var self = this;
console.log(self.needThisStuff);
}
how can I access needThisStuff using $watch or $observe or anything else, I am using angular 1.5
I'm pretty sure the object is undefined because your console.log fires as soon as the component loads. The data hasn't been returned yet, so it is still undefined.
As a basic way to test this, you could wrap your console.log in a $timeout just to test.
To fix this, use angular component lifecycle hooks. Change your Ctrl to be something like this:
function Ctrl {
var self = this;
this.$onChanges = function(changesObj) {
console.log(changesObj.needThisStuff);
}
}
$onChanges is called whenever one-way bindings are updated, which in this case is your needThisStuff variable.

Is there an Angular $scope event emitted after bindings have been applied to a controller?

I'm using a directive in an Angular (1.4) application that uses the bindToController option. Here's my directive definition (written in TypeScript):
export class MyDirective implements ng.IDirective {
public restrict = 'E';
public templateUrl = 'path/to/my/template.html';
public controller = 'MyController';
public controllerAs = 'vm';
public bindToController = true;
public scope = {
myScopeProperty: '#'
};
}
This is working as expected - the myScopeProperty property is correctly bound to the controller. However, this binding process happens after the object is constructed, which means I can't perform any logic that depends on the value of this bound property while the object is being constructed. For example (again, in TypeScript):
export class MyController {
public myScopeProperty;
constructor() {
// this logs "undefined", even if the my-scope-property
// attribute on the directive has a value
console.log('myScopeProperty: ' + this.myScopeProperty);
}
}
Is there a $scope event I can listen to inside this controller object that is fired after Angular has finished applying its initial binding values to this object?
There is no such event and there's none should be, because one-way # binding means that myScopeProperty value may be updated multiple times.
$scope.$watch('vm.myScopeProperty', () => { ... }));
is the recommended way to watch for binding changes.
$timeout(() => { ... });
or $onInit controller hook (polyfillable in 1.4. with angular-component) may be used to to postpone the code to a time when myScopeProperty has been already interpolated (the first time).

Exposing angular directive function to another module

Suppose I have a module with a directive as follows (this is a rough not tested)
I need to implement 3 basic things
Configuration for the element that will appear
Event listeners that the base controller can use
Public methods that the base controller can call
angular.module("componentModule",[]) .directive("myComp",function(){
return{
replace:true,
template:'<h2>This is my component</h2>',
scope:{config= "#"},
link:function(scope,element,attr){
this.deleteElement = function(id){
//writing the code to delete this component
//This is a API function that the user can call to delete
}
if (!scope.config.visible){
//this is a configuration object for the element
this.visible(false)}
}
} })
then i have my base HTML like containing the directive call like below
<div myComm="first" config="eleConfig"></myComp>
<div myComm="second" config="newEleConfig"></myComp>
I have a separate controller for my base HTML as follows,
angular.module("baseApp",['componentModule'])
.controller('baseCtrl',function(){
$scope.eleConfig = {
visible:true,
delete:function(e){
//This is called if we call the delete method
}
}
//this is how the delete method is to be called
$scope.first.deleteElement();
})
Question
How to call the deleteElement() method in the baseCtrl as shown above (want to do it the same way KENDO UI does)
The pattern that angular uses is to expose the directive API to the scope. This is how ng-model and ng-form both expose ngModelController and ngFormController APIs.
Here is how I would do it:
angular.module("componentModule",[])
.directive("myComp",function($parse){
return{
replace:true,
scope: {
config: '&'
},
template:'<h2>This is my component</h2>',
controller: function($scope) {
//Directive API functions should be added to the directive controller here or in the link function (if they need to do DOM manipulation)
},
link:function(scope,element, attr, ctrl){
//add to directive controller
if(scope.config().visible) {
//element should be visible, etc.
}
ctrl.deleteElement = function(){
//if this function is called we want to call the config.delete method:
if(scope.config && scope.config.delete) {
//calling the scope.config() method returns the config object from the parent
scope.config().delete(element);
}
}
if(attr.myComp) {
//change to scope.$parent
scope.$parent[attr.myComp] = ctrl;
}
}
}
})
Assuming markup of:
<div my-comp="first" config="configObject"></div>
<div my-comp="second" config="configObject"></div>
In your base controller
$scope.first.deleteElement();
or
$scope.second.deleteElement();
would delete the appropriate element.
UPDATE:
I've updated the directive based on your updated question. You want to pass a config object into the directive. The best way to do that is with an & binding. If you use the & binding, you need to remember that the directive will create a new scope, and you have to attach the controller to $scope.$parent.
In your first requirement, you said you want to write the delete function in the directive, but in the case of KendoUI the actual delete(change) function implementation is done in the base controller and the delete(change) event triggered when the component value changes, which in turn calls the delete function defined in the base controller by the directive.
If you want to implement something like KendoUI does then look at this
link toplunker
Switch on the browser console to see the log. KendoUI component's change event happens automatically when the input element changes but in this case i manually triggered the delete event after 3 seconds.

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.

Categories

Resources