AngularJS - Remove custom event listener - angularjs

I have a non AngularJS snippet that is communicating with my AngularJS modules via custom events. Each of these AngularJS modules represents a different route. When I am leaving each route $onDestroy is triggered but does not remove the event listener. What am I missing to un-register my custom event?
Non AngularJS HTML Snippet
<script>
function sendEvent() {
const payload = getPayload();
const event = new CustomEvent('myCustomEvent', { payload });
window.dispatchEvent(event);
}
</script>
<button onclick="sendEvent()">Send Custom Event</button>
AngularJS Component
Class ModuleA {
$onInit() {
this.$window.addEventListener('myCustomEvent', this.onCustomEventClick.bind(this));
}
$onDestroy() {
this.$window.removeEventListener('myCustomEvent', this.onCustomEventClick.bind(this), false);
}
}
Class ModuleB {
$onInit() {
this.$window.addEventListener('myCustomEvent', this.onCustomEventClick.bind(this));
}
$onDestroy() {
this.$window.removeEventListener('myCustomEvent', this.onCustomEventClick.bind(this), false);
}
}

Every time you call bind it will create a new function and return it instead of modifying the function itself. So the even listeners you provide to addEventListener and removeEventListener are different functions, thus the registered ones are not removed.
To solve it, call bind once in $onInit and keep a reference to the returned function and always use that reference:
Class ModuleB {
$onInit() {
this.onCustomEventClick = this.onCustomEventClick.bind(this)
this.$window.addEventListener('myCustomEvent', this.onCustomEventClick);
}
$onDestroy() {
this.$window.removeEventListener('myCustomEvent', this.onCustomEventClick, false);
}
}

Related

Add angularjs component via service

I am trying to add angularjs component using the code as per below. app.component doesn't work like this
Where as if I execute app.component from outsite it works.
How to fix this issue. Notice that the API will return component names. I just need to render them.
app.service('applookup', function($http) {
this.register = function() {
return $http.get('http://localhost:3003/api/provider/fetch/app1').then(function(res){
app.component('appleComponent', {
template : 'test'
});
return res.data.componentList;
});
}
});
As #William and #Zooly mentioned in comments. We can add reference to $compileProvider.component as per below in app.config
app.register = {
component : function(name, object) {
$compileProvider.component(name, object);
return (this);
}
Then use app.register.component to register the component

SocketIO duplicate events with AngularJS

I've ran into issues in the past wherein SocketIO will emit duplicate events when using AngularJS. There are two common reasons as to why this occurs:
Creating your event listeners inside the connect event listener.
Event listeners being dynamically created multiple times (usual case with AngularJS).
Luckily, there are two common solutions that match with the above scenarios. Please see the accepted answer to understand these solutions.
Creating your event listeners inside the connect event listener
This bug occurs if you create listeners inside your connect event listener. Simply move these listeners outside the connect event listener.
Before:
socket.on('connection', _ => {
console.log('Connected');
socket.on('someOtherEvent', data => {
console.log(data);
});
});
After:
socket.on('connection', _ => {
console.log('Connected');
});
socket.on('someOtherEvent', data => {
console.log(data);
});
Event listeners being dynamically created multiple times (usual case with AngularJS).
This is a common problem if you register your event listeners inside a controller. To fix this, edit your service like so:
angular.module('socketIO', []).factory('socket', $rootScope => {
class SocketHandler {
constructor(url) {
this._socket = false;
this._url = url;
this._events = {};
this._build();
}
_build() {
// Change for your configuration options below.
this._socket = io.connect(this._URL);
}
on(event, callback) {
if(this._events.hasOwnProperty(event))
return this._events[event];
return this._events[event] = this._socket.on(event, (...args) => {
$rootScope.$apply(_ => {
callback.apply(this._socket, args);
});
});
}
emit(event, data, callback = false) {
return this._socket.emit(event, data, (...args) => {
if(!callback)
return;
$rootScope.$apply(_ => {
callback.apply(this._socket, args);
});
});
}
}
return { build: SocketHandler };
});
You can then use this in your constructor:
angular.module('someController', socketIO => {
const socket = socketIO.build('your socket url');
});
The reason this fix works is because when you switch pages in AngularJS (with the default router) it recreates an instance of the controller. This means that multiple event listeners are now bound to a single event. We prevent this from occurring by caching our event listeners in SocketHandler._events, and returning the cached listener if we have it cached.

How to update child components from the updated list of parents

I'm new to Angular and currently using version 1.6.
I'm implementing the component style of Angular. I just want to ask what's the best way to communicate from parent to child components? I know there is an existing question but I have a specific scenario (I'm not sure if it's unique or not).
Here is the scenario:
Modal -> create new todo -> Parent ( update the object ) -> personal todo ( update the list )
I have a modal for creating todo.
Then after creating new todo pass the value on the parent to update the object of todo.
And when I updated the parent list of todo pass to the personal todo components to update the list on the view.
angular.module('tab')
.controller('TabController', TabController);
function TabController() {
let vm = this;
let updatedTodoObject = {};
vm.$onInit = function () {
vm.personalTodo = vm.todo.own_todo;
vm.externalTodo = vm.todo.external_todo;
}
vm.$onChanges = function (changes) {
console.log('I\'m triggered');
}
vm.updateTodoList = updateTodoList;
function updateTodoList( result ) {
updatedTodoObject = angular.copy(vm.todo);
updatedProjectObject.user_todos.push(result)
if( vm.todo !== updatedTodoObject) {
vm.todo = updatedTodoObject;
} else {
console.log("Still in reference");
}
}
vm.getUpdatedTodotList = function( ) {
return vm.todo;
}
}
angular.module('...')
.component('...', {
bindings: {
onResultTodoUpdated: '&'
},
controllerAs: 'todo',
controller: ['TodoService', '$log', '$state', function(TodoService, $log, $state) {
let vm = this;
let todo = {};
vm.newTodoModal = function() {
TodoService.newTodoModal()
.then(function (TodoName) {
TodoService.createTodo(TodoName)
.then(function(response) {
if( response.status === 201 ) {
todo = {
...
...
}
vm.onResultTodoUpdated( { result: todo } );
}
})
.catch(function(error) {
console.log(error);
});
angular.module('...')
.component('...', {
bindings: {
todos: "<"
},
controllerAs: 'personal',
controller: function(){
let vm = this;
vm.isShowTodoArchived = false;
vm.$onInit = function () {
getWatchedTodo();
}
function getWatchedTodo () {
vm.todos = vm.todos;
vm.todosSize = vm.todos.length;
}
My question again is how I can pass the updated data after I create to the child component which is in charge of displaying the todo list?
UPDATED
<div class="tab-pane active" id="todosTab">
<nv-new-todo on-result-todo-updated="todo.updateTodoList(result)"></nv-new-project>
<div class="my-todos">
<nv-personal-todo todos="todo.personalTodo" ></nv-personal-todo>
<nv-external-todo todos="todo.externalTodo"></nv-external-todo>
</div>
</div>
How to update child components with changes from parents
Use one-way bindings <
< or <attr - set up a one-way (one-directional) binding between a local scope property and an expression passed via the attribute attr. The expression is evaluated in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. You can also make the binding optional by adding ?: <? or <?attr.
For example, given <my-component my-attr="parentModel"> and directive definition of scope: { localModel:'<myAttr' }, then the isolated scope property localModel will reflect the value of parentModel on the parent scope. Any changes to parentModel will be reflected in localModel, but changes in localModel will not reflect in parentModel.
— AngularJS Comprehensive Directive API Reference - scope
And the $onChanges life-cycle hook:
$onChanges(changesObj) - Called whenever one-way bindings are updated. The changesObj is a hash whose keys are the names of the bound properties that have changed, and the values are an object of the form { currentValue, previousValue, isFirstChange() }. Use this hook to trigger updates within a component.
— AngularJS Developer Guide - Components
With object content — Use the $doCheck Life-cycle Hook
When binding an object or array reference, the $onChanges hook only executes when the value of the reference changes. To check for changes to the contents of the object or array, use the $doCheck life-cycle hook:
app.component('nvPersonalTodo', {
bindings: {
todos: "<"
},
controller: function(){
var vm = this;
this.$doCheck = function () {
var oldTodos;
if (!angular.equals(oldTodos, vm.todos)) {
oldTodos = angular.copy(vm.todos);
console.log("new content");
//more code here
};
}
})
From the Docs:
The controller can provide the following methods that act as life-cycle hooks:
$doCheck() - Called on each turn of the digest cycle. Provides an opportunity to detect and act on changes. Any actions that you wish to take in response to the changes that you detect must be invoked from this hook; implementing this has no effect on when $onChanges is called. For example, this hook could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not be detected by Angular's change detector and thus not trigger $onChanges. This hook is invoked with no arguments; if detecting changes, you must store the previous value(s) for comparison to the current values.
— AngularJS Comprehensive Directive API Reference -- Life-cycle hooks
For more information,
AngularJS angular.equals API Reference
AngularJs 1.5 - Component does not support Watchers, what is the work around?
Simple DEMO
angular.module("app",[])
.component("parentComponent", {
template: `
<fieldset>
Inside parent component<br>
parentData={{$ctrl.parentData}}
<child-component in-data="$ctrl.parentData"></child-component>
</fieldset>
`,
controller: function () {
this.$onInit = () => {
this.parentData = 'test'
};
},
})
.component("childComponent",{
bindings: {
inData: '<',
},
template: `
<fieldset>Inside child component<br>
inData={{$ctrl.inData}}
</fieldset>
`,
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app">
<parent-component>
</parent-component>
<body>
For more information, see
AngularJS Developer Guide - Component-based application architecture
AngularJS Comprehensive API Reference - scope

Angular v1 Component to Component data transfer

I'm struggling with a problem within Angular v1.6.1 where I am trying to transfer some data from a component to another component.
I have a component called navbar which resides in app\common\navbar has a controller that fetches data from a service. The following files make up navbar component
navbar.component.js
import controller from './navbar.controller';
import template from './navbar.html';
export default navbarComponent = {
restrict: 'E',
bindings: {},
template,
controller
};
navbar.controller.js
class NavbarController {
constructor(httpService) {
const dataUrl = "/assets/data/header.json";
this.name = 'Navbar';
this.headerData = {};
console.log("In "+this.name);
httpService.getData(dataUrl)
.then((response) => {
angular.extend(this.headerData,response.data);
},(error) => { throw error; });
}
}
export default NavbarController;
navbar.html
<section>
<top-header top-data="$ctrl.headerData"></top-header>
<section>
and my httpService resides in app\services folder. It fetches content using axios http library and looks something like this
httpService.js
import axios from 'axios';
export class HttpService {
constructor() {
this.name = "HttpService";
}
getData(api_url){
return axios.get(api_url)
.then((response) => response, (error) => error);
}
}
The component which uses my navbar component's headerData is top-header and resides in app\common\top-header. This is what it contains
top-header.component.js
import template from './top-header.html';
import controller from './top-header.controller';
export default topHeaderComponent = {
restrict: 'E',
template,
bindings: {
topData: '<'
},
controller,
};
top-header.controller.js
class TopHeaderController {
constructor() {
this.name = 'TopHeader';
this.topHeaderData = {};
this.$onInit = function() {
this.topHeaderData = this.topData;
console.log(this.topHeaderData);
console.log(this.topHeaderData.telephoneNumber);
console.log(this.topHeaderData);
}
}
}
export default TopHeaderController;
top-header.html
{{$ctrl.topHeaderData.telephoneNumber}}
and finally my static files resides in assets\data and the JSON I'm trying to fetch header.json contains
header.json
{
"telephoneNumber": 12345678
}
So the problem now I see is that the data does show up in my top-header component but I'm not sure what's happening but the data disappears (comes up undefined) once I try to access the object property.
What I'm saying is that in top-header.controller.js
when I console.log(this.topHeaderData); it shows the object but when I try to console.log(this.topHeaderData.telephoneNumber); it comes up undefined
I think the problem exists because of the execution priority of the Directives. I even set navbar component priority to 5 and it didn't help as the data is undefined.
top-header.controller.js
this.$onInit = function() {
this.topHeaderData = this.topData;
console.log(this.topHeaderData); // shows topData
console.log(this.topHeaderData.telephoneNumber); // undefined
console.log(this.topHeaderData); // shows topData
}
This data this.topHeaderData.telephoneNumber is essential as I use this in my template.
How can I resolve this issue? Any help would be greatly appreciated.
The problem may be in top-header.controller.js: you're assigning binded topData to this.topheaderData in $onInit hook but when component is initialized the data hasn't been fetched yet. Instead of $onInit you should use $onChange hook method which is called by Angular when binded property is updated (in your case when data is fetched from server)
Angular component docs:
$onChanges(changesObj) - Called whenever one-way bindings are updated.
The changesObj is a hash whose keys are the names of the bound
properties that have changed, and the values are an object of the form
{ currentValue, previousValue, isFirstChange() }. Use this hook to
trigger updates within a component such as cloning the bound value to
prevent accidental mutation of the outer value.

angularJS ES6 Directive

I am trying to develop an application in angular es6 . I have a problem with directve.
Here is my code
export default class RoleDirective {
constructor() {
this.template="";
this.restrict = 'A';
this.scope = {
role :"#rolePermission"
};
this.controller = RoleDirectiveController;
this.controllerAs = 'ctrl';
this.bindToController = true;
}
// Directive compile function
compile(element,attrs,ctrl) {
console.log("df",this)
}
// Directive link function
link(scope,element,attrs,ctrl) {
console.log("dsf",ctrl.role)
}
}
// Directive's controller
class RoleDirectiveController {
constructor () {
console.log(this.role)
//console.log("role", commonService.userModule().getUserPermission("change_corsmodel"));
//$($element[0]).css('visibility', 'hidden');
}
}
export default angular
.module('common.directive', [])
.directive('rolePermission',[() => new RoleDirective()]);
The problem is i couldn't get the role value inside constructor.
here is my html implementation
<a ui-sref="event" class="button text-uppercase button-md" role-permission="dfsd" detail="sdfdsfsfdssd">Create event</a>
If i console this it will get the controller object. But it will not get any result while use this.role.
Ok, so I managed to find out how this works.
Basically, the scope values cannot be initialized on the controller's constructor (because this is the first thing executed on a new object) and there is also binding to be considered.
There is a hook that you can implement in your controller that can help you with your use case: $onInit:
class RoleDirectiveController {
constructor () {
// data not available yet on 'this' - they couldn't be
}
$onInit() {
console.log(this.role)
}
}
This should work. Note that this is angular1.5+ way of doing things when not relying on $scope to hold the model anymore. Because if you use the scope, you could have it in the controller's constructor (injected).

Resources