How can I load angular components in a page in sequence?

2.8k Views Asked by At

My problem scenario is as follows,

I have a page with a large form (+100 inputs). These inputs are divided into sections and those sections are developed as different components (for reusing purposes). So the general layout of the page is something like this,

<div>
    <form-section-a></form-section-a>
    <form-section-b></form-section-b>
    <form-section-c></form-section-c>
    ...
    <form-section-z></form-section-z>
</div>

Each form section will take some time to process because most of the inputs are material select boxes, and necessary data need to be loaded from the REST API and processed.

In the above shown layout, angular will try to render all form sections at once, hence adding up processing times of each section, which will cause the browser to freeze.

So, my plan is to load sections one after the other. Is there any recommended way to achieve this?

I have tried to write a structural directive to load components one after the other. Even though the directive works, there is no way I can know when the component has finished its internal processing work (May be the AfterViewInit hook). The directive looks something like this,

<div tcSequentialRenderer>
    <form-section-a *tcIfPreviousBlockLoaded></form-section-a>
    <form-section-b *tcIfPreviousBlockLoaded></form-section-b>
    <form-section-c *tcIfPreviousBlockLoaded></form-section-c>
    ...
    <form-section-z *tcIfPreviousBlockLoaded></form-section-z>
</div>

.

@Directive({selector: '[tcSequentialRenderer]'})
export class SequentialRendererDirective implements AfterViewInit {

  @ContentChildren(IfPreviousBlockLoadedDirective) directives: QueryList<IfPreviousBlockLoadedDirective>;

  ngAfterViewInit() {
    // start from first item
    if (this.directives.length > 0) {
      this.directives.toArray()[0].show();
    }

    let directivesArray = this.directives.toArray();
    for (let i = 1; i < directivesArray.length; i++) {
      directivesArray[i - 1].done.subscribe(status => {
        if (status) {
          directivesArray[i].show();
        }
      });
    }
  }
}

.

@Directive({selector: '[tcIfPreviousBlockLoaded]'})
export class IfPreviousBlockLoadedDirective {

  private isShown = false;

  public done: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

  constructor(
    private tplref: TemplateRef<any>,
    private vcref: ViewContainerRef
  ) { }

  public show(): void {
    if (!this.isShown) {
      this.vcref.createEmbeddedView(this.tplref);
      this.isShown = true;
    }
  }
}

If I can somehow access the associated component from the IfPreviousBlockLoadedDirective this method will work seamlessly.

Are there any suggestions to fix this, or are there any other ways to achieve this without changing the form-section components?

NOTE: Form sections can be any angular component.

3

There are 3 best solutions below

2
On

You're going way too far for so little.

Use event emitters bound to AfterViewInit hooks :

  <div>
    <form-section-a (viewLoaded)="displaySectionB = true"></form-section-a>
    <form-section-b *ngIf="displaySectionB" (viewLoaded)="displaySectionC = true"></form-section-b>
</div>
@Output() viewLoaded = new EventEmitter();
ngAfterViewInit() { this.viewLoaded.next(true); }

Because your components are depending on one each other, you will have to explicitly declare the conditions.

There's more advanced ways of doing that (for instance using arrays), but I'm sure you will find ways to do that on your own !

0
On

The best possible solution I can think is to have a following architecture:

Step-manager (This component will handle stepping into and out of each form section)

  • The step-manager will also provide a service, which will be available to all children components.
  • The Step-manager can also have stuff like a progress indicator (Progress-bar)
  • The step-manager can react to a behavior-subject on the service and switch between components/Different templates within a component.

Service

  • The service can handle all kinds of component interaction.
  • The service will contain a behavior-subject that will be used by all child components to inform the step-manager that either the back button was pressed, or next button was pressed.
  • The service will also have ability to make all service calls for each of the form-input data.

Child-components

  • The child components can be separate components (Not recommended)
  • They can be divided into different sections (For example: 3 steps of 'Personal Information' can be 1 component, 5 steps of 'Education information' can be 1 component)(I recommend this)
  • Each child component will have different templates embedded, which will be shown/hidden based on boolean values. (All handled by step-manager)

Please do let me know if this makes sense.

0
On

I think subject is good idea for your requirement. In fact, I have 3 forms in my app, and I used ion-modal-controller there. Simple example below:

class OneService {
  public formSubject: Subject<Object> = new Subject<Object>();
}

class ParentPage {
  constructor(oneService: OneService ) {
    this.oneService.formSubject.subscribe(nextStepComponentName => {
      this.nextStep(nextStepComponentName);
    });
  }
  nextStep(nextStepComponentName) {
     switch nextStepComponentName:
         case 'ChildComponentB':
            //load it
            break;
         default:
            this.cancel();
  }
}

class ChildComponentA {
  constructor(oneService: OneService ) {
  }
  save() {
     //do save.subscribe and
     this.oneService.formSubject.next('ChildComponentB');
  }
}

after child call subject.next(), then parent know the child finished. Hope it is helpful.