Call method after view (DOM) is updated (in Angular 2) - javascript

Assume that I have the following directive:
import {Directive, Input, OnChanges } from '#angular/core'
#Directive({
selector: 'select[my-select]'
})
export class NbSelect implements OnChanges {
#Input() ngModel;
constructor() {
}
ngOnChanges(changes) {
doSomeStuffAfterViewIsUpdated();
}
doSomeStuffAfterViewIsUpdated() {
console.log(jQuery('select[my-select]').val()); // this should write the new value
}
}
I'm using it in somewhere in a template: ...<select my-select [(ngModel)]="someReference">...
At some point I change the value of someReference in a component: someReference = "newValue"
What is happening now: The doSomeStuffAfterViewIsUpdated function is called before the DOM is updated. Which means the console shows the old value of the select.
What I want: The doSomeStuffAfterViewIsUpdated function should be called after the DOM is updated by angular, so the console shows the "newValue".
What should I use instead of ngOnChanges to make it work like I want?
Please note that the important thing here is that the code should run after the DOM update. The question is not about how can I access the new value of the element.
Thx!

I'm a bit late to the party, but since I had a similar problem and a probable solution, I thought I'd share:
What you're looking for is MutationObservers (CanIUse);
I solved a very similar issue with a directive similar to this (simplified for... simplicity? :P):
ngOnChanges() {
this.observer = new MutationObserver(() => { this.methodToCall() });
this.observer.observe(this.elRef.nativeElement, this.observerOptions); // See linked MDN doc for options
}
private methodToCall(): void {
this.observer.disconnect();
}
ngOnDestroy() {
if(this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
}
The MutationObserver callback will be triggered when either an attribute, children, etc is modified in the DOM (or added, deleted, etc)
It is possible you won't need the ngOnChanges at all depending on your use case and it would make the code a bit cleaner too.

Related

RxJS: Mimic Angulars #Input() binding?

Im pretty new to Angular 8 and RxJS and stumbled upon an issue:
I make an angular app which relies heavily on an externally loaded THREE.js scene. Services handle those scenes.
So most of the time there is No HTML Template (only maintaining scene via js object) => no bindings ?
So I was thinking... is there a way to use Rxjs subjects/observables to achieve something like Input() binding?
thats what i basically want to
this.sub = myService.watch('window.deviceorientation')
.subscribe({next(x => { if(x) this.sub.unsubscribe; doStuff();})})
I want to continously check a certain object (any really), be notified as soon as it exists (or instantly if it already does). I bet there is some weird combination of RxJS Operators which can do exactly this?
(So basically its a little bit like AngularJS scope.$watch but to keep performance I'd of course clean up subscriptions.)
You can use the fromEvent rxjs creation utility to achieve the desired effect.
import { fromEvent } from 'rxjs';
orientation$ = fromEvent(window, 'deviceorientation');
const subscription = orientation$.subscribe((event)=>{
//do stuff
});
// when disponse is required
subscription.unsubscribe();
So I worked my way around this.
I created a root-level component that allows me to watch for specific changes on scope:
//HTML
<app-change-detector [watch]="window.anyObject" (onChange)="anyRootService.onObjectChanged($event)">
//TS
#Component({
selector: 'app-change-detector'
})
export class ChangeDetectorComponent implements OnChanges {
#Input() watch: any;
#Output() onChange = new EventEmitter<any>();
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes.watch) {
this.onChange.emit(changes.watch.currentValue);
}
}
}

Prevent ngOnChanges from firing after emitting event (Angular 2+)

In Angular 2+, custom two-way databinding can be accomplished by using #Input and #Output parameters. So if I want a child component to communicate with a third party plugin, I could do it as follows:
export class TestComponent implements OnInit, OnChanges {
#Input() value: number;
#Output() valueChange = new EventEmitter<number>();
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
// Update the third party plugin with the new value from the parent component
thirdPartyPlugin.setValue(this.value);
}
}
And use it like this:
<test-component [(value)]="value"></test-component>
After the third party plugin fires an event to notify us of a change, the child component updates the parent component by calling this.valueChange.emit(newValue). The issue is that ngOnChanges then fires in the child component because the parent component's value has changed, which causes thirdPartyPlugin.setValue(this.value) to be called. But the plugin is already in the correct state, so this is a potentially unnecessary/expensive re-render.
So what I often do is create a flag property in my child component:
export class TestComponent implements OnInit, OnChanges {
ignoreModelChange = false;
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
// Set ignoreModelChange to true if ngChanges will fire, so that we avoid an
// unnecessary (and potentially expensive) re-render.
if (this.value === newValue) {
return;
}
ignoreModelChange = true;
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
if (ignoreModelChange) {
ignoreModelChange = false;
return;
}
thirdPartyPlugin.setValue(this.value);
}
}
But this feels like a hack.
In Angular 1, directives which took in a parameter using the = binding had the same exact issue. So instead, I would accomplish custom two-way databinding by requiring ngModelController, which did not cause a re-render after a model update:
// Update the parent model with the new value from the third party plugin. After the model
// is updated, $render will not fire, so we don't have to worry about a re-render.
thirdPartyPlugin.onSomeEvent(function (newValue) {
scope.$apply(function () {
ngModelCtrl.$setViewValue(newValue);
});
});
// Update the third party plugin with the new value from the parent model. This will only
// fire if the parent scope changed the model (not when we call $setViewValue).
ngModelCtrl.$render = function () {
thirdPartyPlugin.setValue(ngModelCtrl.$viewValue);
};
This worked, but ngModelController really seems to be designed for form elements (it has built in validation, etc.). So it felt a bit odd to use it in custom directives which are not form elements.
Question: Is there a best practice in Angular 2+ for implementing custom two-way databinding in a child component, which does not trigger ngOnChanges in the child component after updating the parent component using EventEmitter? Or should I integrate with ngModel just as I did in Angular 1, even if my child component is not a form element?
Thanks in advance!
Update: I checked out Everything you need to know about change detection in Angular suggested by #Maximus in the comments. It looks like the detach method on ChangeDetectorRef will prevent any bindings in the template from being updated, which could help with performance if that's your situation. But it does not prevent ngOnChanges from being called:
thirdPartyPlugin.onSomeEvent(newValue => {
// ngOnChanges will still fire after calling emit
this.changeDetectorRef.detach();
this.valueChange.emit(newValue);
});
So far I haven't found a way to accomplish this using Angular's change detection (but I learned a lot in the process!).
I ended up trying this with ngModel and ControlValueAccessor. This seems to accomplish what I need since it behaves as ngModelController in Angular 1:
export class TestComponentUsingNgModel implements ControlValueAccessor, OnInit {
value: number;
// Angular will pass us this function to invoke when we change the model
onChange = (fn: any) => { };
ngOnInit() {
thirdPartyPlugin.onSomeEvent(newValue => {
this.value = newValue;
// Tell Angular to update the parent component with the new value from the third
// party plugin
this.onChange(newValue);
});
}
// Update the third party plugin with the new value from the parent component. This
// will only fire if the parent component changed the model (not when we call
// this.onChange).
writeValue(newValue: number) {
this.value = newValue;
thirdPartyPlugin.setValue(this.value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
}
And use it like this:
<test-component-using-ng-model [(ngModel)]="value"></test-component-using-ng-model>
But again, if the custom component is not a form element, using ngModel seems a bit odd.
Also ran into this problem (or at least something very similar).
I ended up using hacky approach you discussed above but with a minor modification, I used setTimeout in order to reset state just in case.
(For me personally ngOnChanges was mainly problematic if using two-way binding, so the setTimeout prevents a hanging disableOnChanges if NOT using two-way binding).
changePage(newPage: number) {
this.page = newPage;
updateOtherUiVars();
this.disableOnChanges = true;
this.pageChange.emit(this.page);
setTimeout(() => this.disableOnChanges = false, 0);
}
ngOnChanges(changes: any) {
if (this.disableOnChanges) {
this.disableOnChanges = false;
return;
}
updateOtherUiVars();
}
This is exactly the intention of Angular and its something you should try to work with rather than against. Change detection works by components detecting changes in its template bindings and propagating them down the component tree. If you can design your application in such a way that you are relying on the immutability of components inputs', you can control this manually by setting #Component({changeDetection:ChangeDetectionStrategy.OnPush}) which will test references to determine whether to continue change detection on children components.
So, that said, my experience is that wrappers of 3rd party plugins may not efficiently handle and take advantage of this type of strategy appropriately. You should attempt to use knowledge of the above, together with good design choices like the separation of concerns of presentation vs container components to leverage the detection strategy to achieve good performance.
You can also pass changes: SimpleChanges to ngOnInit(changes: SimpleChanges) and inspect the object to learn more about your data flow.

Angular 2: How to detect changes in an array? (#input property)

I have a parent component that retrieves an array of objects using an ajax request.
This component has two children components: One of them shows the objects in a tree structure and the other one renders its content in a table format. The parent passes the array to their children through an #input property and they display the content properly. Everything as expected.
The problem occurs when you change some field within the objects: the child components are not notified of those changes. Changes are only triggered if you manually reassign the array to its variable.
I'm used to working with Knockout JS and I need to get an effect similar to that of observableArrays.
I've read something about DoCheck but I'm not sure how it works.
OnChanges Lifecycle Hook will trigger only when input property's instance changes.
If you want to check whether an element inside the input array has been added, moved or removed, you can use IterableDiffers inside the DoCheck Lifecycle Hook as follows:
constructor(private iterableDiffers: IterableDiffers) {
this.iterableDiffer = iterableDiffers.find([]).create(null);
}
ngDoCheck() {
let changes = this.iterableDiffer.diff(this.inputArray);
if (changes) {
console.log('Changes detected!');
}
}
If you need to detect changes in objects inside an array, you will need to iterate through all elements, and apply KeyValueDiffers for each element. (You can do this in parallel with previous check).
Visit this post for more information: Detect changes in objects inside array in Angular2
You can always create a new reference to the array by merging it with an empty array:
this.yourArray = [{...}, {...}, {...}];
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [].concat(this.yourArray);
The code above will change the array reference and it will trigger the OnChanges mechanism in children components.
Read following article, don't miss mutable vs immutable objects.
Key issue is that you mutate array elements, while array reference stays the same. And Angular2 change detection checks only array reference to detect changes. After you understand concept of immutable objects you would understand why you have an issue and how to solve it.
I use redux store in one of my projects to avoid this kind of issues.
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
You can use IterableDiffers
It's used by *ngFor
constructor(private _differs: IterableDiffers) {}
ngOnChanges(changes: SimpleChanges): void {
if (!this._differ && value) {
this._differ = this._differs.find(value).create(this.ngForTrackBy);
}
}
ngDoCheck(): void {
if (this._differ) {
const changes = this._differ.diff(this.ngForOf);
if (changes) this._applyChanges(changes);
}
}
It's work for me:
#Component({
selector: 'my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
export class MyComponent implements DoCheck {
#Input() changeArray: MyClassArray[]= [];
private differ: IterableDiffers;
constructor(private differs: IterableDiffers) {
this.differ = differs;
}
ngDoCheck() {
const changes = this.differ.find(this.insertedTasks);
if (changes) {
this.myMethodAfterChange();
}
}
This already appears answered. However for future problem seekers, I wanted to add something missed when I was researching and debugging a change detection problem I had. Now, my issue was a little isolated, and admittedly a stupid mistake on my end, but nonetheless relevant.
When you are updating the values in the Array or Object in reference, ensure that you are in the correct scope. I set myself into a trap by using setInterval(myService.function, 1000), where myService.function() would update the values of a public array, I used outside the service. This never actually updated the array, as the binding was off, and the correct usage should have been setInterval(myService.function.bind(this), 1000). I wasted my time trying change detection hacks, when it was a silly/simple blunder. Eliminate scope as a culprit before trying change detection solutions; it might save you some time.
Instead of triggering change detection via concat method, it might be more elegant to use ES6 destructuring operator:
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [...this.yourArray];
You can use an impure pipe if you are directly using the array in your components template. (This example is for simple arrays that don't need deep checking)
#Pipe({
name: 'arrayChangeDetector',
pure: false
})
export class ArrayChangeDetectorPipe implements PipeTransform {
private differ: IterableDiffer<any>;
constructor(iDiff: IterableDiffers) {
this.differ = iDiff.find([]).create();
}
transform(value: any[]): any[] {
if (this.differ.diff(value)) {
return [...value];
}
return value;
}
}
<cmp [items]="arrayInput | arrayChangeDetector"></cmp>
For those time travelers among us still hitting array problems here is a reproduction of the issue along with several possible solutions.
https://stackblitz.com/edit/array-value-changes-not-detected-ang-8
Solutions include:
NgDoCheck
Using a Pipe
Using Immutable JS NPM github

Why does component view update when change detection is set to onPush? [duplicate]

I thought I was pretty clear on how Angular Change detection works after this discussion: Why is change detection not happening here when [value] changed?
But take a look at this plunk: https://plnkr.co/edit/jb2k7U3TfV7qX2x1fV4X?p=preview
#Component({
selector: 'simple',
template: `
<div (click)="onClick()">
{{myData[0].name}}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Simple {
public #Input() myData;
constructor() {
}
public onClick() {
}
}
Click on a, it's changed to c
I understand that the click event triggers change detection on the App level, but [myData]="testData" is still referring to the same object, and I am using On Push on Simple, why does a get changed?
That's by design.
If you have component with OnPush change detection then its detectChangesInternal function won't be triggered unless one of four things happens:
1) one of its #Inputs changes
~2.4.x
~4.x.x
Note: #Inputs should be presented in template. See issue https://github.com/angular/angular/issues/20611 and comment
2) a bound event is triggered from the component (that is your case)
Caveats: There is some difference here between 2.x.x and 4
Angular ChangeDetectionStrategy.OnPush with child component emitting an event
~2.4.x
~4.x.x
3) you manually mark the component to be checked (ChangeDetectorRef.markForCheck())
4) async pipe calls ChangeDetectorRef.markForCheck() internally
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
https://github.com/angular/angular/blob/2.4.8/modules/%40angular/common/src/pipes/async_pipe.ts#L137
In other words if you set OnPush for component then after the first checking component's status will be changed from CheckOnce to Checked and after that it's waiting as long as we do not change status. It will happen in one of three things above.
See also:
https://github.com/angular/angular/issues/11678#issuecomment-247894782
There are also good explanations of how angular2 change detection work:
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
https://hackernoon.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f
Here is Live Example(Thanks to Paskal) that explains onPush change detection. (Comp16 looks like your component. You can click at this box).

How to expose angular 2 methods publicly?

I am currently working on porting a Backbone project to an Angular 2 project (obviously with a lot of changes), and one of the project requirements requires certain methods to be accessible publicly.
A quick example:
Component
#component({...})
class MyTest {
private text:string = '';
public setText(text:string) {
this.text = text;
}
}
Obviously, I could have <button (click)="setText('hello world')>Click me!</button>, and I would want to do that as well. However, I'd like to be able to access it publicly.
Like this
<button onclick="angular.MyTest.setText('Hello from outside angular!')"></click>
Or this
// in the js console
angular.MyTest.setText('Hello from outside angular!');
Either way, I would like the method to be publicly exposed so it can be called from outside the angular 2 app.
This is something we've done in backbone, but I guess my Google foo isn't strong enough to find a good solution for this using angular.
We would prefer to only expose some methods and have a list of public apis, so if you have tips for doing that as well, it'd be an added bonus. (I have ideas, but others are welcomed.)
Just make the component register itself in a global map and you can access it from there.
Use either the constructor or ngOnInit() or any of the other lifecycle hooks to register the component and ngOnDestroy() to unregister it.
When you call Angular methods from outside Angular, Angular doesn't recognize model change. This is what Angulars NgZone is for.
To get a reference to Angular zone just inject it to the constructor
constructor(zone:NgZone) {
}
You can either make zone itself available in a global object as well or just execute the code inside the component within the zone.
For example
calledFromOutside(newValue:String) {
this.zone.run(() => {
this.value = newValue;
});
}
or use the global zone reference like
zone.run(() => { component.calledFromOutside(newValue); });
https://plnkr.co/edit/6gv2MbT4yzUhVUfv5u1b?p=preview
In the browser console you have to switch from <topframe> to plunkerPreviewTarget.... because Plunker executes the code in an iFrame. Then run
window.angularComponentRef.zone.run(() => {window.angularComponentRef.component.callFromOutside('1');})
or
window.angularComponentRef.zone.run(() => {window.angularComponentRef.componentFn('2');})
This is how i did it. My component is given below. Don't forget to import NgZone. It is the most important part here. It's NgZone that lets angular understand outside external context. Running functions via zone allows you to reenter Angular zone from a task that was executed outside of the Angular zone. We need it here since we are dealing with an outside call that's not in angular zone.
import { Component, Input , NgZone } from '#angular/core';
import { Router } from '#angular/router';
#Component({
selector: 'example',
templateUrl: './example.html',
})
export class ExampleComponent {
public constructor(private zone: NgZone, private router: Router) {
//exposing component to the outside here
//componentFn called from outside and it in return calls callExampleFunction()
window['angularComponentReference'] = {
zone: this.zone,
componentFn: (value) => this.callExampleFunction(value),
component: this,
};
}
public callExampleFunction(value: any): any {
console.log('this works perfect');
}
}
now lets call this from outside.in my case i wanted to reach here through the script tags of my index.html.my index.html is given below.
<script>
//my listener to outside clicks
ipc.on('send-click-to-AT', (evt, entitlement) =>
electronClick(entitlement));;
//function invoked upon the outside click event
function electronClick(entitlement){
//this is the important part.call the exposed function inside angular
//component
window.angularComponentReference.zone.run(() =
{window.angularComponentReference.componentFn(entitlement);});
}
</script>
if you just type the below in developer console and hit enter it will invoke the exposed method and 'this works perfect ' will be printed on console.
window.angularComponentReference.zone.run(() =>
{window.angularComponentReference.componentFn(1);});
entitlement is just some value that is passed here as a parameter.
I was checking the code, and I have faced that the Zone is not probably necessary.
It works well without the NgZone.
In component constructor do this:
constructor(....) {
window['fncIdentifierCompRef'] = {
component = this
};
}
And in the root script try this:
<script>
function theGlobalJavascriptFnc(value) {
try {
if (!window.fncIdentifierCompRef) {
alert('No window.fncIdentifierCompRef');
return;
}
if (!window.fncIdentifierCompRef.component) {
alert('No window.fncIdentifierCompRef.component');
return;
}
window.fncIdentifierCompRef.component.PublicCmpFunc(value);
} catch(ex) {alert('Error on Cmp.PublicCmpFunc Method Call')}
}
</script>
This works to me.
The problem is that Angular's components are transpiled into modules that aren't as easy to access as regular JavaScript code. The process of accessing a module's features depends on the module's format.
An Angular2 class can contain static members that can be defined without instantiating a new object. You might want to change your code to something like:
#component({...})
class MyTest {
private static text: string = '';
public static setText(text:string) {
this.text = text;
}
}
Super simple solution!! save component or function with an alias outside
declare var exposedFunction;
#Component({
templateUrl: 'app.html'
})
export class MyApp {
constructor(public service:MyService){
exposedFunction = service.myFunction;
}
at index.html add in head
<script>
var exposedFunction;
</script>
Inside exposed function do not use this. parameters if you need them you will have to use closures to get it to work
This is particularly useful in ionic to test device notifications on web instead of device

Categories

Resources