Creating a display widget
A display widget renders read-only content. It has no data binding, no validation, and no events — just props. The example we’ll walk through is productDescription — a product image alongside a markdown description.
Widget definition
Section titled “Widget definition”import { gui } from '@golemui/gui-shared';
gui.displays.custom('productDescription', { img: 'assets/product-image.png', description: `### My Cool ProductThe declarative form engine.`,});{ "kind": "display", "type": "productDescription", "props": { "img": "assets/product-image.png", "description": "### GolemUI\nThe declarative form engine for JavaScript. Build complex, data-driven forms with zero boilerplate." }}For inline ad-hoc markup that doesn’t justify a full custom widget, use the Renderer widget instead.
This is a great place to show how you can use GolemUI’s library of custom elements inside your own widgets. The gui-markdown-text web component ships with @golemui/gui-components and renders markdown as HTML — no extra dependencies needed.
Implementation
Section titled “Implementation”Key concepts (React)
useDisplayWidgetis the simplest of the four hooks — it returns justuidand the mergedtemplateData(your custom props plus engine-managed values).- There’s no
onValueChanged, no events; you just render. - Importing
@golemui/gui-componentsonce unlocks every GolemUI custom element (like<gui-markdown-text>) inside your JSX. style={{ flex: templateData.size }}lets the display participate inflexparents — see Sizing custom widgets for the rationale.
import type { DisplayWidget, WithWidget } from '@golemui/core';import { useDisplayWidget } from '@golemui/react';import '@golemui/gui-components';
interface ProductDescriptionProps { img: string; description: string;}
export function ProductDescription(widgetInstance: WithWidget) { const widget = widgetInstance.widget as DisplayWidget; const { uid, templateData } = useDisplayWidget<ProductDescriptionProps>(widget);
return ( <div className="product-description" style={{ flex: templateData.size }}> <div className="product-description__widget" id={uid}> <img className="product-description__image" src={templateData.img} alt="Product" /> <gui-markdown-text md={templateData.description} /> </div> </div> );}Key concepts (Angular)
- Provide
DisplayWidgetAdapter, theninit()it inngOnInitanddestroy()it inngOnDestroy. The adapter exposes a reactivetemplateDatasignal. - Since
gui-markdown-textis a web component (not an Angular component), addCUSTOM_ELEMENTS_SCHEMAto theschemasarray so Angular doesn’t complain about unknown elements. - The host binding
'[style.flex]': 'this.adapter.templateData().size'lets the display participate inflexparents — see Sizing custom widgets for the rationale.
import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnDestroy, OnInit,} from '@angular/core';import { DisplayWidgetAdapter } from '@golemui/angular';import type { DisplayWidget, WithWidget } from '@golemui/core';import '@golemui/gui-components';
interface ProductDescriptionProps { img: string; description: string;}
@Component({ standalone: true, selector: 'app-product-description', providers: [DisplayWidgetAdapter], schemas: [CUSTOM_ELEMENTS_SCHEMA], host: { class: 'product-description', '[style.flex]': 'this.adapter.templateData().size', }, template: ` @let templateData = adapter.templateData();
<div class="product-description__widget" [id]="widget.uid"> <img class="product-description__image" [src]="templateData.img" alt="Product" /> <gui-markdown-text [md]="templateData.description"></gui-markdown-text> </div> `,})export class ProductDescriptionComponent implements OnInit, OnDestroy, WithWidget{ widget!: DisplayWidget;
protected adapter: DisplayWidgetAdapter<ProductDescriptionProps> = inject(DisplayWidgetAdapter);
ngOnInit(): void { this.adapter.init(this.widget); }
ngOnDestroy(): void { this.adapter.destroy(); }}Key concepts (Lit)
- Consume
formContextand providedisplayWidgetContextfor nested widget resolution. - Subscribe to
adapter.templateDataChanged$to driverequestUpdate(); clean up indisconnectedCallback. - In Lit, custom elements work natively — no schema or special config needed. Just import
@golemui/gui-componentsand use the tag. - Adding
class="product-description"and bindingflextotemplateData.sizelets the display 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 { Subscription } from 'rxjs';import type { DisplayWidget, WithWidget } from '@golemui/core';import { DisplayWidgetAdapter, displayWidgetContext, formContext, type LitFormContext,} from '@golemui/lit';import '@golemui/gui-components';
interface ProductDescriptionProps { img: string; description: string;}
@customElement('app-product-description')export class ProductDescriptionElement extends LitElement implements WithWidget{ widget!: DisplayWidget;
@consume({ context: formContext }) formContext!: LitFormContext<any>;
@provide({ context: displayWidgetContext }) adapter = new DisplayWidgetAdapter<ProductDescriptionProps>();
subscriptions: Subscription[] = [];
override createRenderRoot() { return this; }
override connectedCallback() { super.connectedCallback(); this.classList.add('product-description'); 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-description__widget" id=${this.widget.uid}> <img class="product-description__image" src=${this.adapter.templateData.img} alt="Product" /> <gui-markdown-text .md=${this.adapter.templateData.description} ></gui-markdown-text> </div> `; }
override disconnectedCallback() { super.disconnectedCallback(); this.adapter.destroy(); this.subscriptions.forEach((s) => s.unsubscribe()); }}Key concepts (Vue)
useDisplayWidgetis the simplest of the four composables — it returns justuidand the mergedtemplateDataref.- There’s no
onValueChanged, no events; you just render. - Importing
@golemui/gui-componentsonce unlocks every GolemUI custom element (like<gui-markdown-text>) inside your SFC template. Make sure your Vite plugin marksgui-*tags as custom elements (see Integration / Vue). :style="{ flex: templateData.size }"lets the display participate inflexparents — see Sizing custom widgets for the rationale.
<script setup lang="ts">import type { DisplayWidget, WithWidget } from '@golemui/core';import { useDisplayWidget } from '@golemui/vue';import '@golemui/gui-components';
interface ProductDescriptionProps { img: string; description: string;}
const props = defineProps<WithWidget>();const widget = props.widget as DisplayWidget;const { uid, templateData } = useDisplayWidget<ProductDescriptionProps>(widget);</script>
<template> <div class="product-description" :style="{ flex: templateData.size }"> <div class="product-description__widget" :id="uid"> <img class="product-description__image" :src="templateData.img" alt="Product" /> <gui-markdown-text :md.prop="templateData.description" /> </div> </div></template>Key concepts (Vanilla JS)
- Extend
LitElementfromlit(already a transitive dependency via@golemui/lit). ConsumeformContextand providedisplayWidgetContextso the form engine can resolve your widget through the registry. - Subscribe to
adapter.templateDataChanged$to driverequestUpdate(); clean up indisconnectedCallback. - Importing
@golemui/gui-componentsonce unlocks every GolemUI custom element (like<gui-markdown-text>) — no framework opt-in needed. - Register the class with
customElements.define('app-product-description', ProductDescriptionElement)at the bottom of the file. - Adding
class="product-description"and bindingflextotemplateData.sizelets the display participate inflexparents — see Sizing custom widgets for the rationale.
import { html, LitElement } from 'lit';import { consume, provide } from '@lit/context';import { Subscription } from 'rxjs';import type { DisplayWidget, WithWidget } from '@golemui/core';import { DisplayWidgetAdapter, displayWidgetContext, formContext, type LitFormContext,} from '@golemui/lit';import '@golemui/gui-components';
interface ProductDescriptionProps { img: string; description: string;}
export class ProductDescriptionElement extends LitElement implements WithWidget{ widget!: DisplayWidget;
@consume({ context: formContext }) formContext!: LitFormContext<any>;
@provide({ context: displayWidgetContext }) adapter = new DisplayWidgetAdapter<ProductDescriptionProps>();
subscriptions: Subscription[] = [];
override createRenderRoot() { return this; }
override connectedCallback() { super.connectedCallback(); this.classList.add('product-description'); 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-description__widget" id=${this.widget.uid}> <img class="product-description__image" src=${this.adapter.templateData.img} alt="Product" /> <gui-markdown-text .md=${this.adapter.templateData.description} ></gui-markdown-text> </div> `; }
override disconnectedCallback() { super.disconnectedCallback(); this.adapter.destroy(); this.subscriptions.forEach((s) => s.unsubscribe()); }}
customElements.define('app-product-description', ProductDescriptionElement);Result
Section titled “Result”Here’s the product description displaying an image alongside rendered markdown:
See also
Section titled “See also”- Renderer widget — for one-off bits of markup.
- 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.displays.custom(...).