Pokemon Game
Remote Fetch + Projection Example
This example shows how to fetch remote data and map it into multiple fields when a user selects a value.
What this example demonstrates
- Remote API fetch
- Mapping API response to fields (projection)
- Updating multiple fields from one request
- Calculated fields
- Event → Trigger → Action flow
Basic Idea
Change → Fetch → Map Response → Update Fields → Recalculate
Everything is configured declaratively in the field model.
Triggering a Remote Fetch
When the user selects a value, a fetch action is triggered.
{
triggers: [
{
on: 'change',
action: 'fetch',
fetchUrl: '$value',
projection: {
hp: 'stats[0].base_stat',
attack: 'stats[1].base_stat',
defense: 'stats[2].base_stat',
image: 'sprites.other.showdown.front_default'
}
source: 'type',
}
]
}
The engine will:
- Call the API
- Read the response
- Map values into fields
- Update the UI
Projection (Mapping API Response)
Projection maps API response fields into form fields.
{
projection: {
hp: 'stats[0].base_stat',
attack: 'stats[1].base_stat',
defense: 'stats[2].base_stat',
image: 'sprites.other.showdown.front_default'
}
}
This allows updating multiple fields from a single API response without writing component code.
Calculated Fields
Fields can calculate values based on other fields.
new OutputField({
key: 'damage',
label: 'Damage',
calculation: 'Number(player.attack) - Number(opponent.defense) / 2'
})
Calculated fields automatically update when dependent values change.
Mental Model
Think in this pattern when building forms:
User action → Trigger → Action → State update → UI update
Examples:
- Select value → fetch related data
- Enter input → validate asynchronously
- Change number → recalculate result
- Click button → trigger action
- Select item → load additional details
Why This Example Matters
This example demonstrates that Preforms is not just a form renderer.
It is a declarative, event-driven form engine where behavior is defined using triggers and actions instead of component code.
One Sentence Summary
When something happens, trigger an action that updates field state, and the UI updates automatically.
Live Demo
Result
import { Component, EventEmitter, Output, ViewEncapsulation } from '@angular/core';
import { Preforms } from '@preforms/angular/core';
import { DYNAMIC_FORM_FETCHER } from '@preforms/angular/core';
import { NATIVE_FORM_ELEMENTS } from '@preforms/angular/native';
import {
DialogField,
FieldButton,
FieldGroup,
FieldWrapper,
FormDivider,
FormFieldEventType,
FormImage,
FormTitle,
InputField,
NumberField,
OutputField,
SelectField,
TriggerAction,
} from '@preforms/ts';
import { from } from 'rxjs';
function randomIntBetween(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function createPlayer(playerName: string, url = '$value', showdown = 'back_default') {
return new FieldGroup({
key: playerName,
hidden: true,
fields: [
new FormImage({
src: '',
key: 'image',
}),
new InputField({
type: 'number',
key: 'hp',
value: 0,
className: 'indicator',
readonly: true,
}),
new InputField({
type: 'number',
key: 'attack',
value: 0,
className: 'indicator',
readonly: true,
}),
new InputField({
type: 'number',
key: 'defense',
value: 0,
className: 'indicator',
readonly: true,
}),
new NumberField({
key: 'hit',
readonly: true,
className: 'hit',
}),
new OutputField({
key: 'damage',
calculation: `
${playerName}.hit ? Number(${playerName}.defense) - Number(${playerName}.hit) : 0`,
for: ['p1', 'p2'],
// hidden: true,
}),
],
triggers: [
{
on: FormFieldEventType.SELECT,
action: TriggerAction.FETCH,
fetchUrl: url,
source: 'pokemon',
mode: 'patch',
projection: {
target: 'value',
select: {
hp: 'stats[0].base_stat',
attack: 'stats[1].base_stat',
defense: 'stats[2].base_stat',
image: `sprites.other.showdown.${showdown}`,
},
},
},
{
on: FormFieldEventType.SELECT,
action: TriggerAction.UPDATE,
state: {
hidden: false,
},
},
],
});
}
@Component({
selector: 'app-dynamic-form-fetch-remote',
template: `<preforms (submittedData)="logData($event)" [fields]="fields" className="pokemon" />`,
imports: [Preforms],
styles: [
`
.pokemon {
.grid {
max-width: 400px;
padding: 20px;
display: grid;
grid-template-areas: 'a a' 'b b';
gap: 10px;
grid-auto-rows: 40px 300px;
align-items: end;
}
.hit {
.preforms-input-field {
all: unset;
}
}
.indicator {
.preforms-input-field {
max-width: 100%;
}
}
.primary {
background-color: rgb(255, 187, 0);
padding: 0.6rem 2rem;
border: 1px solid #cfd6e3;
border-radius: 25px;
font-weight: 700;
margin-top: 30px;
display: inline-block;
}
}
`,
],
providers: [
NATIVE_FORM_ELEMENTS,
{
provide: DYNAMIC_FORM_FETCHER,
useValue: (url: string) => {
return from(fetch(url).then((res) => res.json()));
},
},
],
encapsulation: ViewEncapsulation.None,
})
export class DynamicFormFetchRemoteComponent {
@Output() formChange = new EventEmitter<any>();
fields = [
new OutputField({
key: 'score',
calculation: 'Number(p1.damage) > Number(p2.damage)',
for: ['p1', 'p2'],
hidden: true,
}),
new DialogField({
key: 'popup',
fields: [],
triggers: [
{
on: 'change',
action: 'update',
state: {
fields: [new FormTitle('You win!')],
},
condition: true,
source: 'score',
},
{
on: 'change',
action: 'update',
state: {
fields: [new FormTitle('You lose!')],
},
condition: false,
source: 'score',
},
],
}),
new FieldWrapper({
className: 'grid',
fields: [
new FormDivider({ label: 'Player' }),
new FormDivider({ label: 'Enemy' }),
createPlayer('p1'),
createPlayer(
'p2',
'https://pokeapi.co/api/v2/pokemon/' + randomIntBetween(1, 151),
'front_default',
),
],
}),
new FormDivider({ label: 'Play!' }),
new SelectField({
key: 'type',
label: 'Select Pokemon Type',
options: [],
triggers: [
{
on: FormFieldEventType.INIT,
action: TriggerAction.FETCH,
fetchUrl: 'https://pokeapi.co/api/v2/type/',
mode: 'patch',
projection: {
target: 'options',
select: {
value: 'url',
label: 'name',
},
source: 'results',
},
},
],
}),
new SelectField({
key: 'pokemon',
label: 'Select Pokemon',
hidden: true,
options: [],
triggers: [
{
on: FormFieldEventType.CHANGE,
source: 'type',
action: TriggerAction.FETCH,
fetchUrl: '$value',
mode: 'patch',
projection: {
target: 'options',
select: {
value: 'pokemon.url',
label: 'pokemon.name',
},
source: 'pokemon',
},
},
{
on: FormFieldEventType.SELECT,
source: 'type',
action: TriggerAction.UPDATE,
state: {
hidden: false,
},
},
],
}),
new FieldButton({
type: 'button',
label: 'Attack!',
hidden: true,
className: 'primary',
triggers: [
{
on: 'select',
source: 'pokemon',
action: 'update',
state: {
hidden: false,
},
},
{
on: 'click',
action: 'update',
state: {
value: {
hit: 'p1.attack',
},
},
target: ['p2'],
},
{
on: 'click',
action: 'update',
state: {
value: {
hit: 'p2.attack',
},
},
target: ['p1'],
},
{
on: 'click',
action: TriggerAction.OPEN_DIALOG,
target: 'popup',
debounce: 500,
},
],
}),
];
logData(data: any) {
console.log(data);
// this.formChange.emit(data);
}
}