Share a method between two child components (Angular) - javascript

There is such structure of components:
Desired Behavior
child1_component - is a header.
child2_component - is a body.
There is a button inside child1_component.
Clicking on that button I want to invoke a method inside child2_component.
Question
What is the best way to implement this?

One way to approach this would be to use a service with rxjs subjects and observables.
When the user clicks on the button in child1_component then it calls a method that in turn calls a method inside the shared service.
When the method in the service is called it can emit a value as an observable via a subject.
child2_component then subscribes to the observable within the shared service and can operate some logic based on when it receives data from the service.
More on services here: https://angular.io/tutorial/toh-pt4
Great tutorial on subjects and rxjs: https://blog.angulartraining.com/rxjs-subjects-a-tutorial-4dcce0e9637f

On your general.component.html :
<app-child1 (clicked)="app1Clicked($event)"></app-child1>
<app-child2 #child2></app-child2>
On your general.component.ts:
#ViewChild('child2', {static: true}) child2: Child2Component;
app1Clicked($event) {
this.child2.doSomething()
}
On the child1.components.ts:
#Output() clicked = new EventEmitter<any>();
onClick() {
this.clicked.emit();
}
Finally on the child2.component.ts:
doSomething() {
alert('ok');
}

There are 2 ways to do it:
1.Service:
export class ActionService {
private someAction = new Subject();
someActionEmitted$(): Observable<unknown> {
return this.someAction.asObservable();
}
emitSomeAction(): void {
this.someAction.next();
}
}
//childComponent1
export class ChildComponent1 {
constructor(private actionService: ActionService) {
}
emitAction(): void {
this.actionService.emitSomeAction();
}
}
//childComponent2
export class ChildComponent2 implements OnInit, OnDestroy {
private destroy$ = new Subject();
constructor(private actionService: ActionService) {
}
ngOnInit(): void {
this.actionService.someActionEmitted$()
.pipe(takeUntil(this.destroy$)) // dont forget to unsubscribe, can cause memory leaks
.subscribe(() => this.doSomething());
}
doSomething(): void {
// your logic here
}
ngOnDestroy(): void {
this.destroy$.next();
}
}
2. Using Parent Component
<child-component1 (btnClicked)="childComponentBtnClick()"></child-component1>
<child-component2 [clickBtnSubject]="childBtnClicked"></child-component1>
Ts logic:
export class ParentComponent {
childBtnClicked = new Subject();
childComponentBtnClick(): void {
this.childBtnClicked.next();
}
}
//childComponent1
export class ChildComponent1 {
#Output() btnClicked = new EventEmitter();
emitAction(): void {
this.btnClicked.emit(); // you can pass value to emit() method
}
}
//childComponent2
export class ChildComponent2 implements OnInit, OnDestroy {
#Input() clickBtnSubject: Subject;
ngOnInit(): void {
this.clickBtnSubject
.pipe(takeUntil(this.destroy$)) // dont forget to unsubscribe, can cause memory leaks
.subscribe(() => this.doSomething());
}
doSomething(): void {
// your logic here
}
ngOnDestroy(): void {
this.destroy$.next();
}
}

Related

Angular - NgForm valueChanges called multiple times on page load

I have a problem using the valueChanges function of ngForm. When binding an Input variable to the form with [(ngModel)], the form gets called multiple times on page load.
Is there a good way to only detect user changes?
export class ContainerComponent implements OnInit, AfterViewInit {
#Input() formData: Object;
#ViewChild('form') form: NgForm;
constructor() { }
ngOnInit(): void {
}
ngAfterViewInit(): void {
this.form.form.valueChanges.subscribe((value) => {
//Gets called multiple times on page load
});
}
}
Perhaps it will be sufficient to just check for dirty/touched state
From: https://angular.io/guide/form-validation
To prevent the validator from displaying errors before the user has a chance to edit the form, you should check for either the dirty or touched states in a control.
When the user changes the value in the watched field, the control is marked as "dirty".
When the user blurs the form control element, the control is marked as "touched".
I solved the problem:
export class ContainerComponent implements OnInit, AfterViewInit {
#Input() formData: Object;
#ViewChild('form') form: NgForm;
constructor() { }
ngOnInit(): void {
}
ngAfterViewInit(): void {
this.form.form.valueChanges.subscribe((value) => {
if(this.form.form.dirty) {
//DO STUFF
}
});
}
}
Try this :
export class ContainerComponent implements OnInit, AfterViewInit {
#Input() formData: Object;
#ViewChild('form') form: NgForm;
constructor() { }
ngOnInit(): void {
this.form.form.valueChanges.subscribe((value) => {
//Gets called multiple times on page load
});
}
ngAfterViewInit(): void {
}
}

Unsubscribing in angular

How can I unsubscribe in this function after dialog's closeAll method? Is it possible to turn subscription to a variable and then somehow apply it to 'onProjectAdded' instance?
this.dialog
.open(GridAddDialogComponent)
.componentInstance.onProjectAdded.subscribe((projectData) => {
this._gridApi.setRowData([...this.rowData1, projectData]);
})
this.dialog.closeAll()
}
There are 2 ways:
1. Auto unsubscribe after component destroy:
export class ExampleComponentComponent implements OnInit, OnDestroy {
readonly _destroy$: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);
constructor(
private readonly dialog: MatDialog
) { }
ngOnInit() {
this.dialog
.open(GridAddDialogComponent)
.componentInstance.onProjectAdded
.pipe(takeUntil(this._destroy$))
.subscribe((projectData) => {
this._gridApi.setRowData([...this.rowData1, projectData]);
})
this.dialog.closeAll();
}
ngOnDestroy(): void {
this._destroy$.next(null);
this._destroy$.complete();
}
}
Unsubscribe when you need it(in my case afret destroying component but you can do it everywhere):
export class ExampleComponentComponent implements OnInit, OnDestroy {
public subscriptions: Subscription[] = [];
constructor(
private readonly dialog: MatDialog
) { }
ngOnInit() {
const subscription = this.dialog
.open(GridAddDialogComponent)
.componentInstance.onProjectAdded
.subscribe((projectData) => {
this._gridApi.setRowData([...this.rowData1, projectData]);
});
this.subscriptions.push(subscription);
this.dialog.closeAll();
}
ngOnDestroy(): void {
this.subscriptions.forEach((sub) => sub.unsubscribe());
}
}
Also you can use single subscription instead of array of it.

How to publish event from a component and receive it from another component in nativescript

How can I publish custom event from a component and receive it from another component in nativescript.
something like:
ComponentOne.ts
this.event.publish('someEvent', {name: 'a name'})
ComponentTwo.ts
this.event.subscribe('someEvent', (data) => {
const name = data.name;
})
You can use subject for this case
import { Injectable } from "#angular/core";
import { Subject } from "rxjs";
#Injectable()
export class MessageService {
private subject = new Subject<any>();
constructor() {}
sendMessage(message: any) {
this.subject.next(message);
}
getData() {
return this.subject.asObservable();
}
}
I defined 2 method here. The first method using next() to send message to the next subcriber. So in your component you just need to simply subscribe like this to get the data
private subscription$: Subscription;
public ngOnInit(): void {
this.subscription$ = this.messageervice
.getData()
.subscribe(data => { console.log(data); })
}
public ngOnDestroy(): void {
this.subscription$.unsubscribe();
}
Found a workaround.
the basic idea is to register a custom event on the root frame of the app and listen on it from the other components
ComponentOne.ts
frameModule.topmost().notify({
eventName: 'punched',
object: frameModule.topmost(),
})
ComponentTwo.ts
frameModule.topmost().on('punched', () => {
console.log('event received');
})

Reference to service from dynamically added component in Angular 6 CLI

I created a directive in Angular 6 named 'DeleteDirective' and reference to a service 'DeleteService' to make sure I can delete an item from my application. After the item is marked as deleted (in PHP back-end), I'll show an Undo element via the 'UndoComponent' that I dynamically added in the DeleteService. No problems so far.
#Directive({
selector: '[appDelete]'
})
export class DeleteDirective {
constructor(
#Inject(ViewContainerRef) viewContainerRef,
renderer: Renderer2
) {
service.renderer = renderer;
service.setRootViewContainerRef(viewContainerRef);
service.addUndoElement();
}
#HostListener('click') onClick() {
// (Some code to execute deletion)
this.deleteService.showUndoElement();
}
#Injectable({
providedIn: 'root'
})
export class DeleteService {
constructor(
rendererFactory: RendererFactory2,
private factoryResolver: ComponentFactoryResolver,
private appRef: ApplicationRef,
) {
this.renderer = rendererFactory.createRenderer(null, null);
this.factoryResolver = factoryResolver;
}
setRootViewContainerRef(viewContainerRef) {
this.rootViewContainer = viewContainerRef;
}
addUndoElement() {
const factory = this.factoryResolver.resolveComponentFactory(UndoComponent);
const component = factory.create(this.rootViewContainer);
// this.rootViewContainer.insert(component.hostView);
this.appRef.attachView(component.hostView);
const domElem = (component.hostView as EmbeddedViewRef<any>)
.rootNodes[0] as HTMLElement;
document.body.appendChild(domElem);
}
}
Now, in the UndoComponent HTML I created a link to undo the action, named restoreItem. I would like to use another service named ListService to get some data again.
#Injectable()
export class UndoComponent implements OnInit {
constructor(private listService: ListService) {
}
restoreItem() {
this.currentList = this.listService.getSelectedList();
console.log(this.currentList); // null
}
}
It seems I cannot reference to the ListService (or any other service) from this dynamically added component to the DOM. It returns null. Any ideas how I can access a service from a dynamically added Component? Thanks so much for any directions!
Edit: added Listservice stub code for clarification
#Injectable({
providedIn: 'root'
})
export class ListService {
lists: List[];
list: List[];
currentList: List;
constructor(private http: HttpClient) { }
setSelectedList(list: List): void {
this.currentList = list;
}
getSelectedList(): List {
return this.currentList;
}
private handleError(error: HttpErrorResponse) {
console.log(error);
return throwError('Error! something went wrong.');
}
}
Are you setting the value of currentList in ListService in anyway.
setSelectedList in ListService is never called which is being used to set value of currentList. So currentList remains null.

Angular4 - let multiple unrelated components notify each other of the problem of updating data, and whether there is a cleaner coding method?

I have encountered a project in progress, let multiple unrelated components notify each other of the update data, is there a cleaner coding method?
There are 3 components (more likely later) and a common-data component. They have no parent-child relationship with each other and only show on the same screen.
The desired effect is to press the button of any component, update the contents of common-data, and notify yourself and other components to fetch new messages from common-data.
At present, my approach is to use Rx's Observable and Subscription, but they must be imported in the component.ts and service.ts files of each component, and a lot of duplicate code appears, it is very messy, I don't know what is better. practice?
Thanks!
My code :
The sample name is test-a-comp (a.b.c and so on, the code is the same)
test-a-comp.html
<p>
{{ownMessage}}
</p>
<button (click)="sendChange()">update</button>
test-a-comp.component
import { Component, OnInit } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
import { CommonData } from '../common-data/common-data';
import { TestACompService } from './test-a-comp.service';
import { TestBCompService } from '../test-b-comp/test-b-comp.service';
import { TestCCompService } from '../test-c-comp/test-c-comp.service';
#Component({
selector: 'app-test-a-comp',
templateUrl: './test-a-comp.component.html',
styleUrls: ['./test-a-comp.component.css']
})
export class TestACompComponent implements OnInit {
subscription: Subscription;
ownMessage;
constructor(
private testAService: TestACompService,
private testBService: TestBCompService,
private testCService: TestCCompService,
) {
this.subscription = this.testAService.getMessage()
.subscribe((test) => {
CommonData.message = test;
});
this.subscription = this.testBService.getMessage()
.subscribe(() => {
this.ownMessage = CommonData.message;
});
this.subscription = this.testCService.getMessage()
.subscribe(() => {
this.ownMessage = CommonData.message;
});
}
ngOnInit() {
}
sendChange() {
this.testAService.sendMessage();
}
}
test-a-comp.service:
import { Injectable } from '#angular/core';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
#Injectable()
export class TestACompService {
subscription: Subscription;
private subject = new Subject<any>();
constructor() {
}
getMessage(): Observable<any> {
return this.subject.asObservable();
}
sendMessage(): void {
this.subject.next('update message from A');
}
}
As far as i understand & you've mentioned in the above, there is a button in one of the component (test-a-component.html). If you update the button, you need to send message to other components which are subscribed.
The Components which have no Parent-Child relationship can communicate via a service:
Create a single service file (In your case: test-a-comp.service)
Create a Subject on what data you need to communicate via this service:
export class testMessageService {
constructor() {}
// Observable string sources
private message = new Subject<string>();
//Observable string streams
testMessage$ = this.message.asObservable();
constructor() {}
// Method to send message when a button is clicked
sendMessage(message: string) {
this.message.next(message);
}
/* You don't need "getMessage()" method as you've already subscribed to
the observables. There subscribed Observable string streams are
injected in your components (As below point 3) to display / do other
operation on the message. */
}
In your other Components, where you want to receive messages, do the following:
export class TestComponent 1 {
myMessage1: string;
constructor(private TestMessageService: testMessageService) {}
TestMessageService.testMessage$.subscribe(message => {
this.myMessage1 = message;
});
}
export class TestComponent 2 {
myMessage2: string;
constructor(private TestMessageService: testMessageService) {}
TestMessageService.testMessage$.subscribe(message => {
this.myMessage2 = message;
});
}
export class TestComponent 3 {
myMessage3: string;
constructor(private TestMessageService: testMessageService) {}
TestMessageService.testMessage$.subscribe(message => {
this.myMessage3 = message;
});
}
For more information/guidance refer Component interaction via a common
service: https://angular.io/guide/component-interaction
Hope this helps!

Categories

Resources