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 ProductMy product is the best`,});Key concepts
Section titled “Key concepts”useDisplayWidget(React) andDisplayWidgetAdapter(Angular / Lit) give you the mergedtemplateData(your custom props plus engine-managed values).- No
onValueChanged, no events — you just render. - 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”import * as Core from '@golemui/core';import { useDisplayWdiget } from '@golemui/react';import '@golemui/gui-components';
interface ProductDescriptionProps { img: string; description: string;}
export function ProductDescription(widgetInstance: Core.WithWidget) { const widget = widgetInstance.widget as Core.DisplayWidget; const { uid, templateData } = useDisplayWdiget<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> );}useDisplayWdiget returns just uid and templateData — the simplest hook of the four. Your custom props (img, description) are available directly on templateData.
Notice the <gui-markdown-text> custom element: import @golemui/gui-components once and you can use any of GolemUI’s web components in your JSX.
import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnDestroy, OnInit,} from '@angular/core';import * as Angular from '@golemui/angular';import * as Core from '@golemui/core';import '@golemui/gui-components';
interface ProductDescriptionProps { img: string; description: string;}
@Component({ standalone: true, selector: 'app-product-description', providers: [Angular.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, Core.WithWidget{ widget!: Core.DisplayWidget;
protected adapter: Angular.DisplayWidgetAdapter<ProductDescriptionProps> = inject(Angular.DisplayWidgetAdapter);
ngOnInit(): void { this.adapter.init(this.widget); }
ngOnDestroy(): void { this.adapter.destroy(); }}Since gui-markdown-text is a web component (not an Angular component), add CUSTOM_ELEMENTS_SCHEMA to the schemas array so Angular doesn’t complain about unknown elements.
import { html, LitElement } from 'lit';import { customElement } from 'lit/decorators.js';import { consume, provide } from '@lit/context';import { Subscription } from 'rxjs';import * as Core from '@golemui/core';import * as Lit from '@golemui/lit';import '@golemui/gui-components';
interface ProductDescriptionProps { img: string; description: string;}
@customElement('app-product-description')export class ProductDescriptionElement extends LitElement implements Core.WithWidget{ widget!: Core.DisplayWidget;
@consume({ context: Lit.formContext }) formContext!: Lit.LitFormContext<any>;
@provide({ context: Lit.displayWidgetContext }) adapter = new Lit.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()); }}In Lit, custom elements work natively — no schema or special config needed. Just import the package and use the tag.
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.
- Form Definition API / Custom Widgets —
gui.displays.custom(...).