Rent a car: Submitting the form
The Reserve button uses actionType: 'submit'. On click the form runs every validator; if all pass, <gui-form> dispatches a formSubmit event whose payload is a FormSubmitEvent carrying the collected form data. The host listens for that event — the form definition itself needs nothing more than the button shape.
Step 1 — The submit button
Section titled “Step 1 — The submit button”The button stays as it was in the previous chapters — actionType: 'submit' is all the wiring you need on the widget side:
gui.actions.button({ label: 'Reserve', actionType: 'submit',}),{ "kind": "action", "type": "button", "label": "Reserve", "actionType": "submit"}Validation gates submission for free: an invalid form simply surfaces the error messages from the validation chapter and never fires formSubmit.
Step 2 — Listen for it
Section titled “Step 2 — Listen for it”Add a handleFormSubmit handler to <gui-form>’s formSubmit output, alongside the existing formEvent listener from the previous chapter. The handler receives a FormSubmitEvent whose .data is the collected form data. The snippets below import the JSON form file (./rent-a-car.form.json); if you’re using the programmatic API, swap the import path for ./rent-a-car.form (the .ts file):
import { GuiForm } from '@golemui/gui-react';import type { FormEvent, FormSubmitEvent } from '@golemui/core';import { searchCars } from './car.service';import rentACarForm from './rent-a-car.form.json';import { CarItemRenderer } from './CarItemRenderer';
const config = { formDef: rentACarForm, // …itemRenderers (and `formConfig.states` if you're on the programmatic API) from earlier chapters…};
async function handleFormEvent(event: FormEvent) { // …loadCars / filterCars handling from the previous chapter…}
function handleFormSubmit(event: FormSubmitEvent) { console.log(event.data);}
export function RentACarPage() { return ( <GuiForm config={config} formEvent={handleFormEvent} formSubmit={handleFormSubmit} /> );}import { Component } from '@angular/core';import { CommonModule } from '@angular/common';import { FormComponent } from '@golemui/gui-angular';import type { FormEvent, FormSubmitEvent } from '@golemui/core';import { searchCars } from './car.service';import rentACarForm from './rent-a-car.form.json';import { CarItemRenderer } from './car-item-renderer.component';
@Component({ imports: [CommonModule, FormComponent], selector: 'app-rent-a-car', template: ` <gui-form [config]="config" (formEvent)="handleFormEvent($event)" (formSubmit)="handleFormSubmit($event)" ></gui-form> `,})export class RentACarPage { protected config = { formDef: rentACarForm, // …itemRenderers (and `formConfig.states` if you're on the programmatic API) from earlier chapters… };
async handleFormEvent(event: FormEvent) { // …loadCars / filterCars handling from the previous chapter… }
handleFormSubmit(event: FormSubmitEvent) { console.log(event.data); }}import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-lit';import type { FormEvent, FormSubmitEvent } from '@golemui/core';import { searchCars } from './car.service';import { carItemRenderer } from './car-item-renderer';import rentACarForm from './rent-a-car.form.json';
@customElement('rent-a-car-page')export class RentACarPage extends LitElement { config = { formDef: rentACarForm, // …itemRenderers (and `formConfig.states` if you're on the programmatic API) from earlier chapters… };
async handleFormEvent(event: FormEvent) { // …loadCars / filterCars handling from the previous chapter… }
handleFormSubmit(event: FormSubmitEvent) { console.log(event.data); }
override createRenderRoot() { return this; }
override render() { return html` <gui-form .config=${this.config} @formEvent=${(e: CustomEvent<FormEvent>) => this.handleFormEvent(e.detail)} @formSubmit=${(e: CustomEvent<FormSubmitEvent>) => this.handleFormSubmit(e.detail)} ></gui-form> `; }}<script setup lang="ts">import { GuiForm } from '@golemui/gui-vue';import type { FormEvent, FormSubmitEvent } from '@golemui/core';import { searchCars } from './car.service';import rentACarForm from './rent-a-car.form.json';import CarItemRenderer from './CarItemRenderer.vue';
const config = { formDef: rentACarForm, // …itemRenderers (and `formConfig.states` if you're on the programmatic API) from earlier chapters…};
async function handleFormEvent(event: FormEvent) { // …loadCars / filterCars handling from the previous chapter…}
function handleFormSubmit(event: FormSubmitEvent) { console.log(event.data);}</script>
<template> <GuiForm :config="config" @form-event="handleFormEvent" @form-submit="handleFormSubmit" /></template>import '@golemui/gui-components/index.css';import '@golemui/gui-lit';import { searchCars } from './car.service';import { carItemRenderer } from './car-item-renderer';import rentACarForm from './rent-a-car.form.json';
const config = { formDef: rentACarForm, // …itemRenderers (and `formConfig.states` if you're on the programmatic API) from earlier chapters…};
async function handleFormEvent(event) { // …loadCars / filterCars handling from the previous chapter…}
function handleFormSubmit(event) { console.log(event.data);}
const form = document.getElementById('app-form');form.config = config;form.addEventListener('formEvent', (e) => handleFormEvent(e.detail));form.addEventListener('formSubmit', (e) => handleFormSubmit(e.detail));Result
Section titled “Result”import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.dropdown('car', { labelField: 'label', valueField: 'id', itemRenderer: 'carItemRenderer', inputDebounce: 300, label: 'Select car', validator: { type: 'string', required: true, messages: { required: 'Please pick a car model', invalid: 'Please pick a car model', }, }, onLoad: 'loadCars', onFilter: 'filterCars', }), gui.layouts.grid([ gui.inputs.dropdown('collectOffice', { labelField: 'label', valueField: 'id', items: [ { id: 'lhr', label: 'London Heathrow', }, { id: 'cdg', label: 'Paris CDG', }, { id: 'fra', label: 'Frankfurt Main', }, ], label: 'Collect from office', validator: { type: 'string', required: true, messages: { required: 'Choose where you\'ll pick up the car', }, }, }), gui.inputs.dropdown('returnOffice', { labelField: 'label', valueField: 'id', items: [ { id: 'lhr', label: 'London Heathrow', }, { id: 'cdg', label: 'Paris CDG', }, { id: 'fra', label: 'Frankfurt Main', }, ], label: 'Return to office', validator: { type: 'string', required: true, messages: { required: 'Choose where you\'ll drop the car off', }, }, include: { in: ['differentReturn'], }, }), ], { direction: 'row', autoFit: true, }), gui.inputs.booleanInput('differentReturn', { label: 'Choose a different return location', }), gui.inputs.rangeCalendar('rentalDates', { numberOfMonths: 2, label: 'Rental dates', validator: { required: true, minItems: 1, maxItems: 1, messages: { required: 'Please select your rental dates', minItems: 'Pick a rental date range', maxItems: 'Only one date range, please', }, }, }), gui.inputs.radiogroup('rentalType', { options: [ { label: 'Daily', value: 'daily', }, { label: 'Weekly', value: 'weekly', }, { label: 'Monthly', value: 'monthly', }, ], label: 'Rental type', validator: { type: 'string', required: true, messages: { required: 'Choose Daily, Weekly, or Monthly', }, }, }), gui.inputs.booleanInput('driverOver25', { label: 'Driver aged over 25', validator: { const: true, required: true, messages: { const: 'Drivers must be at least 25 years old to rent', required: 'Confirm the driver is over 25', }, }, }), gui.inputs.booleanInput('hasDiscountCode', { label: 'I have a discount code', }), gui.inputs.textInput('discountCode', { label: 'Discount code', validator: { required: true, minLength: 4, messages: { required: 'Enter your discount code', minLength: 'Discount codes are at least 4 characters', }, }, include: { in: ['hasDiscount'], }, }), gui.actions.button({ label: 'Reserve', actionType: 'submit', }),];{ "states": { "differentReturn": "$form.differentReturn === true", "hasDiscount": "$form.hasDiscountCode === true" }, "form": [ { "kind": "input", "type": "dropdown", "path": "car", "label": "Select car", "validator": { "type": "string", "required": true, "messages": { "required": "Please pick a car model", "invalid": "Please pick a car model" } }, "on": { "load": "loadCars", "filter": "filterCars" }, "props": { "labelField": "label", "valueField": "id", "itemRenderer": "carItemRenderer", "inputDebounce": 300 } }, { "kind": "layout", "type": "grid", "props": { "direction": "row", "autoFit": true }, "children": [ { "kind": "input", "type": "dropdown", "path": "collectOffice", "label": "Collect from office", "validator": { "type": "string", "required": true, "messages": { "required": "Choose where you'll pick up the car" } }, "props": { "labelField": "label", "valueField": "id", "items": [ { "id": "lhr", "label": "London Heathrow" }, { "id": "cdg", "label": "Paris CDG" }, { "id": "fra", "label": "Frankfurt Main" } ] } }, { "kind": "input", "type": "dropdown", "path": "returnOffice", "label": "Return to office", "include": { "in": ["differentReturn"] }, "validator": { "type": "string", "required": true, "messages": { "required": "Choose where you'll drop the car off" } }, "props": { "labelField": "label", "valueField": "id", "items": [ { "id": "lhr", "label": "London Heathrow" }, { "id": "cdg", "label": "Paris CDG" }, { "id": "fra", "label": "Frankfurt Main" } ] } } ] }, { "kind": "input", "type": "toggle", "path": "differentReturn", "label": "Choose a different return location" }, { "kind": "input", "type": "rangeCalendar", "path": "rentalDates", "label": "Rental dates", "props": { "numberOfMonths": 2 }, "validator": { "type": "array", "required": true, "minItems": 1, "maxItems": 1, "messages": { "required": "Please select your rental dates", "minItems": "Pick a rental date range", "maxItems": "Only one date range, please" } } }, { "kind": "input", "type": "radiogroup", "path": "rentalType", "label": "Rental type", "validator": { "type": "string", "required": true, "messages": { "required": "Choose Daily, Weekly, or Monthly" } }, "props": { "options": [ { "label": "Daily", "value": "daily" }, { "label": "Weekly", "value": "weekly" }, { "label": "Monthly", "value": "monthly" } ] } }, { "kind": "input", "type": "toggle", "path": "driverOver25", "label": "Driver aged over 25", "validator": { "type": "boolean", "const": true, "required": true, "messages": { "const": "Drivers must be at least 25 years old to rent", "required": "Confirm the driver is over 25" } } }, { "kind": "input", "type": "toggle", "path": "hasDiscountCode", "label": "I have a discount code" }, { "kind": "input", "type": "textinput", "path": "discountCode", "label": "Discount code", "include": { "in": ["hasDiscount"] }, "validator": { "type": "string", "required": true, "minLength": 4, "messages": { "required": "Enter your discount code", "minLength": "Discount codes are at least 4 characters" } } }, { "kind": "action", "type": "button", "label": "Reserve", "actionType": "submit" } ]}Fill the form, click Reserve, and the collected form data arrives in your handleFormSubmit callback.
Wrapping up
Section titled “Wrapping up”You’ve now built a real-world form that uses:
- declarative widget composition with
gridandflexlayouts, - validators per field,
- named states with
includerules, - a custom item renderer,
- async data loading with
onLoad/onFilterandinputDebounce, - the form’s
formSubmitevent to capture submission and read the collected data.
Where next?
Section titled “Where next?”- Features — deep dives into every cross-cutting capability.
- Form Definition API — the canonical JSON schema reference (and the matching
gui.*programmatic shortcuts). - Extending GolemUI — building your own widgets.
- Widgets Reference — every built-in widget.