简体   繁体   中英

What is the correct, elegant way to call a child function from a parent?

I have three components:

MasterHeader (will always be the same) Content (this one will vary in content, specifically in the HTML) MasterFooter (will always be the same)

MasterHeader and MasterFooter have some buttons. They will always do the same thing, what will change is the content. MasterFooter has a Save button. I want to be able to take some form data in the Content component, and send it to the save method in the MasterFooter. The way that I am achieving that right now is:

In MasterFooter I have something like:

import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-master-footer',
  templateUrl: './master-footer.component.html',
  styleUrls: ['./master-footer.component.css']
})
export class MasterFooterComponent implements OnInit {

  @Output() save = new EventEmitter();

  constructor() { }

  ngOnInit(): void {
  }

  onSave() {
    this.save.emit()
  }

  actualSave(formDict: any, editing: boolean, id: number, masterTable: string) {
    //Actual saving code goes here
  }
}

And then in the Content component (remember this one will be different, there could be multiple components that are content but they will contain a form with data and they will always have a MasterHeader and MasterFooter) something like this:

import { Component, OnInit, ViewChild } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-physician',
  templateUrl: './physician.component.html',
  styleUrls: ['./physician.component.css']
})
export class PhysicianComponent implements OnInit {
  form: FormGroup;
  editing: boolean = false;
  id: number = 0;

  @ViewChild('footer') footer: any;

  constructor() {
    this.physicianForm = new FormGroup({
      //Define form controls here
    });
  }

  ngOnInit(): void {
  }

  onSave() {
    const formDict = this.form.getRawValue();
    this.footer.actualSave(formDict, this.editing, this.id, 'somename');
  }

}

As you can see, I emit from the MasterFooter to toggle the save from the Content (in this case it's PhysicianComponent). Then in the save that's toggled from the PhysicianComponent, I call the actualSave function inside the MasterFooter. I can't help but feel that this is an over complication of things, and that there should be a better, more elegant way of doing this. But as far as I've read I haven't found anything, or I just haven't been able to understand it. Any help to make this better will be appreciated.

EDIT: Before the bounty I want to say, if there is a better way to do this please let me know. This is the best that I could come up with, with my knowledge of Angular.

In this kind of complication i used to declare a service for each component that i'd like to access its properties and methods from other components.

In your case, you can create footer.service.ts and header.service.ts , then call them instance in the content component constructor like this:

constructor(
    private _header : HeaderService,
    private _footer : FooterService
){}

and then you can access them properties and methods whenever you want.

one more thing, if you have the same functions between header and footer you can optimize it and make one shared service that contains one single declaration of these functions,

so instead of having _header & _footer , we can just do

constructor(
    private _ui : UserInterface.service.ts 
){}

You're pretty much there. I think one way to improve on this is by not using @ViewChild. Since you lose typing by making it "any".

To solve this you could introduce a service like @Ala Mouhamed mentioned. But it's also possible to do this by inputting a Subject from the Content component. That will allow you to submit the form data through there.

export class MasterFooterComponent implements OnInit {
  // PhysicianModel is a model that represents the form data.
  @Input() saveForm$: Subject<PhysicianModel>;
  @Output() save = new EventEmitter();

  constructor() {}

  ngOnInit(): void {
    this.saveForm$.subscribe((formData) => {
      this.actualSave(formData);
    });
  }

  onSave() {
    this.save.emit();
  }

  actualSave(formData: PhysicianModel) {
    // Actual saving code goes here
  }
}

and:

export class PhysicianComponent {
  saveForm$: Subject<PhysicianModel> = new Subject();
  physicianForm: FormGroup;

  constructor(private fb: FormBuilder) { 
     this.physicianForm = new FormGroup({
        //Define form controls here
     });
   }

  onSave() {
    const formDict = this.profileForm.getRawValue() as ContentForm;
    this.saveForm$.next(formDict);
  }
}

Here is a working example: https://stackblitz.com/edit/angular-ivy-sbkdnu?devtoolsheight=33&file=src/app/content/content.component.ts

One of the problems with this approach is Typing. The example above uses "PhysicianModel", but that only works for that one model. If you don't care about typing, you could simply replace it with "any". But passing the different forms and their respective data as typed classes is better for maintainability.

Let's say you want to add a DoctorComponent, that submits a DoctorForm, that aligns with a DoctorModel class. Now you can no longer rely on passing a single type to the Subject. So it will have to become a generic. While we are scoped in our PhysicianComponent we have knowledge of this type (PhysicianComponent = PhysicianModel). That means we can pass PhysicianModel as type. However these types cannot be passed to a component. So a service or some external class works better here to preserve typing.

I would create a common UI manager service and initialize rxjs BehaviorSubject inside it. https://rxjs.dev/guide/subject#behaviorsubject

Header, Footer and Content components could then inject that service and read and save the button / save state.

This same service could be extended to manage also the state of other ui components like sidebar, menus, etc.

Example of UI Manager service:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

// This interface can be modified and extended to include also other information, for example darkMode etc. 
interface UIState {
  editorSaveState: string;
  sideNavOpen: boolean;
}

@Injectable({
  providedIn: 'root',
})
export class UIManagerService {

  private _uiState: UIState = {
    editorSaveState: 'unsaved',
    sideNavOpen: false
  };

   // Observable emitting UI state values for depending components.
  private uiState$: BehaviorSubject<UIState> = new BehaviorSubject(
    this._uiState
  );

  // private method used by this service to set the UI state and update BehaviorSubject
  private set uiState(state: UIState) {
    this.uiState$.next(state);
  }

  // Components use this getter method to access the UI State
  public get uiState(): UIState {
    return this.uiState$.getValue();
  }

  // Call this public method to change the sidenav status. Write additional logic if needed
  public sideNavToggle(): void {
    this._uiState.sideNavOpen = !this._uiState.sideNavOpen;
    this.uiState = this._uiState; // call setter to store and emit changes
  }

}
  // Call this public method to toggle editor save state. Write additional logic if needed
  public editorSaveStateChange(state: string): void {
    this._uiState.editorSaveState = state;
    this.uiState = this._uiState; // call setter to store and emit changes
  }
}

A component needing that service could use it like this:

constructor(private ui: UIManagerService) {}

get uiState(): UIState {
  return this.ui.uiState;
}

onSave(): void {
  this.ui.editorSaveStateChange('save');
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM