I have built a function that correctly appends a new row to a table when the last row gets the focus.
This works successfully, but the directive is no longer triggering the cloned row
How do I fix the clone so that the elements are added with directives attached.
I need to trigger the directive link after the clone is complete.
each row has a directive attached.
<tr add-table-row-empty>
<td>...
And this is the directive.
module Panda {
#directive('$log', '$compile')
export class AddTableRowEmpty implements ng.IDirective
{
public restrict: string = "A";
constructor(public $log: ng.ILogService, public $compile: ng.ICompileService)
{
}
public link: Function = ($scope: any, element: angular.IAugmentedJQuery, attrs: angular.IAttributes) => {
var inputs = $('input', element);
inputs.on('focus', () => this.addIfLastRow(element));
}
private addIfLastRow(element: angular.IAugmentedJQuery) {
if (!($(element).is(':last-child')))
return;
this.addRow(element);
}
private addRow(element: angular.IAugmentedJQuery)
{
// this should do a deep clone including all events etc.
var clone = element.clone(true, true);
$("input", clone).each((i, _) => $(_).val(""));
element.after(clone);
clone
.hide()
.fadeIn(1000);
}
}
panda.directive("addTableRowEmpty", <any>AddTableRowEmpty);
}
Related
For an educational side-project I am working on, I want to avoid using AngularJS Material Design, UI Bootstrap, or any custom libraries that provide modal functionality.
However, I've hit a snag. I've created a service that is supposed to manage and dynamically create modals. It provides an open function that accepts a spec object, which it then reproduces in the DOM.
What this code actually does:
1. The modal is correctly appended to the DOM.
2. The modal controller's $onInit function fires.
What this code does not do:
1. Bind the $ctrl.message property in the markup to the instance of the controller that we know starts.
Normally, I would ask my question after providing code, however there's a good bit of code required to reproduce this problem (it's below, sans some AngularJS boilerplate.) As that's the case, though, here's my question:
In what way can I get the modals being spun off by this service to properly bind their contents to their given controller?
What I've tried:
As you can see in ModalService.bindModalDiv, I've tried a few avenues of thought, mostly using $compile. Yet, $compile and the resulting link function don't actually seem to be binding the new DOM elements to Angular.
I've tried using $controller to explicitly bind the new scope being generated to the someModalCtrl being instantiated, but that doesn't seem to help at all.
Because I can hit breakpoints on the someModalCtrl, and see the console.log message I used as a sanity check, I think I'm misunderstanding how exactly I'm supposed to bind the new DOM elements to Angular. I'm sure I'm missing something basic that I've managed to forget about or disregard, somehow.
One more note:
I'm sure my problems with getting the modal to bind to AngularJS properly aren't the only problems present. Please remember, I'm doing this partially as a learning excersize; if y'all can help me figure out my modal problem, I'll keep on doing my due diligence and hunting down the flaws I've doubtless built into this approach. Therefore, if you see something that's not a modal problem, it's OK to draw my attention to it, but I won't rewrite the question to fix whatever you find - unless it's absolutely essential that I do. As an example - I know that ModalService.open has some issues in how I'm implementing the promise setup. A $rootScope.$watch is probably more reasonable.
modalSvc.ts:
export interface IModalSpecObject {
parent?: string | Element | JQuery;
templateURL: string
controller: string;
controllerAs?: string;
data: object;
}
export class ModalInstance {
public isOpen: boolean = true;
public returnData: object = null;
public element: JQLite = null;
public $parent: JQuery = null;
public constructor(
public specObject: IModalSpecObject
) {
}
public close(returnData: object): void {
if (this.element)
this.element.remove();
this.isOpen = false;
this.returnData = returnData;
}
}
export class ModalService {
public pollRate: number = 250;
public instance: ModalInstance = null;
public static $inject: string[] = [
'$q', '$rootScope', '$compile', '$controller'
];
public constructor(
public $q: ng.IQService,
public $rootScope: ng.IRootScopeService,
public $compile: ng.ICompileService,
public $controller: ng.IControllerService
) {
}
public open(specObject: IModalSpecObject): ng.IPromise<{}> {
if (this.instance && this.instance.isOpen)
this.instance.close(null);
this.instance = new ModalInstance(specObject);
const $parent: JQuery = this.setParent(specObject);
const modalDiv: JQLite = this.buildModal(specObject);
this.bindModalDiv(modalDiv, $parent);
const result: ng.IPromise<{}> = this.$q((resolve) => {
setInterval(() => {
if (!this.instance.isOpen) {
resolve(this.instance.returnData);
}
}, this.pollRate);
});
return result;
}
private buildModal(specObject: IModalSpecObject): JQLite {
const modalDiv: JQLite = angular.element('<div/>');
modalDiv.addClass('modal');
const $modalPanel: JQuery = $('<div/>');
$modalPanel.addClass('modal-panel');
// Inject HTML template...
$modalPanel.load(specObject.templateUrl);
// Set up the angular controller...
const controllerAs: string = specObject.controllerAs
? specObject.controllerAs
: '$ctrl';
$modalPanel.attr('ng-controller', `${specObject.controller} as ${controllerAs}`);
modalDiv.append($modalPanel);
this.instance.element = modalDiv;
return modalDiv;
}
private setParent(specObject: IModalSpecObject): JQuery {
let $parent: JQuery;
if(!specObject.parent)
$parent = $(document);
else if (typeof specObject.parent === "string"
|| specObject.parent instanceof Element)
$parent = $(specObject.parent);
else if (specObject.parent instanceof jQuery)
$parent = specObject.parent;
else
$parent = $(document);
this.instance.$parent = $parent;
return $parent;
}
// !!!! !!!! I suspect this is where my problems lie. !!!! !!!!
private bindModalDiv(modalDiv: JQLite, $parent: JQuery): void {
const newScope: ng.IScope = this.$rootScope.$new(true);
// Try #1: Bind generated element to parent...
//$parent.append(this.$compile(modalDiv)(newScope));
// Try #1a: Generate bindings, then append to parent...
//const element: JQLite = this.$compile(modalDiv)(newScope);
//$parent.append(element);
// Try #2: Bind element to parent, then generate ng bindings...
//$parent.append(modalDiv);
//this.$compile(modalDiv)(newScope);
// Try #3: Well, what if we bind a controller to the scope?
const specObject: IModalSpecObject = this.instance.specObject;
const controllerAs: string = specObject.controllerAs
? specObject.controllerAs
: '$ctrl';
this.$controller(`${specObject.controller} as ${controllerAs}`, {
'$scope': newScope
});
const element = this.$compile(modalDiv)(newScope);
$parent.append(element);
}
}
angular
.module('app')
.service('modalSvc', ModalService);
SomeController.ts:
SomeController.ts pretty much just controls a button to trigger the modal's appearance; I've not included the markup for that reason.
export class SomeController {
public static $inject: string[] = [ 'modalSvc' ];
public constructor(
public modalSvc: ModalService
) {
}
public $onInit(): void {
}
public openModal(): void {
const newModal: IModalSpecObject = {
parent: 'body',
templateUrl: '/someModal.html',
controller: 'someModalCtrl',
data: {
'message': 'You should see this.'
}
};
this.modalSvc.open(newModal)
.then(() => {
console.log('You did it!');
});
}
}
angular.module('app').controller('someCtrl', SomeController);
someModal.html:
<div class="modal-header">
Important Message
</div>
<!-- This should read, "You should see this." -->
<div class="modal-body">
{{ $ctrl.message }}
</div>
<!-- You should click this, and hit a breakpoint and/or close the modal. -->
<div class="modal-footer">
<button ng-click="$ctrl.close()">Close</button>
</div>
someModal.ts:
export class SomeModalController {
public message: string = '';
public static $inject: string[] = [ 'modalSvc' ];
public constructor(
public modalSvc: ModalService
) {
}
public $onInit(): void {
console.log('$onInit was triggered!');
this.message = this.modalSvc.instance.specObject.data['message'];
}
public close(): void {
this.modalSvc.instance.close(null);
}
}
angular
.module('app')
.controller('someModalCtrl', SomeModalController);
I figured out where I went wrong - I needed to use $().load()'s callback. JQuery load is asynchronous, which meant that $compile was working correctly; however, the HTML in my modal partial wasn't loaded by the time $compile had done its job, thus the unbound HTML.
A slight modification of my ModalService got around this, though.
Revised fragment of ModalSvc.ts:
// This is just a convenience alias for void functions. Included for completeness.
export type VoidFunction = () => void;
// ...
public open(specObject: IModalSpecObject): ng.IPromise<{}> {
if (this.instance && this.instance.isOpen)
this.instance.close(null);
this.instance = new ModalInstance(specObject);
const $parent: JQuery = this.setParent(specObject);
// open already returned a promise before, we just needed to return
// the promise from build modal, which in turn sets up the true
// promise to resolve.
return this.buildModal(specObject)
.then((modalDiv: JQLite) => {
this.bindModalDiv(modalDiv, $parent);
const result: ng.IPromise<{}> = this.$q((resolve) => {
// Also, side-note: to avoid resource leaks, always make sure
// with these sorts of ephemeral watches to capture and release
// them. Resource leaks are _no fun_!
const unregister: VoidFunction = this.$rootScope.$watch(() => {
this.instance.isOpen
}, () => {
if (! this.instance.isOpen) {
resolve(this.instance.returnData);
unregister();
}
});
});
return result;
});
}
private buildModal(specObject: IModalSpecObject): ng.IPromise<{}> {
const modalDiv: JQLite = angular.element('<div/>');
modalDiv.addClass('modal');
this.instance.element = modalDiv;
const $modalPanel: JQuery = $('<div/>');
$modalPanel.addClass('modal-panel');
// By wrapping $modalPanel.load in a $q promise, we can
// ensure that the modal is fully-built before we $compile it.
const result: ng.IPromise<{}> = this.$q((resolve, reject) => {
$modalPanel.load(specObject.templateUrl, () => {
modalDiv.append($modalPanel);
resolve(modalDiv);
});
});
return result;
}
private setParent(specObject: IModalSpecObject): JQuery {
let $parent: JQuery;
if(!specObject.parent)
$parent = $(document);
else if (typeof specObject.parent === "string"
|| specObject.parent instanceof Element)
$parent = $(specObject.parent);
else if (specObject.parent instanceof jQuery)
$parent = specObject.parent;
else
$parent = $(document);
this.instance.$parent = $parent;
return $parent;
}
private bindModalDiv(modalDiv: JQLite, parent: JQLite): void {
// parent should be a JQLite so I can use the injector() on it.
parent.injector().invoke(['$rootScope', '$compile', ($rootScope, $compile) => {
const newScope: ng.IScope = $rootScope.$new(true);
this.$controller(this.getControllerAsString(), {
'$scope': newScope
});
const element: JQLite = $compile(modalDiv)(newScope);
parent.append(element);
}]);
}
private getControllerAsString(): string {
const specObject: IModalSpecObject = this.instance.specObject;
const controllerAs: string = specObject.controllerAs
? specObject.controllerAs
: '$ctrl';
return `${specObject.controller} as ${controllerAs}`;
}
I figured this out by going back and doing the step-by-step engineering. I first ensured that $compile was working by creating an element whose contents were {{ 2 + 2 }}, compiling it, then injecting it. When I saw 4 added to my page, I knew that the compile-then-inject aspect of the program worked just fine.
From there, I started building up the modal's construction, and found it working flawlessly...up until I got to jQuery load. When I read the documentation, I saw the error of my ways.
TL;DR: Read the friendly manual!
I'd like to write own directive which can wrap any html element in hyperlink using the passed parameter, so for example:
<button myDirective="parameter">...</button>
will have following effect
<button>...</button>
I'm beginner in AngularJS. Unfortunately I didn't find any helpful tutorial for making this in typescript.
I created sth like that:
export default class LinksHelperDirective implements ng.IDirective {
public static Name = "kb-link";
public restrict = "A";
public urlTemplate = "";
constructor(private readonly $parse: ng.IParseService) {
}
public static Factory() : any {
const directive = ($parse: ng.IParseService) => {
return new LinksHelperDirective($parse);
};
directive["$inject"] = ["$parse"];
return directive;
}
link = (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes,
ngModel: ng.INgModelController) => {
const linkId = this.$parse(attrs["kb-link"])(scope);
const wrapper = angular.element('');
element.wrap(wrapper);
};
}
But unfortunately it doesn't work... even constructor is not called. I registered directive in index file like below:
module.directive(LinksHelperDirective.Name, LinksHelperDirective.Factory(), ["$parse"]);
and in html file:
<button kb-link="1234">Help</button>
Anybody knows what's wrong with that?
You need to use ng-transclude directive for that
class WrapDirective {
restrict = 'E';
transclude = true;
scope = { someLink: '<' };
template: '<a ng-href="url/{{ someLink }}"><ng-transclude></ng-transclude></a>';
}
But you need to wrap your element like <wrap-directive some-link="$ctrl.link"><button>...</wrap-directive>
According to the Angular 1.5 documentation components should only control their own View and Data.
Instead of changing properties of objects passed to the component, a component should create an internal copy of the original data and use callbacks to inform the parent component when this copy has changed.
In this plunk I created a small demo illustrating my problem.
interface IStudent {
id: number,
name: string;
}
/* SERVICE: StudentService */
public class StudentsService {
static $inject = ['$q'];
constructor(private $q: ng.IQService) {
}
public getStudents() : ng.IPromise<IStudent[]> {
return this.$q.when([
{ id: 1, name: 'Adam' },
{ id: 2, name: 'Ben' }
}]);
}
}
/* COMPONENT: student */
class StudentsComponent implements ng.IComponent {
public template = `<student-list on-selected="$ctrl.onSelected(student)"></student-list>
<edit-student student="$ctrl.student" ng-show="$ctrl.student" on-changed="$ctrl.copyChanged(copy)"></edit-student>`;
public controller = StudentsController;
}
class StudentsController {
private student: IStudent;
protected onSelected(student: IStudent) {
this.student = student;
}
protected copyChanged(copy: IStudent) {
this.student.name = copy.name;
}
}
/* COMPONENT: student-list */
class StudentListComponent implements ng.IComponent {
public template = '<ul><li ng-repeat="student in $ctrl.students"><a ng-click="$ctrl.onClick(student)">{{ student.name }}</a></li></ul>';
public controller = StudentListController;
public bindings : any = {
onSelected: '&'
}
}
class StudentListController {
protected students: IStudent[];
static $inject = ['studentsService'];
constructor(private studentsService: StudentsService) {
}
public $onInit() {
this.studentsService.getStudents().then(data => this.students = data);
}
protected onClick(student: IStudent) {
this.onSelected({ student: student });
}
}
/* COMPONENT: edit-student */
class EditStudentComponent implements ng.IComponent {
public template = `<form class="form">
<div class="input-group">
<label for="#" class="control-label">Original</label>
<input type="text" class="form-control" ng-model="$ctrl.student.name" readonly>
</div>
</form>
<form class="form">
<div class="input-group">
<label for="#" class="control-label">Copy</label>
<input ng-change="$ctrl.changed()" type="text" class="form-control" ng-model="$ctrl.copy.name">
</div>
</form>`;
public controller = EditStudentController;
public bindings :any = {
student: '<',
onChanged: '&'
};
}
class EditStudentController {
protected copy: IStudent;
public $onInit() {
console.log('EditStudentComponent.$onInit', this.student);
}
public $onChange() {
console.log('EditStudentComponent.$onChange', this.student);
this.copy = angular.copy(this.student);
}
protected changed() {
console.log('EditStudentController.changed', this.copy);
this.onChanged({ copy: this.copy });
}
}
/* Bootstrap */
angular
.module('app', [])
.component('students', new StudentsComponent())
.component('studentList', new StudentListComponent())
.component('editStudent', new EditStudentComponent())
.service('studentsService', StudentsService)
;
angular.bootstrap(document, ['app']);
I have a list iterating over students. When the user selects a student, a textbox is shown in which the user can change the name of the student. Whenever the name changes, this change is propagated to the parent component which updates the list.
The problem is that after selecting a student in the list, the edit-user component is not initialized and still shows the name of the copy created when the component was created (null).
Can someone tell me how to fix this plunk such that, when clicking a student in the list, the edit component gets initialized with a copy of the selected student?
Edit: changed the plunk, as I accidentally removed the script tag instead of the style tag.
I thought this plunk represented my problem, but alas it didn't. The plunk didn't work because I implemented $onChange instead of $onChanges. I fixed the plunk such that it works as expected.
The cause of my original problem was a completely different one. In my business application I used another component with a ng-transclude directive around my edit component, like this:
<modal-editor>
<edit-student data="$ctrl.data">
<edit-student>
</modal-editor>
As the edit-student component was defined in the isolated scope of the modal-editor component, it didn't receive any changes made to the data variable in the outer scope (but somehow it could still access the data from this outer scope).
After modifying the modal-editor component such that it passed the data to the child component, everything worked as expected:
<modal-editor data="$ctrl.data">
<edit-student data="$ctrl.data">
<edit-student>
</modal-editor>
I want to create an AngularJs directive to authorize based on some claims.
Basically I want an attribute in any html element (button, div, a, etc.) where I specified what groups can view the item and depending on whether the current user has these groups, the element will have an ng-show set to true or false.
This is my directive in typescript. I am injecting a service that takes care of checking whether the current user has or not any of the claims:
//
// Directive to hide or show html element based on group claims for the current user
//
// Usage: <button authorize="mycontroller.authorizedGroupsArray" />
//
namespace app.blocks.directives {
"use strict";
class AuthorizeDirective implements ng.IDirective {
public restrict: string = "A";
public replace: boolean = true;
public scope: any = {
authorizedGroups: "=authorize"
};
constructor(private $compile: ng.ICompileService, private authService: services.IAuthService) {
console.log("authorize directive init");
}
public static factory(): ng.IDirectiveFactory {
const directive = ($compile: ng.ICompileService, authService: services.IAuthService) =>
new AuthorizeDirective($compile, authService);
directive.$inject = [
"$compile",
"app.services.AuthService"
];
return directive;
}
public link(
scope: ng.IScope,
instanceElement: ng.IAugmentedJQuery,
instanceAttributes: ng.IAttributes): void {
let el = angular.element(instanceElement);
let groups: Array<string> = (<any>scope).authorizedGroups || [];
let authorized: boolean = this.authService.isUserAuthorized(groups); // returns true if the user has any of the groups among its claims
el.attr("ng-show", String(authorized));
//remove the attribute, otherwise it creates an infinite loop.
el.removeAttr("authorize");
this.$compile(el)(scope);
}
}
angular
.module("app.blocks.directives")
.directive("authorize", AuthorizeDirective.factory());
}
And I am using this directive like this assuming that for example in myCtrl I have a property like:
public get groupsAuthorized(): Array<string> {
return [
"accounting"
"administrators"
];
}
So in my view:
<button type="button" class="btn btn-default" authorize="myCtrl.groupsAuthorized">TEST</button>
The problem with this code is that works but it throws an error
app.globalexceptionhandler.config.ts:28 Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!(…)(anonymous function) # app.globalexceptionhandler.config.ts:28$apply # angular.js:17792done # angular.js:11831completeRequest # angular.js:12033requestLoaded # angular.js:11966
angular.js:68 Uncaught Error: [$sce:itype] Attempted to trust a non-string value in a content requiring a string: Context: html
The directive code is executed only once, and I am removing the attribute authorize after execution to ensure the directive is not executed in a loop. So I am not too sure what's going on.
If I remove the isolated scope (comment out the following):
//public scope: any = {
// authorizedGroups: "=authorize"
//};
and I try to get the argument passed in the authorize attribute like this in my directive:
let groups: any = instanceAttributes.authorize;
then the value of this groups variable is not the array itself but the string literal "myCtrl.groupsAuthorized" which is not valid because I want an array of groups, not the name of the array.
Why the isolated scope is causing the digest error? How to resolve the issue and get the array of groups in my directive?
UPDATE WORKAROUND:
I have a workaround in which basically I pass to the directive a string instead an array.
I would have the controller return a string with the groups comma separated:
public get groupsAuthorized(): string {
let groups: Array<string> = [
"accounting",
"administrators"
];
return groups.join(",");
}
and then I would use the directive in a similar way only this time passing that "magic" string rendered first in the view.
<button type="button" class="btn btn-default" authorize="{{myCtrl.groupsAuthorized}}">TEST</button>
and finally in my directive I would convert that comma separated string into an array like this:
public link(
scope: ng.IScope,
instanceElement: ng.IAugmentedJQuery,
instanceAttributes: ng.IAttributes): void {
let groupsCommaSeparated: string = (<any>instanceAttributes).authorize;
let groups: Array<string> = groupsCommaSeparated.split(",");
let element = angular.element(instanceElement);
let authorized: boolean = this.authService.isUserAuthorized(groups);
element.attr("ng-show", String(authorized));
//remove the attribute, otherwise it creates an infinite loop.
element.removeAttr("authorize");
this.$compile(element)(scope);
}
but this is not an elegant solution and it could cause problems if the format of the comma separated groups is not correct or if it has spaces after each comma! and I still would like to have my directive argument passing directly an array.
I'm working on using a kendo inside of an angular 2 project.
Getting the widget set up correctly is no problem:
ngOnInit() {
let options = inputsToOptionObject(KendoUIScheduler, this);
options.dataBound = this.bound;
this.scheduler = $(this.element.nativeElement)
.kendoScheduler(options)
.data('kendoScheduler');
}
When that runs, the plugin modifies the DOM (and, to my knowleged, without modifiying the shadow DOM maintained by angular2). My issue is that if I want to use a component anywhere inside of the plugin, like in a template, Angular is unaware of it's existence and won't bind it.
Example:
public views:kendo.ui.SchedulerView[] = [{
type: 'month',
title: 'test',
dayTemplate: (x:any) => {
let date = x.date.getDate();
let count = this.data[date];
return `<monthly-scheduler-day [date]="test" [count]=${count}"></monthly-scheduler-day>`
}
}];
The monthly-scheduler-day class:
#Component({
selector: 'monthly-scheduler-day',
template: `
<div>{{date}}</div>
<div class="badge" (click)=dayClick($event)>Available</div>
`
})
export class MonthlySchedulerDayComponent implements OnInit{
#Input() date: number;
#Input() count: number;
constructor() {
console.log('constructed');
}
ngOnInit(){
console.log('created');
}
dayClick(event){
console.log('clicked a day');
}
}
Is there a "right" way to bind these components inside of the markup created by the widget? I've managed to do it by listening for the bind event from the widget and then looping over the elements it created and using the DynamicComponentLoader, but it feels wrong.
I found some of the details I needed in this thread: https://github.com/angular/angular/issues/6223
I whipped this service up to handle binding my components:
import { Injectable, ComponentMetadata, ViewContainerRef, ComponentResolver, ComponentRef, Injector } from '#angular/core';
declare var $:JQueryStatic;
#Injectable()
export class JQueryBinder {
constructor(
private resolver: ComponentResolver,
private injector: Injector
){}
public bindAll(
componentType: any,
contextParser:(html:string)=>{},
componentInitializer:(c: ComponentRef<any>, context: {})=>void):
void
{
let selector = Reflect.getMetadata('annotations', componentType).find((a:any) => {
return a instanceof ComponentMetadata
}).selector;
this.resolver.resolveComponent(componentType).then((factory)=> {
$(selector).each((i,e) => {
let context = contextParser($(e).html());
let c = factory.create(this.injector, null, e);
componentInitializer(c, context);
c.changeDetectorRef.detectChanges();
c.onDestroy(()=>{
c.changeDetectorRef.detach();
})
});
});
}
}
Params:
componentType: The component class you want to bind. It uses reflection to pull the selector it needs
contextParser: callback that takes the existing child html and constructs a context object (anything you need to initialize the component state)
componentInitializer - callback that initializes the created component with the context you parsed
Example usage:
let parser = (html: string) => {
return {
date: parseInt(html)
};
};
let initer = (c: ComponentRef<GridCellComponent>, context: { date: number })=>{
let d = context.date;
c.instance.count = this.data[d];
c.instance.date = d;
}
this.binder.bindAll(GridCellComponent, parser, initer );
Well your solution works fine until the component needs to change its state and rerender some stuff.
Because I haven't found yet any ability to get ViewContainerRef for an element generated outside of Angular (jquery, vanilla js or even server-side)
the first idea was to call detectChanges() by setting up an interval. And after several iterations finally I came to a solution which works for me.
So far in 2017 you have to replace ComponentResolver with ComponentResolverFactory and do almost the same things:
let componentFactory = this.factoryResolver.resolveComponentFactory(componentType),
componentRef = componentFactory.create(this.injector, null, selectorOrNode);
componentRef.changeDetectorRef.detectChanges();
After that you can emulate attaching component instance to the change detection cycle by subscribing to EventEmitters of its NgZone:
let enumerateProperties = obj => Object.keys(obj).map(key => obj[key]),
properties = enumerateProperties(injector.get(NgZone))
.filter(p => p instanceof EventEmitter);
let subscriptions = Observable.merge(...properties)
.subscribe(_ => changeDetectorRef.detectChanges());
Of course don't forget to unsubscribe on destroy:
componentRef.onDestroy(_ => {
subscriptions.forEach(x => x.unsubscribe());
componentRef.changeDetectorRef.detach();
});
UPD after stackoverflowing once more
Forget all the words above. It works but just follow this answer