Dynamic FieldArray
A realistic dynamic shopping cart built entirely with Preforms.
This example demonstrates how to use FieldArray, FormRow, OutputField for real-time calculations, and the powerful aggregates system to enforce business rules all declaratively.
Users can add up to 8 items, remove items, edit product names, prices, and quantities. Subtotals and the grand total update automatically. Validation prevents duplicate product names and ensures the cart total does not exceed $2000.
Features Demonstrated
- Dynamic array of items with add and remove buttons
- Per-item real-time subtotal calculation
- Grand total calculation using JavaScript expressions
- Cross-item validation using aggregates
- Clean tabular layout with FormRow
- Initial value seeding
How It Works
1 FieldArray
The FieldArray manages a dynamic list of cart items.
- minItems: 1 and maxItems: 8 enforce limits.
- addButton and removeButton provide built-in UI controls.
- value seeds the array with an initial item.
2. Per-Item Row (FormRow)
Each row contains:
- TextField for product name
- NumberField for price and quantity
- OutputField for subtotal (read-only)
The subtotal uses the $index placeholder to reference the current array item.
3. Real-Time Calculations
- Subtotal (inside array): Number(items[$index].price) * Number(items[$index].qty)
- Grand Total (outside array): Uses .reduce() over all items with the [*] wildcard to watch every price and quantity. The for property tells Preforms which fields should trigger recalculation.
4. Aggregates (Advanced Validation)
Aggregates let you define rules over the entire array without writing custom validators.
TypeScriptaggregates: [
{
action: 'product', // Special action: Σ(price × qty) across all items
field: ['price', 'qty'], // Fields to multiply per item
value: 2000,
operator: 'lte', // ≤
message: 'Cart total cannot exceed $2000'
},
{
action: 'unique',
field: 'name',
message: 'Product names must be unique'
}
];
Supported Aggregate Actions:
- "sum", "count", "avg" — standard numerical aggregations
- "product" — multiplies two fields per item then aggregates the results (ideal for price × quantity scenarios)
- "unique" — ensures all values in the specified field are distinct
Live Demo
Result
TS
SCSS
import { Component, EventEmitter, Output, ViewEncapsulation } from '@angular/core';
import { Preforms } from '@preforms/angular/core';
import { NATIVE_FORM_ELEMENTS } from '@preforms/angular/native';
import {
FieldArray,
FormRow,
TextField,
NumberField,
OutputField,
FormDivider,
SubmitButton,
FieldButton,
DialogField,
TextareaField,
Aggregate,
FormGrid,
} from '@preforms/ts';
@Component({
selector: 'app-cart-builder',
template: `
<preforms className="cart-form" (submittedData)="logData($event)" [fields]="fields" />
`,
styleUrl: './cart-builder.component.scss',
imports: [Preforms],
encapsulation: ViewEncapsulation.None,
providers: [NATIVE_FORM_ELEMENTS],
})
export class CartBuilderComponent {
@Output() formChange = new EventEmitter<any>();
fields = [
new FieldArray({
key: 'items',
label: 'Shopping Cart',
minItems: 1,
maxItems: 8,
addButton: true,
removeButton: true,
value: [{ name: 'Wireless Mouse', price: 25, qty: 1 }],
fields: [
new FormGrid({
className: 'cart-row',
gap: '1rem',
fields: [
new TextField({
key: 'name',
placeholder: 'Product name',
required: true,
}),
new NumberField({
key: 'price',
label: 'Price',
min: 1,
value: 0,
required: true,
}),
new NumberField({
key: 'qty',
label: 'Qty',
min: 1,
value: 1,
required: true,
}),
new FieldButton({
type: 'button',
label: 'Comment',
command: 'show-modal',
commandfor: 'comment-dialog[$index]',
className: 'comment-btn',
}),
new OutputField({
key: 'totalPrice',
calculation:
'"$"+(Number(items[$index].price) * Number(items[$index].qty)).toFixed(2)',
for: ['price[$index]', 'qty[$index]'],
className: 'subtotal',
}),
new DialogField({
id: 'comment-dialog[$index]',
fields: [
new TextareaField({
key: 'comment',
rows: 5,
}),
new FormRow([
new SubmitButton('Save'),
new FieldButton({
label: 'Close',
type: 'button',
command: 'close',
commandfor: 'comment-dialog[$index]',
}),
]),
],
}),
],
columns: '1fr 1fr 1fr 1fr',
}),
],
aggregates: [
// Special action: Σ(price × qty) across all items
Aggregate.product({
field: ['price', 'qty'], // Fields to multiply per item
value: 2000,
operator: 'lte',
message: 'total cannot exceed $2000',
}),
Aggregate.unique('name'),
],
}),
new FormDivider({ label: 'Summary' }),
new OutputField({
key: 'total',
calculation: `
"$" + items.reduce(
(sum, i) => sum + (Number(i.price) * Number(i.qty)),
0
)
`,
for: ['price[*]', 'qty[*]', 'items'],
}),
new SubmitButton({
label: 'Checkout',
className: 'checkout-btn',
}),
];
logData(data: any) {
this.formChange.emit(data);
}
}