Freego
Freego

Reputation: 466

How to call a sibling component function in router-outlet and wait for its completion in Angular

I'm currently developping a web application consisting of a succession of long forms. I have a global component which template looks like this :

<app-header></app-header>
<div class="main-container">
  <router-outlet></router-outlet>
</div>
<app-navigation></app-navigation>

So I have multiple pages taking turns inside the router-outlet and 2 components that doesn't change, in particular the navigation component, which consists of buttons allowing to go back and forth between the differents screens/forms.

The goal for me was to be able to call a function of the page/form component inside the router-outlet from the navigation component to trigger form validation and a mandatory webservice call, and then trigger the navigation.

I did that with three things, an interface that every page/forms must implement and which looks like this :

export interface FormValidation {
  executeValidation();
}
let subscription;
export function validationEventSubscriber(action: Subject<any>, handler: () => void, off: boolean = false) {
  if (off && subscription) {
    subscription.unsubscribe();
  } else {
    subscription = action.subscribe(() => handler());
  }
}

A Service to manage the forms which contains this :

export class FormsService {

  //...other properties...

  validationSubject = new Subject();

  constructor() { }

  executeValidation() {
    this.validationSubject.next();
  }
  
  //...other functions...
}

And a Service to manage the Navigation which calls Router.navigateByUrl depending on different business rules.

Then in my NavigationComponent I have a method that calls executeValidation:

  //function called when clicking the button to go to the next page
  nextPage() {
    this.formsService.executeValidation();
  }

And finally within the pages component I have this :

  constructor(...) {
    this.executeValidation = this.executeValidation.bind(this);
    validationEventSubscriber(this.formsService.validationSubject, this.executeValidation);
  }

  executeValidation() {
    
    //...Validation...

    this.navigationService.nextPage();
  }

This workflow is working as expected I can trigger the executeValidation function of the pages from the NavigationComponent, but what I'm struggling to achieve is to make the validation asynchronous.

What bothers me here is that I'm calling the NavigationService inside the page and I need to do so in every pages which copy/paste the same line of code inside every executeValidation method. I'd prefer to do that from within the NavigationComponent but I would have to wait to the validation inside the Page Component to end, and that's where I struggle.

If anyone has an idea on how to tackle this problem, i'm all ears !

Upvotes: 1

Views: 613

Answers (1)

Picci
Picci

Reputation: 17762

I tend to have pure business logic confined into services and everything related to the "view" part into components, including the navigation via router.

So, if this philosophy fits with you, this is the way I would proceed.

Create a service myService which exposes the following

private _validate$ = new Subject<bool>();
public validate$ = this._validate$.asObservable();
public executeValidation() {
  this._validate.next(true);
}

myService in injected into app-navigation Component and in all Components that get loaded into the router-outlet.

Then you can create another validationService where you define one or more validation methods which call the webservice(s) that actually perform the job.

validationService would look like this

private _validationResult$ = new Subject<bool>();
public validationResult$ = this_validationResult$.asObservable();
// each form may have its own validation method
// the input of each validation method can be typed via an interface or let any as here
public validateFormX(input: any) {
   // assume validation is a post web service exposed by a remote server
   this.http.post("validationX_url", input).pipe(
     // when the response comes we notify it using _validationResult$
     tap({
      next: resp => this._validationResult$.next(resp),
      error: err => this._validationResult$.error(err)
     }) 
   )
}

We inject the validationService into all Components that get loaded into the router-outlet. Each of this Components so has to subscribe both to myService.validate$ and validationService.validationResult$ like this

// when myService.validate$ notifies, then we launch the validation logic
this.myService.validate$.subscribe(
  // assume each Component in the router-outlet is able to create the input for validation
  const input = buildValidationInput();
  () => this.validationService.validateFormX(input)
)

this.validationService.validationResult$.subscribe(
  validationResp => {
    // depending on the validation response navigate to the corresponding page
    this.router.navigate(....)
  }
)

If you follow this approach you have the advantage that most of the logic in in the services which you can test much easier than the components.

Upvotes: 1

Related Questions