Creating a layout widget
A layout widget is a container. It receives a children array from the form definition and is responsible for rendering them. The example we’ll walk through is productCard — a card container with a title header that wraps its children inside a styled card.
Widget definition
Section titled “Widget definition”import { gui } from '@golemui/gui-shared';
gui.layouts.custom( 'productCard', [ // child widgets go here ], { title: 'Product Card Name' },);{ "kind": "layout", "type": "productCard", "props": { "title": "Product Card Name" }, "children": [ /* child widgets go here */ ]}Implementation
Section titled “Implementation”Pick your framework — each tab walks through the key concepts and the full implementation side by side.
Key concepts (React)
useLayoutWidgetreturns the widget’s resolved children, already filtered byinclude/exclude, plus a mergedtemplateData(your custom props + engine-managed values likesize,lang,deps).- Render each child by handing it to
<WidgetRenderer />— that’s the React entry point that resolves a widget instance to its registered component. - The
style={{ flex: templateData.size }}line lets the layout participate inflexparents — see Sizing custom widgets for the rationale.
import type { LayoutWidget, NonFunctionWidget, WithWidget } from '@golemui/core';import { useLayoutWidget, WidgetRenderer } from '@golemui/react';
interface ProductCardProps { title: string;}
export function ProductCard(widgetInstance: WithWidget) { const widget = widgetInstance.widget as LayoutWidget; const { uid, children, templateData } = useLayoutWidget<ProductCardProps>(widget);
return ( <div className="product-card" style={{ flex: templateData.size }}> <div className="product-card__widget" id={uid}> <h2 className="product-card__title">{templateData.title}</h2> <div className="product-card__content"> {children.map((child) => { const w = child as NonFunctionWidget<string>; return <WidgetRenderer key={w.uid} widget={w} />; })} </div> </div> </div> );}Key concepts (Angular)
- Provide
LayoutWidgetAdapterin the component’sproviders, theninit()it inngOnInitanddestroy()it inngOnDestroy. The adapter exposes a reactivetemplateDatasignal. - Render children with the structural
guiWidgetdirective, which resolves and instantiates each child from the registry. - The host binding
'[style.flex]': 'this.adapter.templateData().size'lets the layout participate inflexparents — see Sizing custom widgets for the rationale.
import { Component, inject, OnDestroy, OnInit } from '@angular/core';import { LayoutWidgetAdapter, WidgetDirective } from '@golemui/angular';import type { LayoutWidget, WithWidget } from '@golemui/core';
interface ProductCardProps { title: string;}
@Component({ standalone: true, selector: 'app-product-card', imports: [WidgetDirective], providers: [LayoutWidgetAdapter], host: { class: 'product-card', '[style.flex]': 'this.adapter.templateData().size', }, template: ` @let templateData = adapter.templateData();
<div class="product-card__widget" [id]="widget.uid"> <h2 class="product-card__title">{{ templateData.title }}</h2> <div class="product-card__content"> @for (child of templateData.children; track child.uid) { <ng-container guiWidget [widget]="child" /> } </div> </div> `,})export class ProductCardComponent implements OnInit, OnDestroy, WithWidget{ widget!: LayoutWidget;
protected adapter: LayoutWidgetAdapter<ProductCardProps> = inject(LayoutWidgetAdapter);
ngOnInit(): void { this.adapter.init(this.widget); }
ngOnDestroy(): void { this.adapter.destroy(); }}Key concepts (Lit)
- Consume
formContextand providelayoutContextso child widgets can resolve through the registry. - Drive re-renders by subscribing to
adapter.templateDataChanged$and callingrequestUpdate(); clean up the subscription indisconnectedCallback. - Render children with
<gui-widget>and therepeatdirective for keyed list updates. - Adding
class="product-card"and bindingflextotemplateData.sizelets the layout participate inflexparents — see Sizing custom widgets for the rationale.
import { html, LitElement } from 'lit';import { customElement } from 'lit/decorators.js';import { consume, provide } from '@lit/context';import { repeat } from 'lit-html/directives/repeat.js';import { Subscription } from 'rxjs';import type { LayoutWidget, WithWidget } from '@golemui/core';import { formContext, layoutContext, LayoutWidgetAdapter, type LitFormContext,} from '@golemui/lit';
interface ProductCardProps { title: string;}
@customElement('app-product-card')export class ProductCardElement extends LitElement implements WithWidget{ widget!: LayoutWidget;
@consume({ context: formContext }) formContext!: LitFormContext<any>;
@provide({ context: layoutContext }) adapter = new LayoutWidgetAdapter<ProductCardProps>();
subscriptions: Subscription[] = [];
override createRenderRoot() { return this; }
override connectedCallback() { super.connectedCallback(); this.classList.add('product-card'); this.adapter.context = this.formContext; this.adapter.init(this.widget);
this.subscriptions.push( this.adapter.templateDataChanged$.subscribe(() => this.requestUpdate(), ), ); }
override render() { return html` <div class="product-card__widget" id=${this.widget.uid}> <h2 class="product-card__title"> ${this.adapter.templateData.title} </h2> <div class="product-card__content"> ${repeat( this.adapter.templateData.children || [], (child: any) => child.uid, (child: any) => html`<gui-widget .widget=${child}></gui-widget>`, )} </div> </div> `; }
override disconnectedCallback() { super.disconnectedCallback(); this.adapter.destroy(); this.subscriptions.forEach((s) => s.unsubscribe()); }}Key concepts (Vanilla JS)
- Extend
LitElementfromlit(already a transitive dependency via@golemui/lit). ConsumeformContextand providelayoutContextso child widgets can resolve through the registry. - Drive re-renders by subscribing to
adapter.templateDataChanged$and callingrequestUpdate(); clean up the subscription indisconnectedCallback. - Render children with
<gui-widget>and therepeatdirective for keyed list updates. - Register the class with
customElements.define('app-product-card', ProductCardElement)at the bottom of the file. - Adding
class="product-card"and bindingflextotemplateData.sizelets the layout participate inflexparents — see Sizing custom widgets for the rationale.
import { html, LitElement } from 'lit';import { consume, provide } from '@lit/context';import { repeat } from 'lit-html/directives/repeat.js';import { Subscription } from 'rxjs';import type { LayoutWidget, WithWidget } from '@golemui/core';import { formContext, layoutContext, LayoutWidgetAdapter, type LitFormContext,} from '@golemui/lit';
interface ProductCardProps { title: string;}
export class ProductCardElement extends LitElement implements WithWidget{ widget!: LayoutWidget;
@consume({ context: formContext }) formContext!: LitFormContext<any>;
@provide({ context: layoutContext }) adapter = new LayoutWidgetAdapter<ProductCardProps>();
subscriptions: Subscription[] = [];
override createRenderRoot() { return this; }
override connectedCallback() { super.connectedCallback(); this.classList.add('product-card'); this.adapter.context = this.formContext; this.adapter.init(this.widget);
this.subscriptions.push( this.adapter.templateDataChanged$.subscribe(() => this.requestUpdate(), ), ); }
override render() { return html` <div class="product-card__widget" id=${this.widget.uid}> <h2 class="product-card__title"> ${this.adapter.templateData.title} </h2> <div class="product-card__content"> ${repeat( this.adapter.templateData.children || [], (child: any) => child.uid, (child: any) => html`<gui-widget .widget=${child}></gui-widget>`, )} </div> </div> `; }
override disconnectedCallback() { super.disconnectedCallback(); this.adapter.destroy(); this.subscriptions.forEach((s) => s.unsubscribe()); }}
customElements.define('app-product-card', ProductCardElement);Result
Section titled “Result”Here’s the Product Card layout rendering its child widgets:
See also
Section titled “See also”- Custom Widgets Overview — the full Product Card example with all four kinds.
- Sizing custom widgets — why every widget reads
templateData.size. - Form Definition API / Custom Widgets —
gui.layouts.custom(...)reference. - Features / Widget Loaders — registering the loader.