Skip to content

Custom web components

Mateu can render any standard web component by declaring it as an Element in your ViewModel. This lets you embed third-party UI libraries, visualizations, or custom HTML elements without writing JavaScript or modifying the Mateu frontend.

When to use: reach for Element when Mateu’s built-in field types are not enough — 3D viewers, maps, rich text editors, chart libraries, or any component distributed as a web component.


Use Element.builder() to declare any HTML element or web component:

Element.builder()
.name("model-viewer")
.attributes(Map.of(
"src", src,
"auto-rotate", "auto-rotate",
"camera-controls", "camera-controls"
))
.style("width: 30rem; height: 30rem;")
.build()

This renders as:

<model-viewer
src="..."
auto-rotate
camera-controls
style="width: 30rem; height: 30rem;">
</model-viewer>

The name is the HTML tag name. attributes maps to HTML attributes. style sets inline CSS.


Custom web components often require a script to be loaded. Use UICommand.AddContentToHead to inject it:

@Override
public List<UICommand> commands(HttpRequest httpRequest) {
return List.of(
UICommand.builder()
.type(UICommandType.AddContentToHead)
.data(Element.builder()
.name("script")
.attributes(Map.of(
"id", "model-viewer-js",
"src", "https://ajax.googleapis.com/ajax/libs/model-viewer/3.0.1/model-viewer.min.js",
"type", "module"
))
.build())
.build()
);
}

This injects:

<script
id="model-viewer-js"
src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.0.1/model-viewer.min.js"
type="module">
</script>

Give the script element a stable id. Mateu uses the id to prevent duplicate injection if the page is rendered multiple times in the same session.


The .on() map on Element connects web component events to backend actions:

.on(Map.of(
"load", "model-loaded", // <model-viewer load> → handleAction("model-loaded")
"click", "model-clicked" // <model-viewer click> → handleAction("model-clicked")
))

When the web component fires the named event, Mateu sends it to the backend and calls handleAction with the matching action id.


Use OnValueChangeTrigger to call an action when a field’s value changes:

@Override
public List<Trigger> triggers(HttpRequest httpRequest) {
return List.of(
OnValueChangeTrigger.builder()
.propertyName("src")
.actionId("src-changed")
.build()
);
}

When src changes, handleAction("src-changed", ...) is called. The action can return a new state to re-render the component with the updated value.


This example wires together a radio selector, a <model-viewer> component, a head-injected script, and event listeners:

@Route(value = "/components/web-component", parentRoute = "")
public class WebComponentPage implements ComponentTreeSupplier, ActionHandler, CommandSupplier, TriggersSupplier {
String src = "/images/model-viewer/NeilArmstrong.glb";
@Override
public Form component(HttpRequest httpRequest) {
return Form.builder()
.title("Web component")
.content(List.of(
FormField.builder()
.id("src")
.dataType(FieldDataType.string)
.stereotype(FieldStereotype.radio)
.options(List.of(
new Option("/images/model-viewer/NeilArmstrong.glb", "Neil Armstrong"),
new Option("/images/model-viewer/ford_mustang_1965.glb", "Ford Mustang")
))
.build(),
Element.builder()
.name("model-viewer")
.attributes(Map.of(
"src", src,
"auto-rotate", "auto-rotate",
"camera-controls", "camera-controls"
))
.style("width: 30rem; height: 30rem;")
.on(Map.of(
"load", "model-loaded",
"click", "model-clicked"
))
.build()
))
.build();
}
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
if ("src-changed".equals(actionId)) {
return this; // re-render with new src value
}
if ("model-loaded".equals(actionId) || "model-clicked".equals(actionId)) {
return Message.builder().text(actionId).build();
}
return null;
}
@Override
public List<UICommand> commands(HttpRequest httpRequest) {
return List.of(
UICommand.builder()
.type(UICommandType.AddContentToHead)
.data(Element.builder()
.name("script")
.attributes(Map.of(
"id", "model-viewer-js",
"src", "https://ajax.googleapis.com/ajax/libs/model-viewer/3.0.1/model-viewer.min.js",
"type", "module"
))
.build())
.build()
);
}
@Override
public List<Trigger> triggers(HttpRequest httpRequest) {
return List.of(
OnValueChangeTrigger.builder()
.propertyName("src")
.actionId("src-changed")
.build()
);
}
}

FieldTypePurpose
nameStringHTML tag name
attributesMap<String, String>HTML attributes
onMap<String, String>Event name → action id mapping
contentStringInner HTML content
styleStringInline CSS
cssClassesStringCSS class names

  • Element = any HTML element or web component, declared in Java
  • attributes = standard HTML attributes
  • on = web component events routed to backend handlers
  • UICommandType.AddContentToHead = inject a <script> or other tag into <head>
  • OnValueChangeTrigger = re-render when a field changes
  • The backend stays in control; the web component is purely a renderer

  • Extensibility — override framework internals or embed micro-frontends
  • Rules — combine with @Hidden to show the element conditionally
  • Layout and composition — position the element within the page grid