Angular 2: Function as an #Input-Property - javascript

You can see my Plunk here.
In this very easy example I am passing a function
_ => {
console.log(number);
}
to a child component with the #Input property. My parent component looks like that:
#Component({
selector: 'my-app',
template: `
<child [func]="getFunction(3)">
</child>
<button type="button" (click)="startChangeDetection()">
Start change detection
</button>
`,
directives : [Child],
styles:[`
.titles {
color:#0099FF
}
.child-style {
background-color:#00ffff
}
` ],
})
export class CarComponent {
startChangeDetection()
{
}
getFunction(number)
{
return _ => {
console.log(number);
}
}
}
The button does nothing else than trigger another round of change detection (there is no implementation in the callback function.
However, my change detection always recognizes my input as a change, nevertheless it never changes.
This is my child component:
#Component({
selector: 'child',
template: `
<h2>Child Component</h2>
`,
inputs: ['func']
})
export class Child {
private func;
ngOnChanges(changes)
{
console.log(changes.func.previousValue.toString());
console.log(changes.func.currentValue.toString());
}
}
You can see, that in ngOnChanges I log my function to the console. But the logged value (obviously) does never change, so the output is always:
function (_) {
console.log(number);
}
function (_) {
console.log(number);
}
Why the heck does Angular even call ngOnChanges? And why does it think there is any change?

This method returns a different function instance every time getFunction is called.
getFunction(number)
{
return _ => {
console.log(number);
}
}
because of <child [func]="getFunction(3)">, getFunction is called every time change detection is run.
Binding to functions is usually not the best idea. If you move out the function creation this way, the same function instance is returned every time and Angular change detection won't recognize it as change:
myCallback = _ => this.myCallBack(number) {
console.log(number);
}
getFunction(number)
{
return this.myCallback;
}

I don't see anything abnormal there. The change detection is getting called twice.
First time because child component is rendered inside car component. The component tree is changed.
Second time because the function getFunction is getting called when you are passing it as getFunction(3) to the input. Thats equivalent to change in input value and hence triggering change detection cycle.

Related

How to properly recalculate a boolean whenever the input observable changes?

I have a component to which I pass an object as its input.
That object comes from an API, so it is not available right away. And the object returned from the API can also be empty.
In the template, I want to hide the value unless it is not an empty object.
So I have this in my component:
import { Component, Input, OnChanges, SimpleChanges } from "#angular/core";
import { BehaviorSubject, Observable, of } from "rxjs";
import { tap } from "rxjs/operators";
import { not, isEmpty } from "ramda";
#Component({
selector: "hello",
template: `
{{ hasName | async }} {{ name | async }}
<h1 *ngIf="(hasName | async)">Hello {{ name | async }}!</h1>
`,
styles: [
`
h1 {
font-family: Lato;
}
`
]
})
export class HelloComponent {
#Input() name: Observable<{} | { first: string }> = of({});
hasName: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
ngOnInit() {
this.name.pipe(
tap(name => console.log(name)),
// the actual logic to calculate the boolean is quite involved, but I
// have simplified it here for brevity.
tap(name => this.hasName.next(not(isEmpty(name))))
);
}
}
The problem is that the console.log in the tap is never printed and the second tap is never run either, so the value for hasName is always false as set at the of the class.
I think what happens is that in OnInit, this.name is still of({}), and not the actual observable passed in through the template from the parent.
I am using of({}) because typing this.name as Observable<{} | { first: string}> | undefined leads to more boilerplate in the class to check it is indeed defined before using it.
How can I make this work in an idiomatic way?
Complete Stackblitz: https://stackblitz.com/edit/angular-ivy-gxqh2d?devtoolsheight=33&file=src/app/hello.component.ts
You can simplify this code a lot.
First off all, the TS definition.
{} | { first: string }
Can be done as
{ first?: string }
While it may look like I'm doing some neat picking here I'm not really. With the first option if you try to access variable.first you'll get an error saying it doesn't exist as the union cannot guarantee this. With the latter it's fine.
Then, the mock. When working with observables, try not to reassign observable. Here's how you can do what you where doing without reassigning:
public name$: Observable<{ first?: string }> = concat(
of({}),
interval(2000).pipe(
map(() => ({
first: `${Math.random()}`
}))
)
);
Finally, the child component. Passing observables as input is a code smell in 99% of the cases. Here all you want is a presentational component (also called dumb component).
This dumb component should not have any logic (as much as possible at least), and here's what you can do:
type Nil = null | undefined
#Component({
selector: "hello",
template: `
<h1 *ngIf="name?.first">Hello {{ name.first }}!</h1>
`
})
export class HelloComponent {
#Input() name: { first?: string } | Nil;
}
Which means that when you call this from the parent, you just do:
<hello [name]="name$ | async"></hello>
Here's an updated stackblitz: https://stackblitz.com/edit/angular-ivy-hnv23a?file=src%2Fapp%2Fapp.component.ts
If you want to react when the input value changes you can write your own setter:
export class HelloComponent {
#Input() set name(name: Observable<{} | { first: string }>) {
this.name$ = name.pipe(
tap(name => console.log(name)),
tap(name => this.hasName.next(not(isEmpty(name))))
);
}
name$: Observable<{} | { first: string }>;
...
}
Then in templates bind async to name$ instead.
Updated demo: https://stackblitz.com/edit/angular-ivy-7u7xkd?file=src%2Fapp%2Fhello.component.ts
I'd suggest to move your logic to app.component.ts instead of passing the whole observable into an input. This could help you to manage the value of that input and prevent assigning a value to it two times (you have assigned an empty object two times in your code - parent & child component).
Simplest thing to make your code work is just to subscribe to the observable you've created inside your ngOnInit:
this.name.pipe(
tap(name => console.log(name)),
tap(name => this.hasName.next(not(isEmpty(name))))
).subscribe();
In case you don't want to manage that (don't want to be worried about non destroyed observables) and you prefer to use the async pipe, you can just reassign this.name inside ngOnInit.
this.name = this.name.pipe(
tap(name => console.log(name)),
tap(name => this.hasName.next(not(isEmpty(name))))
);

How to replace this.$parent.$emit in Vue 3?

I have migrated my application to Vue 3.
Now my linter shows a deprecation error, documented here: https://eslint.vuejs.org/rules/no-deprecated-events-api.html.
The documentation shows how to replace this.$emit with the mitt library, but it doesn't show how to replace this.$parent.$emit.
In your child component:
setup(props, { emit }) {
...
emit('yourEvent', yourDataIfYouHaveAny);
}
Your parent component:
<your-child #yourEvent="onYourEvent" />
...
onYourEvent(yourDataIfYouHaveAny) {
...
}
With script setup syntax, you can do:
<script setup>
const emit = defineEmits(['close', 'test'])
const handleClose = () => {
emit('close')
emit('test', { anything: 'yes' })
}
</script>
No need to import anything from 'vue'. defineEmits is included.
Read more here: https://learnvue.co/2020/01/4-vue3-composition-api-tips-you-should-know/
Due to the composition api, it allows you to use the $attrs inherited in each component to now fulfill this need.
I assume that you are using this.$parent.emit because you know the the child will always be part of the same parent. How do I simulate the above behavior with $attrs?
Lets say I have a table containing row components. However I wish to respond to row clicks in table's parent.
Table Definition
<template>
<row v-bind="$attrs" ></row>
</template>
Row Definition
<template name="row" :item="row" #click=onClick(row)>
Your Row
</template>
export default {
emits: {
row_clicked: () =>{
return true
}
},
onClick(rowData){
this.$emit('row_clicked',rowData)
}
}
Finally, a component containing your table definition, where you have a method to handle the click.
<table
#row_clicked=clicked()
>
</table
Your table component should effectively apply #row_clicked to the row component thus triggering when row emits the event.
There is similar way of doing it by using the context argument that is passed in second argument inside the child component (the one that will emit the event)
setup(props, context){
context.emit('myEventName')
}
...then emit it by calling the context.emit method within the setup method.
In your parent component you can listen to it using the handler like so:
<MyParentComponent #myEventName="handleMyEventName" />
Of course, in the setup method of the MyParentComponent component you can declare the handler like this
//within <script> tag of MyParentComponent
setup(props){
const handleMyEventName() => {
...
}
return { handleMyEventName }
}

Angular 2 runOutsideAngular still change the UI

From my understanding of runOutsideAngular(), if I need to run something that won't trigger the Angular change detection, I need to use this function. My code is not working, however; when I click the button, the UI is changing and the number is 2.
#Component({selector: 'my-cmp',
template: `<h1>{{num}}</h1>
<button (click)="onClick()">Change number</button>`})
class MyComponent implements OnChanges {
num = 1;
constructor(private _ngZone: NgZone ) {
}
onClick() {
this._ngZone.runOutsideAngular(() => {
this.num = 2;
}}));
}
}
If anything is causing change detection, and a bound event like (click)="onClick()" does cause change detection, then Angular will detect the change.
runOutsideAngular doesn't mean Angular won't see the change, it only means that the code run this way doesn't cause change detection, but because the click event already does, it's meaningless in your example.
[In short] you need to change one line in your current code
onClick() {
this._ngZone.runOutsideAngular(() => {
setTimeout(()=>this.num = 2,0); // instead of this.num = 2;
}}));
}
now if you click the on the <button>, this.num will become 2, but you won't see any change in the UI (a temporary inconsistency between view and model)
[Explanation] without runOutsideAngular(), async functions like addEventListener() or setTimeout() behaves differently (monkey patched). their callbacks will try to update UI with Angular after running user's code.
For example, you can treat (click)="onClick()" as:
addEventListener("click",function modifiedCallback(){
onClick();
updateUIifModelChanges(); //call to Angular
})
In order to not triggered UI update we need to satisfy the following two conditions:
not modify model in function onClick (so, modify inside setTimeout())
when the model is indeed modified, do not invoke updateUIifModelChanges (call setTimeout() inside runOutsideAngular)
[More] of cause, the explanation I gave is a very very...simplified version of what happens. setTimeout() has the same function signature whether it's running inside runOutsideAngular() or not. The reason that it behaves differently is because it's running in a different Zone
If you want to prevent change detection then you can
1) subscribe on ngZone.onMicrotaskEmpty like this:
import { NgZone, ChangeDetectorRef } from '#angular/core';
import 'rxjs/add/operator/first';
...
export class MyComponent {
constructor(private ngZone: NgZone, private cdRef: ChangeDetectorRef) {}
onClick() {
// to do something
this.cdRef.detach();
this.ngZone.onMicrotaskEmpty.first().subscribe(() => {
// reattach changeDetector after application.tick()
this.cdRef.reattach();
});
}
}
This handler will run after Application.tick
See also Plunker Example
2) use custom directive like this:
#Directive({
selector: '[outSideEventHandler]'
})
class OutSideEventHandlerDirective {
private handler: Function;
#Input() event: string = 'click'; // pass desired event
#Output('outSideEventHandler') emitter = new EventEmitter();
constructor(private ngZone: NgZone, private elRef: ElementRef) {}
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
this.handler = $event => this.emitter.emit($event);
this.elRef.nativeElement.addEventListener(this.event, this.handler);
});
}
ngOnDestory() {
this.elRef.nativeElement.removeEventListener(this.event, this.handler);
}
}
and then in template you can write:
<button (outSideEventHandler)="onClick()">Click outside zone</button>
or
<button event="mousedown" (outSideEventHandler)="onClick()">Click outside zone</button>
Plunker
3) write custom DOM event handler as described in this article.
https://medium.com/#TheLarkInn/creating-custom-dom-events-in-angular2-f326d348dc8b#.bx4uggfdy
Other solutions see here:
Angular 2 how to keep event from triggering digest loop/detection cycle?
Using ngZone.run is a bit better than the setTimeout solutions since it uses angular specific functionality. Run is meant to be used within ngZone.runOutsideAngular functions.
From the docs:
Running functions via run allows you to reenter Angular zone from a
task that was executed outside of the Angular zone (typically started
via {#link #runOutsideAngular}).
This is actually a very practical example of say a button that increments a number by one but only triggers change detection when the number is even.
#Component({selector: 'my-cmp',
template: `<h1>{{num}}</h1>
<button (click)="onClick()">Change number</button>`})
class MyComponent implements OnChanges {
num = 1;
constructor(private _ngZone: NgZone ) {
}
onClick() {
this._ngZone.runOutsideAngular(() => {
if(this.num % 2 === 0){
// modifying the state here wont trigger change.
this.num++;
} else{
this._ngZone.run(() => {
this.num++;
})
}
}}));
}
}
...
constructor(
private ngZone: NgZone
){
ngZone.runOutsideAngular(() => {
setInterval(()=>{
this.num= new Date().Format('yyyy-MM-dd HH:mm:ss');
},1000);
});
}
...
This is how I have tried to check the difference insideAngular and OutsideAngular
constructor(private zone: NgZone) { }
setProgressOutsideAngular() {
this.zone.runOutsideAngular(() => {
setInterval(() => { ++this.progress, console.log(this.progress) }, 500)
})
}
setProgressInsideAngular() {
this.zone.run(() => setInterval(() => { ++this.progress, console.log(this.progress) }, 500))
}

how can I listen to changes in code in angular 2?

I'm using angular 2. I have a component with an input.
I want to be able to write some code when the input value changes.
The binding is working, and if the data is changed (from outside the component) I can see that there is change in the dom.
#Component({
selector: 'test'
})
#View({
template: `
<div>data.somevalue={{data.somevalue}}</div>`
})
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
dataChagedListener(param) {
// listen to changes of _data object and do something...
}
}
You could use the lifecycle hook ngOnChanges:
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
ngOnChanges([propName: string]: SimpleChange) {
// listen to changes of _data object and do something...
}
}
This hook is triggered when:
if any bindings have changed
See these links for more details:
https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html
As mentioned in the comments of Thierry Templier's answer, ngOnChanges lifecycle hook can only detect changes to primitives. I found that by using ngDoCheck instead, you are able to check the state of the object manually to determine if the object's members have changed:
A full Plunker can be found here. But here's the important part:
import { Component, Input } from '#angular/core';
#Component({
selector: 'listener',
template: `
<div style="background-color:#f2f2f2">
<h3>Listener</h3>
<p>{{primitive}}</p>
<p>{{objectOne.foo}}</p>
<p>{{objectTwo.foo.bar}}</p>
<ul>
<li *ngFor="let item of log">{{item}}</li>
</ul>
</div>
`
})
export class ListenerComponent {
#Input() protected primitive;
#Input() protected objectOne;
#Input() protected objectTwo;
protected currentPrimitive;
protected currentObjectOne;
protected currentObjectTwo;
protected log = ['Started'];
ngOnInit() {
this.getCurrentObjectState();
}
getCurrentObjectState() {
this.currentPrimitive = this.primitive;
this.currentObjectOne = _.clone(this.objectOne);
this.currentObjectTwoJSON = JSON.stringify(this.objectTwo);
}
ngOnChanges() {
this.log.push('OnChages Fired.')
}
ngDoCheck() {
this.log.push('DoCheck Fired.');
if (!_.isEqual(this.currentPrimitive, this.primitive)){
this.log.push('A change in Primitive\'s state has occurred:');
this.log.push('Primitive\'s new value:' + this.primitive);
}
if(!_.isEqual(this.currentObjectOne, this.objectOne)){
this.log.push('A change in objectOne\'s state has occurred:');
this.log.push('objectOne.foo\'s new value:' + this.objectOne.foo);
}
if(this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)){
this.log.push('A change in objectTwo\'s state has occurred:');
this.log.push('objectTwo.foo.bar\'s new value:' + this.objectTwo.foo.bar);
}
if(!_.isEqual(this.currentPrimitive, this.primitive) || !_.isEqual(this.currentObjectOne, this.objectOne) || this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)) {
this.getCurrentObjectState();
}
}
It should be noted that the Angular documentation provides this caution about using ngDoCheck:
While the ngDoCheck hook can detect when the hero's name has changed,
it has a frightful cost. This hook is called with enormous frequency —
after every change detection cycle no matter where the change
occurred. It's called over twenty times in this example before the
user can do anything.
Most of these initial checks are triggered by Angular's first
rendering of unrelated data elsewhere on the page. Mere mousing into
another input box triggers a call. Relatively few calls reveal actual
changes to pertinent data. Clearly our implementation must be very
lightweight or the user experience will suffer.

Angular 2 trigger event

I am trying to trigger a click event on another element but it doesn't work.
My Component:
import {Component, Output, EventEmitter} from 'angular2/core';
#Component({
selector: 'generate-car',
template: `
<button [disabled]="!pressed" (click)="parkingCar()" type="button" class="parking">Parking Car</button>
<car *ngFor="#car of cars" [class]="car.type" (animate)="test()">
<span class="car-number">{{car.id}}</span>
</car>
`
})
When I click on the button I need to trigger (animate) event.
My class:
export class GenerateCar {
#Output() animate = new EventEmitter();
parkingCar() {
this.animate.emit(null)
}
test() {
console.log( 111 )
}
}
I think set up is not correct or something.
You need to understand the concept of EventEmitter of Angular2. It allows you to fire custom event of DOM element and propagate it to its parent.
I don't understand what you would like to do with EventEmitter with your example. But I'd like to give you a direction which might help you in your example.
EventEmitter very simple example
boot.ts
#Component({
selector: 'my-app',
directives:[Car],
template: `
<div>{{name}} from parent</div><br>
<car (animate)="test($event)"></car><bR>
`
,
})
export class BodyContent { //ParentComponent
name:string='Angular1';
test(arg) {
console.log('test start');
//this.animate.subscribe((value) => { this.name = value; });
this.name=arg;
console.log(arg);
}
}
,
})
car.ts
export class Car {
#Output() animate: EventEmitter = new EventEmitter();
setValue()
{
console.log('setValue strat');
this.animate.next('angular2');
// You can also write below line in place of above line
//this.animate.emit('Angular2');
}
}
Working Plunker
Note:
(animate) = "test($event): It will tell Angular to invoke the ParentComponent's test($event) method when ChildComponent(car) fires animate. The data that we passed to the “next()” method in caris available in ParentComponent by passing $event as an argument to the test() method.
For further information you can refer to this nice article.
Hope this will help you to go further.

Categories

Resources