Skip to main content

Custom runner with Angular

This guide shows how to build a custom runner using Angular.

1️⃣ Create the blocks component

The first step is to create the blocks component. This component handles the rendering of the UI for all the blocks (question types) used in the form. Tripetto uses dependency injection to register blocks to the runner. So, our Angular component needs to be able to dynamically serve the Angular components of each block. To let that work, we need to define a block factory class which is the base class for all the blocks.

block.component.ts
import { Component, Directive, Input, OnInit, ViewContainerRef, Type, NgZone } from '@angular/core';
import { IObservableNode, NodeBlock } from '@tripetto/runner';

@Directive()
export abstract class BlockComponentFactory<T extends NodeBlock = NodeBlock> {
@Input() node!: IObservableNode<T>;

get block(): T {
return this.node.block as T;
}
}

@Component({
selector: 'tripetto-block',
templateUrl: './block.component.html',
})
export class BlockComponent extends BlockComponentFactory implements OnInit {
constructor(private viewContainerRef: ViewContainerRef, private zone: NgZone) {
super();
}

ngOnInit() {
if (this.node.block) {
this.zone.run(() => {
const instance = this.viewContainerRef.createComponent<BlockComponentFactory>(
this.node.block!.type.ref as Type<any>
).instance;

instance.node = this.node;
});
}
}
}

We first declare the abstract block factory class named BlockComponentFactory. Next, we declare the BlockComponent itself. It is derived from BlockComponentFactory and it implements the OnInit lifecycle hook. In the init method, the block component class of each node is retrieved and then a component instance is created using createComponent. A node may have no block attached to it. In that case, the node is just a static text. Those nodes are rendered using the block.component.html template.

2️⃣ Create the runner component

The next step is to define the actual runner component. This is the component you can use in your Angular app. It accepts a form definition as input, and emits a finished event when the form completes.

runner.component.ts
import { Component, ChangeDetectionStrategy, Input, EventEmitter, OnDestroy } from '@angular/core';
import { Runner, IDefinition, Instance, IStoryline, Export } from '@tripetto/runner';

@Component({
selector: 'tripetto-runner',
templateUrl: './runner.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RunnerComponent implements OnDestroy {
runner: Runner | undefined;
storyline!: IStoryline;

@Input() set definition(definition: IDefinition) {
this.zone.runOutsideAngular(() => {
this.runner = new Runner({
definition,
mode: 'paginated',
start: true,
});

this.runner.onChange = (ev) => {
this.storyline = ev.storyline;

this.changeDetector.detectChanges();
};

this.runner.onFinish = (instance: Instance) => {
this.finished.emit(Export.exportables(instance));

return true;
};
});
}

@Output() finished = new EventEmitter<Export.IExportables>();

ngOnDestroy() {
this.runner?.destroy();
}
}

Here we declare the RunnerComponent class. It contains the Runner instance that holds the state of the form. Because that state is managed in the Runner instance, we can disable the automatic change detection of the component and run the runner itself outside of Angular using runOutsideAngular. This greatly improves the performance of the runner component since it now only updates/rerenders when there is an actual change in the form.

The class also contains an EventEmitter called finished that is invoked when the form completes.

To let the component work, we need to define a module for the runner as shown in runner.module.ts.

3️⃣ Define blocks

Now we have implemented the runner, we can define our first block. For each block, you need to define two classes: A component class (derived from BlockComponentFactory) that implements the actual rendering template of the block. And, a Tripetto block class (derived from NodeBlock) that serves as the glue between the Angular component and the block used in the runner internals.

hello-world.component.ts
import { tripetto, NodeBlock } from '@tripetto/runner';
import { BlockComponentFactory } from 'block.component';
import { Component } from '@angular/core';

@Component({
templateUrl: './hello-world.component.html',
})
export class HelloWorldComponent extends BlockComponentFactory<HelloWorldBlock> {}

@tripetto({
type: 'node',
identifier: 'hello-world',
ref: HelloWorldComponent,
})
export class HelloWorldBlock extends NodeBlock {}

Here we define a block named HelloWorldBlock. It is registered using the @tripetto decorator using the identifier hello-world. This identifier is used in the form definition to select the right block for a node. In the decorator, the ref property is used to supply a reference to the Angular component of the block. This reference is used by the block component factory to select the right Angular component for each block.

tip

See the block implementation guide to learn how to develop blocks for Tripetto.

4️⃣ Use the component

Now we can use the component in an Angular application.

app.component.ts
import { Component } from '@angular/core';
import { RunnerComponent } from './runner/runner.component';
import { Export } from '@tripetto/runner';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
// This is the form definition we want to run
definition = {
name: 'Example form',
sections: [
{
'id': '0abbc4d4aacecbb8f1aa13e77c6ab7a58c416aefcc42f4fd77d6a1a46a4e3afa',
'nodes': [
{
'id': '362221f5ed388ec503a186f66be7829b542cb307b4f43e91473838a5f8c5ac75',
'block': { 'type': 'hello-world', 'version': '1.0.0' }
}
]
}
]
};

// The runner was finished, output the collected data to the console
onFinished(data: Export.IExportables) {
console.log('Form completed!');

data.fields.forEach((field) => {
if (field.string) {
console.log(`${field.name}: ${field.string}`);
}
});
}
}

🧑‍💻 Code examples

Have a look at the following fully working custom runner examples for Angular.


Angular + Bootstrap

Custom runner example that uses Angular and Bootstrap.

Demo Source code


Angular + Angular Material

Custom runner example that uses Angular and Angular Material.

Demo Source code