qodly-custom-component

v1.0.0
localskills install qodly/qodly-custom-component-1
0 downloads
Created Apr 3, 2026
Metwalli ayoub
SKILL.md

Qodly Custom Component

Guidance for creating and developing Custom Components for Qodly Studio—React-based building blocks that extend Qodly Pages.

Companion skills (same repository)

When this skill is installed from the Qodly skills bundle (or you copied sibling folders into .cursor/skills/):

  • qodly-docs — Read markdown/mdx under qodly-docs/ for Qodly Pro reference: built-in components, page loaders, events, datasources, localization, roles, and custom-component integration topics.
  • qodly-pages — Use qodly-pages/ when authoring web pages (.WebForm JSON), JSON schemas, craft-component catalogs, and Studio page/i18n rules—not for React custom component code.

Overview

Custom Components are user-created React components for Qodly Studio. They use @ws-ui/webform-editor, Module Federation for runtime loading, and Vite for builds. Official docs: developer.4d.com/qodly/Integrations/customComponent/overview.

Initialize a New Component Project

From the directory where the project should live (or run interactively and choose the target path):

npx @qodly/cli init

This is the supported way to create a new custom component project. Do not rely on custom shell scripts for setup—the CLI provisions the full layout.

Add another component inside an existing project (after init):

npx @qodly/cli new component
# or, if @qodly/cli is installed locally: npm run generate:component

Then ensure the new component is exported from src/components/index.tsx following the pattern the CLI generated for existing components.

npx @qodly/cli init produces a project with:

  • package.json (app_id, name, @qodly/cli, @ws-ui/* peer deps)
  • vite.config.ts (Module Federation, Monaco editor, proxy)
  • proxy.config.ts (dev proxy to Qodly server)
  • src/main.tsx, src/App.tsx, src/components/index.tsx

Project Structure

component-name/
├── package.json          # app_id, name, @qodly/cli, @ws-ui/* peerDependencies
├── vite.config.ts       # federation, proxy, Monaco/standalone plugins
├── proxy.config.ts      # initProxy(env) for /rest, /api, /$lib, etc.
├── src/
│   ├── main.tsx         # ReactDOM.render(<App />)
│   ├── App.tsx          # <WebformEditorStandalone userComponents={components} />
│   ├── components/
│   │   ├── index.tsx    # export default { Component1, Component2, ... }
│   │   └── MyComponent/
│   │       ├── index.tsx       # T4DComponent wrapper (Build vs Render)
│   │       ├── MyComponent.config.tsx
│   │       ├── MyComponent.settings.ts
│   │       ├── MyComponent.build.tsx   # Editor canvas (mock data)
│   │       └── MyComponent.render.tsx  # Runtime (real datasource)

Component File Roles

FilePurpose
*.config.tsxT4DComponentConfig: craft, info, defaultProps, datasources.accept
*.settings.tsTSetting[] for Studio property panel (ESetting.TEXT_FIELD, I18NFIELD, DATAGRID, etc.)
*.build.tsxEditor mode: useEnhancedNode, mock/static data for canvas
*.render.tsxRuntime mode: useRenderer, useSources, real datasource binding
index.tsxWraps Build/Render: enabled ? <Build /> : <Render />

Key Patterns

Config (T4DComponentConfig)

import { EComponentKind, T4DComponentConfig } from '@ws-ui/webform-editor';
import { Settings } from '@ws-ui/webform-editor';

export default {
  craft: {
    displayName: 'MyComponent',
    kind: EComponentKind.BASIC,
    props: { name: '', classNames: [], events: [] },
    related: { settings: Settings(MySettings, BasicSettings) },
    sanityCheck: { keys: [{ name: 'datasource', require: true, isDatasource: true }] },
    requiredFields: { keys: ['datasource'], all: false },
  },
  info: {
    displayName: 'MyComponent',
    exposed: true,
    icon: MyIcon,
    events: [{ label: 'On Click', value: 'onclick' }, ...],
    datasources: { accept: ['entitysel'] },  // or ['array'], etc.
  },
  defaultProps: { style: { height: '200px' }, ... },
} as T4DComponentConfig<IProps>;

Build vs Render

  • Build: useEnhancedNode(), connect, no real datasource. Use for drag-and-drop canvas.
  • Render: useRenderer(), useSources(), ds.getValue(), ds.addListener('changed', ...). Use for runtime with Qodly data.

Internationalization (i18n)

Settings (*.settings.ts) — Import getStaticFeaturesExperimentalFlag from @ws-ui/webform-editor, then use ESetting.I18NFIELD when i18n is enabled:

import { ESetting, getStaticFeaturesExperimentalFlag } from '@ws-ui/webform-editor';

const isI18nEnabled = getStaticFeaturesExperimentalFlag('i18n');

// Example setting entry:
// type: isI18nEnabled ? ESetting.I18NFIELD : ESetting.TEXT_FIELD,

Render / labels — Resolve translated strings with useI18n():

import { useI18n } from '@ws-ui/webform-editor';

const { i18n } = useI18n();
const lang = i18n?.userLang?.primary ?? 'de';
const entry = i18n?.keys?.[yourKeyName];
const value = entry?.[lang] ?? entry?.de;

See references/i18n.md for full notes.

Index Wrapper

const MyComponent: T4DComponent<IProps> = (props) => {
  const { enabled } = useEnhancedEditor((state) => ({ enabled: state.options.enabled }));
  return enabled ? <Build {...props} /> : <Render {...props} />;
};
MyComponent.craft = config.craft;
MyComponent.info = config.info;
MyComponent.defaultProps = config.defaultProps;

package.json Essentials

  • app_id: Unique ID (e.g. qodly_431f853d4e15946ba36f)
  • name: Component package name
  • Scripts: dev, build (qodly build), generate:component (qodly new component)
  • Peer deps: @ws-ui/code-editor, @ws-ui/icons, @ws-ui/shared, @ws-ui/store
  • Dependencies: @ws-ui/webform-editor, @ws-ui/craftjs-core, react, react-dom

Vite & Proxy

  • proxy.config.ts: Proxies /rest, /api, /$lib, /login.html, etc. to PROXY_SERVER (default https://127.0.0.1:7443).
  • Env: PROXY_SERVER, API_SECURE, API_KEY, PORT, HOST.
  • Module Federation: exposes ./components as components.js for Qodly Studio.

Development

npm run dev

Runs Vite dev server with proxy. Connect Qodly Studio to the component URL for local development.

Build & Install in Qodly

npm run build

Produces federated bundle. Install the built component in Qodly Studio per Custom Components for Qodly Studio.

Complete Example (Pie Chart)

index.tsx — Build vs Render switch:

const Pie: T4DComponent<IPieProps> = (props) => {
  const { enabled } = useEnhancedEditor((state) => ({ enabled: state.options.enabled }));
  return enabled ? <Build {...props} /> : <Render {...props} />;
};
Pie.craft = config.craft;
Pie.info = config.info;
Pie.defaultProps = config.defaultProps;

Build — Mock data, useEnhancedNode, connect:

const { connectors: { connect } } = useEnhancedNode();
const data = useMemo(() => ({ labels: ['A','B','C'], datasets: [{ data: [1,2,3] }] }), []);
return <div ref={connect}><PieChart data={data} options={options} /></div>;

Render — Real datasource, useRenderer, useSources:

const { connect } = useRenderer();
const { sources: { datasource: ds } } = useSources();
const [value, setValue] = useState(initial);

useEffect(() => {
  if (!ds) return;
  const listener = async () => { const v = await ds.getValue(); setValue(v); };
  listener();
  ds.addListener('changed', listener);
  return () => ds.removeListener('changed', listener);
}, [ds]);

References

Version

Compatible with Qodly Studio - 4D 21. Docs: developer.4d.com/qodly/Integrations/customComponent.