Understanding Components
Learn how Claspo widget components work, from Web Component fundamentals to editor integration.
This guide walks you through creating a basic text editing component. Along the way, you'll learn how to add editor controls for size, margins, and font parameters.
Before diving in, follow the Getting Started guide to set up the project and run the development server.
Component Directory and Main File
Every component lives in the components directory at the project root. Component directories follow PascalCase naming with a "Component" suffix.
For a text component named MyTextComponent, create:
components/
MyTextComponent/
MyTextComponent.js
The JavaScript file inside the directory shares the same name as the component—this is your main file.
All widget components are Web Components with Shadow DOM for encapsulation. To access the SDK, extend from WcElement rather than the standard HTMLElement:
import WcElement from '@claspo/renderer/sdk/WcElement';
export default class MyTextComponent extends WcElement {
}Components need HTML markup. Add static HTML and styles inside the constructor:
constructor() {
super();
this.getRootElement().innerHTML = `
<style>
.text {
outline: none;
min-height: fit-content;
width: 100%;
overflow-wrap: break-word;
}
</style>
<div class="text">Static text</div>
`;
}The getRootElement() method comes from WcElement and returns the ShadowRoot—the container for your component's content. This isolation keeps your styles and markup separate from the page and other components.
The Component Manifest
The manifest tells the Widget Display SDK how to render the component and tells the Editor how to add, update, and customize it.
Start with basic information:
const manifest = {
name: 'MyTextComponent',
componentType: 'TEXT',
version: '1.0.0',
props: {},
metaDescription: {
icon: 'https://cdn.claspo.io/bundled-components/SysTextComponent/assets/img/text-component-icon.svg',
label: {
en: 'My Text',
}
},
};
export default class MyTextComponent extends WcElement {
static define = {
name: 'sys-text',
model: manifest.name,
manifest: manifest,
}
manifest = manifest;
}The manifest's name field must match your directory and main file name. The metaDescription controls how the component appears in the Available Components panel (the left panel by default).
You can define the manifest directly in your main file, but we recommend a separate file for easier maintenance.
Building Your First Component
Open claspo.config.js in the project root. Add MyTextComponent to the useComponents array, then restart the development server.
Adding the Component to the Editor
Open quickstart/frontend/src/config/components-panel.ts. This file lists available components grouped by categories.
You can add your component to any existing group, but for easy verification, create a new category called "My Components" as the first group:
{
label: 'My Components',
components: [
{ componentName: 'MyTextComponent' }
]
}Navigate to the http://localhost:4202/editor.html and verify that the new group appears in the panel. Drag and drop the component into the widget—you should see "Static text".
Text Editing
The most convenient way to edit text is directly in the editor's viewport by double-clicking. Components typically have properties that control their behavior and appearance through the props field.
Add a content object to the manifest's props:
props: {
content: {
text: 'Dynamic text'
}
}Now replace the static text with dynamic binding from props. Web components have a hook for DOM insertion—use it to get current props and subscribe to future updates:
import insertHtmlIntoElement from '@claspo/common/dom/insertHtmlIntoElement';
export default class MyTextComponent extends WcElement {
connectedCallback() {
super.connectedCallback();
this.observeProps((prev, next) => {
insertHtmlIntoElement({
element: this.getRootElement().querySelector('.text'),
html: next.content.text,
});
});
}
}The Editor provides either plain text or HTML code. The insertHtmlIntoElement utility safely inserts HTML into DOM nodes, preventing XSS vulnerabilities.
Reload the page and add the component to the widget. You'll see "Dynamic text" instead of "Static text".
To enable text editing on the <div> element, add the cl-inline-edit="content, text" attribute. The value is a comma-separated prop path:
<div class="text" cl-inline-edit="content, text"></div>Users can now double-click the text to edit it directly in the viewport.
Styling
To control the component's size and appearance, update props with defaults for size, margins, and text parameters:
props: {
content: {
text: 'Dynamic text'
},
adaptiveStyles: {
desktop: [
{
element: "host",
styleAttributes: {
width: "100%",
minWidth: null,
height: "auto",
minHeight: null,
marginTop: "0px",
marginBottom: "0px",
marginLeft: "0px",
marginRight: "0px",
_marginEnabled: false
}
},
{
element: "text",
styleAttributes: {
color: "rgb(50, 66, 67)",
textAlign: "center",
lineHeight: "120%",
fontWeight: "500",
fontSize: "16px",
textShadow: "none"
}
}
],
mobile: [
{
element: "host",
styleAttributes: {
width: "100%",
minWidth: null,
height: "auto",
minHeight: null,
marginTop: "0px",
marginBottom: "0px",
marginLeft: "0px",
marginRight: "0px",
_marginEnabled: false
}
},
{
element: "text",
styleAttributes: {
color: "rgb(50, 66, 67)",
textAlign: "center",
lineHeight: "120%",
fontWeight: "500",
fontSize: "16px",
textShadow: "none"
}
}
]
}
}Here's what this structure provides:
The adaptiveStyles prop handles responsive design by defining separate settings for desktop and mobile environments. The SDK automatically selects the appropriate style set based on the runtime environment.
Each environment contains elements with their style attributes:
- host: The component itself. This element is implicit—every component has it automatically.
- text: A user-defined element pointing to a specific DOM node.
Mark your text node with the cl-element attribute, where the value matches the element name in adaptiveStyles:
<div class="text" cl-inline-edit="content, text" cl-element="text"></div>To apply adaptive styles, update the observeProps callback:
this.observeProps((prev, next) => {
this.applyAutoAdaptiveStyles(next.adaptiveStyles);
insertHtmlIntoElement({
element: this.getRootElement().querySelector('.text'),
html: next.content.text,
});
});Viewport Controls
Beyond text editing, the Editor provides several viewport controls for widget components. Add size and margin controls through the floatingControlsModel field in the manifest:
floatingControlsModel: [
{
type: "GROUP",
propPath: ["adaptiveStyles", "desktop"],
children: [
{
type: "CONTROL",
name: "SIZE",
elementProp: "styleAttributes",
element: "host"
},
{
type: "CONTROL",
name: "MARGIN",
elementProp: "styleAttributes",
element: "host"
}
]
},
{
type: "GROUP",
propPath: ["adaptiveStyles", "mobile"],
children: [
{
type: "CONTROL",
name: "SIZE",
elementProp: "styleAttributes",
element: "host"
},
{
type: "CONTROL",
name: "MARGIN",
elementProp: "styleAttributes",
element: "host"
}
]
}
]This defines control sets for both desktop and mobile environments. Click your component in the editor to try the new controls.
For all floating control options, see Floating Controls Reference.
Property Pane
The property pane (the right panel by default) gives users full control over size, margins, and text styling. Add the propertyPaneModel field to your manifest:
propertyPaneModel: {
content: [
{
type: "GROUP",
propPath: ["adaptiveStyles", "desktop"],
children: [
{
type: "CONTROL",
name: "SIZE",
element: "host",
elementProp: "styleAttributes",
params: {
width: {
options: ["fixed", "fill", "hug"]
},
height: {
options: ["fixed", "hug"]
}
}
},
{
type: "CONTROL",
name: "INDENTATION",
elementProp: "styleAttributes",
element: "host",
params: {
indentationType: "MARGIN"
}
},
{
type: "CONTROL",
name: "TEXT_PARAMS",
params: [
{
element: "text",
isLineSpaceAvailable: true,
isTextTransformAvailable: true
}
]
}
]
},
{
type: "GROUP",
propPath: ["adaptiveStyles", "mobile"],
children: [
{
type: "CONTROL",
name: "SIZE",
element: "host",
elementProp: "styleAttributes",
params: {
width: {
options: ["fixed", "fill", "hug"]
},
height: {
options: ["fixed", "hug"]
}
}
},
{
type: "CONTROL",
name: "INDENTATION",
elementProp: "styleAttributes",
element: "host",
params: {
indentationType: "MARGIN"
}
},
{
type: "CONTROL",
name: "TEXT_PARAMS",
params: [
{
element: "text",
isLineSpaceAvailable: true,
isTextTransformAvailable: true
}
]
}
]
}
]
}Property pane controls support various customization options. In this example, we restrict the height control to exclude the "fill" option, while the width control keeps all three options available.
For all property pane options, see Property Pane Reference.
Context Menu
Power users benefit from context menus that appear on right-click. Add copy-paste actions and a button to navigate the component tree through contextMenuModel:
contextMenuModel: [
{
type: "GROUP",
propPath: ["adaptiveStyles", "desktop"],
children: [
{
type: "CONTROL",
name: "COMPONENT_OPERATIONS"
},
{
type: "CONTROL",
name: "FOCUS_PARENT_COMPONENT"
}
]
},
{
type: "GROUP",
propPath: ["adaptiveStyles", "mobile"],
children: [
{
type: "CONTROL",
name: "COMPONENT_OPERATIONS"
},
{
type: "CONTROL",
name: "FOCUS_PARENT_COMPONENT"
}
]
}
]COMPONENT_OPERATIONS provides copy, paste, and delete functionality. FOCUS_PARENT_COMPONENT lets users navigate up the component tree.
For all context menu options, see Context Menu Reference.
Internationalization (i18n)
Widgets often need multiple language versions. The i18nPropPaths array tells the Editor which props support translation:
i18nPropPaths: [
"content,text"
]Each path uses comma separation to specify the nested prop location.
To enable the language selector in the Editor, add disableLanguageSelect: false to quickstart/frontend/src/config/editor-config.ts.
For all internationalization options, see Internationalization Guide.
Complete Example
import WcElement from '@claspo/renderer/sdk/WcElement';
import insertHtmlIntoElement from '@claspo/common/dom/insertHtmlIntoElement';
const manifest = {
name: 'MyTextComponent',
componentType: 'TEXT',
version: '1.0.0',
props: {
content: {
text: 'Dynamic text'
},
adaptiveStyles: {
desktop: [
{
element: "host",
styleAttributes: {
width: "100%",
minWidth: null,
height: "auto",
minHeight: null,
marginTop: "0px",
marginBottom: "0px",
marginLeft: "0px",
marginRight: "0px",
_marginEnabled: false
}
},
{
element: "text",
styleAttributes: {
color: "rgb(50, 66, 67)",
textAlign: "center",
lineHeight: "120%",
fontWeight: "500",
fontSize: "16px",
textShadow: "none"
}
}
],
mobile: [
{
element: "host",
styleAttributes: {
width: "100%",
minWidth: null,
height: "auto",
minHeight: null,
marginTop: "0px",
marginBottom: "0px",
marginLeft: "0px",
marginRight: "0px",
_marginEnabled: false
}
},
{
element: "text",
styleAttributes: {
color: "rgb(50, 66, 67)",
textAlign: "center",
lineHeight: "120%",
fontWeight: "500",
fontSize: "16px",
textShadow: "none"
}
}
]
}
},
floatingControlsModel: [
{
type: "GROUP",
propPath: ["adaptiveStyles", "desktop"],
children: [
{ type: "CONTROL", name: "SIZE", elementProp: "styleAttributes", element: "host" },
{ type: "CONTROL", name: "MARGIN", elementProp: "styleAttributes", element: "host" }
]
},
{
type: "GROUP",
propPath: ["adaptiveStyles", "mobile"],
children: [
{ type: "CONTROL", name: "SIZE", elementProp: "styleAttributes", element: "host" },
{ type: "CONTROL", name: "MARGIN", elementProp: "styleAttributes", element: "host" }
]
}
],
propertyPaneModel: {
content: [
{
type: "GROUP",
propPath: ["adaptiveStyles", "desktop"],
children: [
{
type: "CONTROL",
name: "SIZE",
element: "host",
elementProp: "styleAttributes",
params: {
width: { options: ["fixed", "fill", "hug"] },
height: { options: ["fixed", "hug"] }
}
},
{
type: "CONTROL",
name: "INDENTATION",
elementProp: "styleAttributes",
element: "host",
params: { indentationType: "MARGIN" }
},
{
type: "CONTROL",
name: "TEXT_PARAMS",
params: [{ element: "text", isLineSpaceAvailable: true, isTextTransformAvailable: true }]
}
]
},
{
type: "GROUP",
propPath: ["adaptiveStyles", "mobile"],
children: [
{
type: "CONTROL",
name: "SIZE",
element: "host",
elementProp: "styleAttributes",
params: {
width: { options: ["fixed", "fill", "hug"] },
height: { options: ["fixed", "hug"] }
}
},
{
type: "CONTROL",
name: "INDENTATION",
elementProp: "styleAttributes",
element: "host",
params: { indentationType: "MARGIN" }
},
{
type: "CONTROL",
name: "TEXT_PARAMS",
params: [{ element: "text", isLineSpaceAvailable: true, isTextTransformAvailable: true }]
}
]
}
]
},
contextMenuModel: [
{
type: "GROUP",
propPath: ["adaptiveStyles", "desktop"],
children: [
{ type: "CONTROL", name: "COMPONENT_OPERATIONS" },
{ type: "CONTROL", name: "FOCUS_PARENT_COMPONENT" }
]
},
{
type: "GROUP",
propPath: ["adaptiveStyles", "mobile"],
children: [
{ type: "CONTROL", name: "COMPONENT_OPERATIONS" },
{ type: "CONTROL", name: "FOCUS_PARENT_COMPONENT" }
]
}
],
i18nPropPaths: ["content,text"],
metaDescription: {
icon: 'https://cdn.claspo.io/bundled-components/SysTextComponent/assets/img/text-component-icon.svg',
label: { en: 'My Text' }
}
};
export default class MyTextComponent extends WcElement {
static define = {
name: 'sys-text',
model: manifest.name,
manifest: manifest
}
manifest = manifest;
constructor() {
super();
this.getRootElement().innerHTML = `
<style>
.text {
outline: none;
min-height: fit-content;
width: 100%;
overflow-wrap: break-word;
}
</style>
<div class="text" cl-inline-edit="content, text" cl-element="text"></div>
`;
}
connectedCallback() {
super.connectedCallback();
this.observeProps((prev, next) => {
this.applyAutoAdaptiveStyles(next.adaptiveStyles);
insertHtmlIntoElement({
element: this.getRootElement().querySelector('.text'),
html: next.content.text
});
});
}
}Updated 1 day ago
More examples available https://github.com/Claspo/claspo-plugin-starter/tree/main/examples
