Exchange Data between multi step forms in Angular2: What is the proven way?
Asked Answered
D

2

13

I can imagine following approaches to exchange Data between multi step forms:

1) Create a component for each form step and exchange data between components over @input, @output (e.g. you cannot change from step5 to 2)

2) Use the new property data in the new router (see here) (e.g. you cannot change from step5 to 2))

3) A shared Service (Dependency Injection) to store data (Component Interaction) (e.g. you can change from step5 to 2)

4) New rudiments with @ngrx/store (not really experienced yet)

Can you give some "gained experience values", what do you use and why?

Darrickdarrill answered 7/7, 2016 at 9:51 Comment(4)
It is going about models, storing and exchange data between components, multiforms are just an example to use for showing it in action,..Darrickdarrill
Yeah, that is, what I like to doDarrickdarrill
Do you have an angular2 specific solution?Darrickdarrill
I have formed the basics for MultiStepAuth but can't make it run on PLUNKER , due to App being run in iframe , see if it helps.Amulet
P
9

Why not use session storage? For instance you can use this static helper class (TypeScript):

export class Session {

  static set(key:string, value:any) {
      window.sessionStorage.setItem(key, JSON.stringify(value));
  }

  static get(key:string) {
      if(Session.has(key)) return JSON.parse(window.sessionStorage[key])
      return null;
  }

  static has(key:string) {
      if(window.sessionStorage[key]) return true;
      return false;
  }

  static remove(key:string) {
      Session.set(key,JSON.stringify(null)); // this line is only for IE11 (problems with sessionStorage.removeItem)
      window.sessionStorage.removeItem(key);
  }

}

And using above class, you can put your object with multi-steps-forms data and share it (idea is similar like for 'session helper' in many backend frameworks like e.g. php laravel).


The other approach is to create Singleton service. It can look like that (in very simple from for sake of clarity) (I not test below code, I do it from head):

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

@Injectable()
export class SessionService {

    _session = {};

    set(key:string, value:any) {
         this._session[key]= value; // You can also json-ize 'value' here
    }

    get(key:string) {
         return this._session[key]; // optionally de-json-ize here
     }

     has(key:string) {
         if(this.get(key)) return true;
         return false;
     }

     remove(key:string) {         
         this._session[key]=null;
     }
}

And then in your main file where you bootstrap application:

...
return bootstrap(App, [
  ...
  SessionService
])
...

And the last step - critical: When you want to use you singleton service in your component - don't put int in providers section (this is due to angular2 DI behavior - read above link about singleton services). Example below for go from form step 2 to step 3:

import {Component} from '@angular/core';
import {SessionService} from './sessionService.service';
...

@Component({
  selector: 'my-form-step-2',
  // NO 'providers: [ SessionService ]' due to Angular DI behavior for singletons
  template: require('./my-form-step-2.html'),
})

export class MyFormStep2  {

  _formData = null;

  constructor(private _SessionService: SessionService) {
     this._formData = this._SessionService.get('my-form-data')
  }

  ...
  submit() {
     this._SessionService.set('my-form-data', this._formData)
  }

}

It should looks like that.

Puccini answered 11/7, 2016 at 17:40 Comment(6)
OK, so instead of session storage, just use global variable to store session values (idea is similar)Sapsucker
Ok, so you can create a Singleton SessionService which you register on bootstrap (so it will be visible every where in application and create once). This service will implement above similar idea. It is clear? Or you want more details?Sapsucker
where is the my-form-step-1 ? Do we have for each step a new component? how is the change from 1 to 2 and back? Why is _formData = null; at the step2? Thank you but sorry; that is not an elegant solution :/Darrickdarrill
You ask about data exchange. So my answer is above Signleton SessionService. This is the answer. The MyFormStep2 was only example how to use SessionService. Thats all. In this example of ussage I assume that you choose multi component for your multi-step-form (each component for each step) In this approach at the beginning the private variable _formData is null, and and constuctor we set up it by value stored in sessionService. But not focus on this example. Focus on answer -SessionService. And could you explain more why SessionService is not elegant? This approach is common in backend world.Sapsucker
What is RequestCounterService by the way?Darrickdarrill
sorry - it was mistake, i copy-paste some code from my past project - i will correct my answerSapsucker
N
15

See my edit below.


Using SessionStorage is not strictly the 'angular' way to approach this in my opinion—a shared service is the way to go. Implementing routing between steps would be even better (as each component can have its own form and different logic as you see fit:

const multistepRoutes: Routes = [
  {
    path: 'multistep',
    component: MultistepComponent,
    children: [
      {
        path: '',
        component: MultistepBaseComponent,
      },
      {
        path: 'step1',
        component: MultistepStep1Component
      },
      {
        path: 'step2',
        component: MultistepStep2Component
      }
    ]
  }
];

The service multistep.service can hold the model and implement logic for components:

import { Injectable, Inject } from '@angular/core';
import { Router } from '@angular/router';

@Injectable()
export class MultistepService { 

  public model = {};
  public baseRoute = '/multistep';
  public steps = [

    'step1', 
    'step2'

  ];

  constructor (
    @Inject(Router) public router: Router) { };

  public getInitialStep() {

    this.router.navigate([this.baseRoute + '/' + this.steps[0]]);

  };

  public goToNextStep (direction /* pass 'forward' or 'backward' to service from view */): any {

    let stepIndex = this.steps.indexOf(this.router.url.split('/')[2]);

    if (stepIndex === -1 || stepIndex === this.steps.length) return;

    this.router.navigate([this.baseRoute + '/' + this.steps[stepIndex + (direction === 'forward' ? 1 : -1)]]);

  };

}; 

Good luck.


EDIT 12/6/2016


Actually, now having worked with the form API for a while I don't believe my previous answer is the best way to achieve this.

A preferrable approach is to create a top level FormGroup which has each step in your multistep form as it's own FormControl (either a FormGroup or a FormArray) under it's controls property. The top level form in such a case would be the single-source of truth for the form's state, and each step on creation (ngOnInit / constructor) would be able to read data for its respective step from the top level FormGroup. See the pseudocode:

   const topLevelFormGroup = new FormGroup({
       step1: new FormGroup({fieldForStepOne: new FormControl('')}),
       step2: new FormGroup({fieldForStepTwo}),
       // ...
   });

   ... 

   // Step1Component

   class Step1Component { 
       private stepName: string = 'step1';
       private formGroup: FormGroup;
       constructor(private topLevelFormGroup: any /* DI */) {
           this.formGroup = topLevelFormGroup.controls[this.stepName];
       }
    }

Therefore, the state of the form and each step is kept exactly where it should be—in the form itself!

Necrosis answered 23/9, 2016 at 11:22 Comment(5)
Wow this might be just what I need, will try it!Plush
do you think your (edited) solution would have an equivalent to the routing one with eg canActivate etc on routes?Lisle
Can you clarify? This basically separates the logic for the form and the logic for the components—so, it shouldn't matter. If you want to you can move the stepName for each component from the component into the service that provides your formGroup. Then, canActivate (or anything else) could do checks on different form steps this.topLevelFormGroup.controls[this.topLevelFormGroup.steps.current].valid or something.Necrosis
Using Nikk's approach child routes can be avoided too. Parent component template can hold child components and Parent Component can show/hide child components based on which user's step movement. In the Parent component you can create main FormGroup with Controls using Reactive Form Creation approach. These form controls will be used in Child components. You can pass the main FormGroup from parent to child using @Input. It means you will be having a single form with several form controls to be used in child components. This way from parent component you will always have updated form values.Appose
We have done in similar way using Angular2 final versionAppose
P
9

Why not use session storage? For instance you can use this static helper class (TypeScript):

export class Session {

  static set(key:string, value:any) {
      window.sessionStorage.setItem(key, JSON.stringify(value));
  }

  static get(key:string) {
      if(Session.has(key)) return JSON.parse(window.sessionStorage[key])
      return null;
  }

  static has(key:string) {
      if(window.sessionStorage[key]) return true;
      return false;
  }

  static remove(key:string) {
      Session.set(key,JSON.stringify(null)); // this line is only for IE11 (problems with sessionStorage.removeItem)
      window.sessionStorage.removeItem(key);
  }

}

And using above class, you can put your object with multi-steps-forms data and share it (idea is similar like for 'session helper' in many backend frameworks like e.g. php laravel).


The other approach is to create Singleton service. It can look like that (in very simple from for sake of clarity) (I not test below code, I do it from head):

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

@Injectable()
export class SessionService {

    _session = {};

    set(key:string, value:any) {
         this._session[key]= value; // You can also json-ize 'value' here
    }

    get(key:string) {
         return this._session[key]; // optionally de-json-ize here
     }

     has(key:string) {
         if(this.get(key)) return true;
         return false;
     }

     remove(key:string) {         
         this._session[key]=null;
     }
}

And then in your main file where you bootstrap application:

...
return bootstrap(App, [
  ...
  SessionService
])
...

And the last step - critical: When you want to use you singleton service in your component - don't put int in providers section (this is due to angular2 DI behavior - read above link about singleton services). Example below for go from form step 2 to step 3:

import {Component} from '@angular/core';
import {SessionService} from './sessionService.service';
...

@Component({
  selector: 'my-form-step-2',
  // NO 'providers: [ SessionService ]' due to Angular DI behavior for singletons
  template: require('./my-form-step-2.html'),
})

export class MyFormStep2  {

  _formData = null;

  constructor(private _SessionService: SessionService) {
     this._formData = this._SessionService.get('my-form-data')
  }

  ...
  submit() {
     this._SessionService.set('my-form-data', this._formData)
  }

}

It should looks like that.

Puccini answered 11/7, 2016 at 17:40 Comment(6)
OK, so instead of session storage, just use global variable to store session values (idea is similar)Sapsucker
Ok, so you can create a Singleton SessionService which you register on bootstrap (so it will be visible every where in application and create once). This service will implement above similar idea. It is clear? Or you want more details?Sapsucker
where is the my-form-step-1 ? Do we have for each step a new component? how is the change from 1 to 2 and back? Why is _formData = null; at the step2? Thank you but sorry; that is not an elegant solution :/Darrickdarrill
You ask about data exchange. So my answer is above Signleton SessionService. This is the answer. The MyFormStep2 was only example how to use SessionService. Thats all. In this example of ussage I assume that you choose multi component for your multi-step-form (each component for each step) In this approach at the beginning the private variable _formData is null, and and constuctor we set up it by value stored in sessionService. But not focus on this example. Focus on answer -SessionService. And could you explain more why SessionService is not elegant? This approach is common in backend world.Sapsucker
What is RequestCounterService by the way?Darrickdarrill
sorry - it was mistake, i copy-paste some code from my past project - i will correct my answerSapsucker

© 2022 - 2024 — McMap. All rights reserved.