Form
<oge-form> lays out the @oge-ui/inputs editors: labels, responsive columns, nestable <fieldset> groups, a validation summary and submit. Items come from projected <oge-form-item> children, from a data-driven items array, or both — children first.
The binding mode is derived from what you bind: [fieldTree] for Angular Signal Forms, [formGroup] for reactive forms, or [(formData)] for a plain signal model. That last one is not a second validation engine — the form compiles each item's validationRules into a Signal Forms schema and runs the very same code path.
Declarative items
Each <oge-form-item> names a model property through field. Dot-notation reaches nested objects. colSpan widens an item across the layout columns, and hint lands in the editor's own subscript so nothing shifts when an error replaces it.
formData → Ada Lovelace
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeForm, OgeFormItem } from '@oge-ui/forms';
@Component({
selector: 'demo-root',
imports: [OgeForm, OgeFormItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-form [(formData)]="employee" [colCount]="2">
<oge-form-item field="firstName" label="First name" [isRequired]="true" />
<oge-form-item field="lastName" label="Last name" />
<oge-form-item field="email" label="E-mail" hint="We never share it." />
<oge-form-item field="notes" editorType="textArea" [colSpan]="2" />
</oge-form>
`,
})
export class Demo {
protected readonly employee = signal({
firstName: 'Ada',
lastName: 'Lovelace',
email: '',
notes: '',
});
}Data-driven items
The same item model as an array — useful when the fields come from a server. Entries render after any projected children, and visibleIndex reorders both sources together.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeForm } from '@oge-ui/forms';
import type { OgeFormItemData } from '@oge-ui/forms';
@Component({
selector: 'demo-root',
imports: [OgeForm],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-form [(formData)]="order" [items]="fields" [colCount]="2" />
`,
})
export class Demo {
protected readonly order = signal({
reference: '',
quantity: 1,
priority: 'normal',
shipped: false,
});
protected readonly fields: OgeFormItemData[] = [
{ field: 'reference', label: 'Reference', isRequired: true },
{ field: 'quantity', label: 'Quantity', editorOptions: { min: 1, max: 99 } },
{
field: 'priority',
label: 'Priority',
editorOptions: { items: ['low', 'normal', 'high'] },
},
{ field: 'shipped', label: 'Shipped', dataType: 'boolean' },
];
}Editor selection
With no editorType, the editor follows dataType — and dataType itself is inferred from the current model value. An editorOptions.items list beats the inferred type (a select box), and an explicit editorType beats everything. Anything richer than the curated options belongs in a template, not in a reflective options bag.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeForm, OgeFormItem } from '@oge-ui/forms';
@Component({
selector: 'demo-root',
imports: [OgeForm, OgeFormItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-form [(formData)]="profile" [colCount]="2">
<!-- no dataType: inferred from the model value -->
<oge-form-item field="name" label="Name" />
<oge-form-item field="age" label="Age" />
<oge-form-item field="birthday" label="Birthday" />
<oge-form-item field="active" label="Active" />
<!-- an option list beats the inferred type -->
<oge-form-item
field="team"
label="Team"
[editorOptions]="{ items: teams }"
/>
<!-- editorType beats everything -->
<oge-form-item field="bio" label="Bio" editorType="textArea" [colSpan]="2" />
</oge-form>
`,
})
export class Demo {
protected readonly teams = ['Platform', 'Design', 'Support'];
protected readonly profile = signal({
name: 'Grace',
age: 45,
birthday: new Date(1980, 4, 12),
active: true,
team: 'Platform',
bio: '',
});
}Groups
A group renders a real <fieldset> with the caption as its <legend> — the markup screen readers expect for a labelled section — and carries its own column count. Groups nest.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeForm, OgeFormGroup, OgeFormItem } from '@oge-ui/forms';
@Component({
selector: 'demo-root',
imports: [OgeForm, OgeFormGroup, OgeFormItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-form [(formData)]="account" [colCount]="2">
<oge-form-group caption="Identity" [colCount]="2">
<oge-form-item field="firstName" label="First name" />
<oge-form-item field="lastName" label="Last name" />
</oge-form-group>
<oge-form-group caption="Contact" [colCount]="2">
<oge-form-item field="email" label="E-mail" />
<oge-form-item field="phone" label="Phone" />
<oge-form-item field="address" label="Address" [colSpan]="2" />
</oge-form-group>
</oge-form>
`,
})
export class Demo {
protected readonly account = signal({
firstName: 'Ada',
lastName: 'Lovelace',
email: 'ada@example.com',
phone: '',
address: '',
});
}Label placement
labelLocation: 'top' keeps each editor's own label chrome. 'start' and 'end' hand the label to the form, which draws a real <label for> in its own column — alignItemLabels gives every row the same label width so the editors line up.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeForm, OgeFormItem } from '@oge-ui/forms';
@Component({
selector: 'demo-root',
imports: [OgeForm, OgeFormItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-form
[(formData)]="settings"
labelLocation="start"
[alignItemLabels]="true"
[showColonAfterLabel]="true"
[colCount]="1"
>
<oge-form-item field="host" label="Host" [isRequired]="true" />
<oge-form-item field="port" label="Port" />
<oge-form-item field="secure" label="Use TLS" />
</oge-form>
`,
})
export class Demo {
protected readonly settings = signal({
host: 'localhost',
port: 5432,
secure: true,
});
}Template slots
Four slots, each legal at form level (applies to every item) or inside a single <oge-form-item> (applies to that one, and wins). ogeFormEditorTemplate replaces only the control and keeps the label, required mark and error text — the context hands you editorId so your control keeps the <label for> association. ogeFormItemTemplate replaces the whole field, and ogeFormGroupCaptionTemplate replaces a legend's content.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeForm, OgeFormItem, OgeFormEditorTemplate, OgeFormLabelTemplate } from '@oge-ui/forms';
@Component({
selector: 'demo-root',
imports: [OgeForm, OgeFormItem, OgeFormEditorTemplate, OgeFormLabelTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-form [(formData)]="ticket" labelLocation="start" [colCount]="1">
<oge-form-item field="title" label="Title" [isRequired]="true" />
<!-- replace only the editor; the form keeps the label, required mark
and error text, and hands you the id its <label for> points at -->
<oge-form-item field="rating" label="Rating">
<ng-template ogeFormLabelTemplate let-text>
<em>{{ text }}</em>
</ng-template>
<ng-template ogeFormEditorTemplate let-item let-editorId="editorId">
<input
type="range"
min="1"
max="5"
[id]="editorId"
[value]="ticket().rating"
(input)="setRating($any($event.target).value)"
/>
<span>{{ ticket().rating }} / 5</span>
</ng-template>
</oge-form-item>
</oge-form>
`,
})
export class Demo {
protected readonly ticket = signal({ title: '', rating: 3 });
protected setRating(value: string): void {
this.ticket.update((t) => ({ ...t, rating: Number(value) }));
}
}Actions & submit
Project the buttons into the ogeFormActions slot. Submitting marks every field touched, runs validation, emits the cancelable submitting event and — only if both pass — submitted. A failed submit reveals the summary and moves focus to the first invalid field.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeForm, OgeFormItem } from '@oge-ui/forms';
import { OgeButton } from '@oge-ui/buttons';
@Component({
selector: 'demo-root',
imports: [OgeForm, OgeFormItem, OgeButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-form
#form
[(formData)]="signup"
[showValidationSummary]="true"
(submitted)="saved.set(true)"
>
<oge-form-item field="email" label="E-mail" [isRequired]="true" />
<oge-form-item field="password" label="Password" [isRequired]="true" />
<div ogeFormActions>
<oge-button
text="Create"
stylingMode="contained"
buttonType="submit"
[useSubmitBehavior]="true"
/>
<oge-button text="Reset" (click)="form.reset()" />
</div>
</oge-form>
`,
})
export class Demo {
protected readonly signup = signal({ email: '', password: '' });
protected readonly saved = signal(false);
}