Timeline Wizard

The TimelineWizard component is designed to facilitate the creation of a timeline-based wizard in Angular applications. This component allows you to create a series of steps, each represented by a component, and navigate through them using a timeline. The TimelineWizard component provides features such as step navigation, asynchronous validation, and dynamic component rendering.

Core Components

TimelineBaseComponent

The TimelineBaseComponent serves as the base class for all step components within the timeline wizard. It provides essential properties and methods for managing the wizard steps. To utilize this, extend the abstract class for each step component and override specific properties and functions accordingly.

Properties

  • allStepsData: An array containing the data of all wizard steps.
  • currentStepData: The data of the current step or null if not available.
  • form: The form group associated with the step. Set to null for steps without a form.
  • next$: An observable that emits when the "Next" button is clicked. Override it to perform custom actions.
  • stepInvalid: Indicates whether the step's form is invalid.

Methods

  • formValueToData(): Converts the step's form value to data.
  • dataToFormValue(data: any): any: Converts step data to the form value that can be used for patchValue.

TimelineStep Interface

The TimelineStep interface defines the properties of a step in the timeline. You can pass an array of steps to the TimelineWizard component as inputs. The shape of this input should match this interface.

  • state: The state of the timeline step (e.g., 'not-started', 'current', 'success', 'error').
  • header: The header of the timeline step.
  • title: The title of the timeline step, acting as an identifier (should be unique).
  • id: The id of the timeline step, acting as an primary identifier. If id is not provided, will use title as identifier instead.
  • description: The description of the timeline step.
  • component: The Angular component associated with the timeline step.
  • data: Step data, which will be converted to form data by the timeline wizard component.

TimelineWizardComponent

TimelineWizardComponent is the main component responsible for managing the timeline wizard.

Inputs

  • live: Controls whether to destroy step components when clicking prev/next buttons.
  • confirmButtonText: The text for the "Confirm" button.
  • timelineSteps: An array of TimelineStep objects representing the steps in the wizard.

Outputs

  • confirmed: Emits all previous steps' data when clicking the finish button in the last step.
  • canceled: Emits when the wizard is canceled.
  • finished: Emits when the wizard is successfully completed.

Methods

  • cancel(): Cancels the wizard.
  • nextStep(): Moves to the next step.
  • previousStep(): Moves to the previous step.

TimelineWizardService

The TimelineWizardService is a service that manages the state and data of the timeline wizard. It provides methods to retrieve steps' data and the current step.

Methods

  • currentStepIndex: Get the index of the current step.
  • isFirstStep: Check if the current step is the first step.
  • isLastStep: Check if the current step is the last step.
  • currentStep: Get the current step object.
  • currentStepData: Get the data of the current step.
  • allStepsData: Get all steps data to be reviewed.
  • getStepData<T>(key: string): T: Get the data of a specific step by its id or title.

Example

In the following example, the wizard comprises four distinct steps. The timelineSteps array defines the sequential order of each step. Three of these steps, namely "Configure Operator," "Configure Service," and "Configure Runtime Properties," involve form input. The last step, labeled "Review," acts as a comprehensive summary, consolidating data from the preceding steps for presentation. Construct the timelineSteps array according to the structure outlined in the TimelineStep type. Additionally, customize event handlers for scenarios such as cancellation (canceled), confirmation (confirmed), and successful completion (finished).

TimelineWizard Component

import {ClrTimelineStepState} from '@clr/angular';
import {TimelineStep, TimelineWizardComponent} from 'clr-lift';
import {Deployment} from './deployment.type';

@Component({
  standalone: true,
  imports: [TimelineWizardComponent],
  template: `
    <cll-timeline-wizard
      [timelineSteps]="timelineSteps"
      [confirmButtonText]="'Finish'"
      (canceled)="onCanceled()"
      (confirmed)="onConfirmed($event)"
      (finished)="onFinished()"
    ></cll-timeline-wizard>
  `
})
export class TimelineWizardDemoComponent {
  // simulate an API response. These values will be set into step forms.
  initialData: Deployment = {
    operator: {
      name: 'my-operator',
      namespace: 'operator-namespace',
    },
    service: {
      cpu: 2,
      replicas: 4,
      url: 'https://example.service.com',
    },
    appProperties: {
      'java.runtime.debug': 'true',
    },
  };

  readonly timelineSteps: TimelineStep[] = [
    {
      state: ClrTimelineStepState.CURRENT,
      title: 'Configure Operator',
      id: 'operator', // use id to find the step data
      component: ConfigureOperatorComponent,
      data: {operator: this.initialData.operator},
    },
    {
      state: ClrTimelineStepState.NOT_STARTED,
      title: 'Configure Service',
      id: 'service',
      component: ConfigureServiceComponent,
      data: {service: this.initialData.service},
    },
    {
      state: ClrTimelineStepState.NOT_STARTED,
      title: 'Configure Runtime Properties', // use title to find the step, id is optional
      component: ConfigureRuntimePropComponent,
      data: {appProperties: this.initialData.appProperties},
    },
    {
      state: ClrTimelineStepState.NOT_STARTED,
      title: 'Review',
      component: ConfigureReviewComponent,
    },
  ];

  onCanceled() {
    window.alert('canceled');
  }

  onConfirmed(data: unknown) {
    window.alert('confirmed to submit, you can see form data from console. Simulate API request');
    console.log(data);
  }

  onFinished() {
    window.alert('finished');
  }
}
export type Deployment = {
  operator: {
    name: string;
    namespace: string;
  };
  service: {
    cpu: number;
    replicas: number;
    url: string;
  };
  appProperties: Record<string, string>;
};

Step Components

Let's delve into the implementation details of each step component. In essence, every step component is required to extend the base class, TimelineBaseComponent, specifying the pertinent step type. The significance of this specification lies in the fact that the currentStepData type aligns with the type defined in TimelineBaseComponent.

For form-based steps, it is essential to override the form property and instantiate the FormGroup as exemplified below.

There exist two methods to retrieve the current step data and all steps data. Firstly, you can employ the TimelineWizardService by invoking the getStepData method with the step id or title as the identifier. Alternatively, within the ngOnInit lifecycle hook, you can access currentStepData and allStepsData —these two properties are input properties in TimelineBaseComponent. It is imperative to note that attempting to access these properties in the constructor is prohibited due to their unavailability at that particular stage of the component lifecycle.

import {TimelineBaseComponent, TimelineWizardService} from 'clr-lift';
import {Deployment} from '../deployment.type';

@Component({
  template: `
    <form clrForm [formGroup]="form">
      <p class="clr-required-mark">Required Information</p>
      <clr-input-container>
        <label class="clr-required-mark">Name</label>
        <input type="text" clrInput [formControl]="form.controls.operator.controls.name" />
        <clr-control-error> Required </clr-control-error>
      </clr-input-container>
      <clr-input-container>
        <label class="clr-required-mark">Namespace</label>
        <input type="text" clrInput [formControl]="form.controls.operator.controls.namespace" />
        <clr-control-error> Required </clr-control-error>
      </clr-input-container>
    </form>
  `
})
export class ConfigureOperatorComponent extends TimelineBaseComponent<Deployment['operator']> implements OnInit {
  override form = new FormGroup({
    operator: new FormGroup({
      name: new FormControl('', [Validators.required]),
      namespace: new FormControl('', [Validators.required]),
    }),
  });

  timelineWizardService = inject(TimelineWizardService);

  // use id 'operator' to find the step
  stepData = this.timelineWizardService.getStepData<Pick<Deployment, 'operator'>>('operator');

  constructor() {
    super();

    // currentStepData shape comes from TimelineBaseComponent<Deployment['operator']>.
    console.log(this.currentStepData); // not available at the time
    console.log(this.stepData); // available from service
  }

  ngOnInit() {
    console.log(this.currentStepData); // will receive the @Input data
    console.log(this.stepData); // available from service
  }
}

In situations where your FormGroup model does not precisely align with the data structure outlined in the timelineSteps, customization of the mapping logic becomes necessary. This involves the overriding of two key methods: formValueToData and dataToFormValue. The example code below illustrates this process:

Consider a scenario where the FormGroup consists of three controls—cpu, replicas, and url. Contrastingly, the timelineSteps data structure follows the pattern service: cpu, replicas, url. This discrepancy necessitates adjustments to ensure compatibility. The overridden formValueToData method exemplifies how to align the returned result with the step data. Additionally, the dataToFormValue method is overridden to facilitate the conversion of step data to the corresponding form value.

export class ConfigureServiceComponent extends TimelineBaseComponent<Deployment['service']> {
  override form = new FormGroup({
    cpu: new FormControl('', [Validators.required]),
    replicas: new FormControl('', [Validators.required]),
    url: new FormControl('https://default-url.com', [Validators.required]),
  });

  override formValueToData() {
    return {service: this.form.value};
  }

  override dataToFormValue(data: any): Record<string, any> {
    return data.service;
  }
}

In essence, these customizations ensure that the form values align seamlessly with the expected step data, maintaining consistency between the form structure and the specified model.

Illustrating a more advanced use case, consider the following example that delves into the intricacies of dataToFormValue and formValueToData customization:

import {KeyValueInputsComponent, TimelineBaseComponent} from 'clr-lift';

type RuntimePropStepData = {appProperties: Array<{key: string; value: string}>};

@Component({
  standalone: true,
  imports: [
    KeyValueInputsComponent,
    //...
  ],
  template: `
    <form clrForm [formGroup]="form">
      <cll-key-value-inputs
        [formArray]="form.controls.appProperties"
        [inputSize]="50"
        [data]="currentStepData?.appProperties!"
      />
    </form>
  `
})
export class ConfigureRuntimePropComponent extends TimelineBaseComponent<RuntimePropStepData> {
  private fb = inject(FormBuilder);

  override form = this.fb.group({
    appProperties: this.fb.array<
      FormGroup<{
        key: FormControl<string>;
        value: FormControl<string>;
      }>
    >([]),
  });

  override formValueToData(): {appProperties: Record<string, string>} {
    const keyValuePairArray = this.form.controls.appProperties.value;
    const props: Record<string, string> = {};
    keyValuePairArray.forEach((kv) => {
      if (kv.key && kv.value) {
        props[kv.key] = kv.value;
      }
    });
    return {appProperties: props};
  }

  override dataToFormValue(data: {appProperties: Record<string, string>}): RuntimePropStepData {
    return {
      appProperties: Object.keys(data.appProperties).map((key) => {
        return {key, value: data.appProperties[key]};
      }),
    };
  }
}

In certain scenarios, your application might require a review step to consolidate and confirm the submission of all preceding form values. Unlike previous step components, there's no need to override the form in the review step, as it doesn't involve any form controls. The key focus is on aggregating data from the preceding steps. Utilize the TimelineWizardService method getStepData to fetch the necessary data and present it in the UI accordingly. Once you've verified that all the data is accurate, you can proceed to submit it.

To accomplish this, you'll need to perform two main tasks. First, you must aggregate all the steps' data and map it to the desired API payload. The second task involves triggering the API call. You can leverage the timelineWizardService.allStepsData to obtain all data. However, note that the shape is an array containing each step's id or title as an identifier along with the respective step data. To address this, you can create a custom data conversion function, such as the buildPayload method outlined below. Subsequently, to initiate the API request, override the next$ observable to construct your API stream.

import {TimelineBaseComponent, TimelineWizardService} from 'clr-lift';
import {Deployment} from '../deployment.type';

export class ConfigureReviewComponent extends TimelineBaseComponent {
  private timelineWizardService = inject(TimelineWizardService);
  private http = inject(HttpClient);

  override next$ = this.submitAPI();

  get operatorStep() {
    // still support to use title to find the step though id exists.
    return this.timelineWizardService.getStepData<Pick<Deployment, 'operator'>>('Configure Operator');
  }
  get serviceStep() {
    return this.timelineWizardService.getStepData<Pick<Deployment, 'service'>>('Configure Service');
  }
  get runtimePropertiesStep() {
    return this.timelineWizardService.getStepData<Pick<Deployment, 'appProperties'>>('Configure Runtime Properties');
  }

  private submitAPI() {
    return this.http.post('your-api', this.buildPayload());
  }

  private buildPayload() {
    const formSpec = this.timelineWizardService.allStepsData.reduce(
      (accumulator, currentStep) => ({...accumulator, ...currentStep.data}),
      {},
    );

    return formSpec;
  }
}

Important Notes

  • Ensure that each step component extends TimelineBaseComponent and follows the provided usage example.
  • Customize the TimelineStep objects to match the steps in your wizard.
  • Handle asynchronous operations in your step components by overriding the next$ observable.

Interactive UI

Configure Operator
Configure Service
Configure Runtime Properties
Review

Required Information