Skip to main content

General rules for EXD extension development

Vue.js version

  • Always use Vue.js version 2 (legacy version)
  • Never use Vue 3 syntax or composition API

EXD extension structure

New components should be created in components directory: <root>/components/.

Start by creating:

  1. zoovu.manifest.js - component metadata (name, type, icon, label)
  2. [component].component.vue - Vue component file
  3. [component].configuration.ts - configuration object definition
  4. [component].preset.ts - preset object definition (mirrors configuration structure)
  5. [component].style.ts - CSS class generation using configuration
  6. types/ folder with type definitions (e.g. enums.ts)

There are other names for "EXD Component", they all mean the same thing:

  • custom component
  • remote component
  • custom extension

File dependencies & rules

  • Configuration ↔ Preset: Must have identical structure; same properties, different syntax
  • Styles ← Configuration: Styles file consumes configuration to generate CSS classes
  • Vue ← Configuration + Styles: Both must be injected into Vue component
  • Runtime check: Configuration and preset objects should be nearly identical when evaluated

Reference files

Always analyze these existing files in the repo:

  • Manifest: zoovu.manifest.js
  • Configuration: /src/buttons.configuration.ts
  • Preset: /src/buttons.preset.ts
  • Styles: /src/buttons.style.ts
  • Vue Component: /src/buttons.component.vue
  • Types: /src/types/enums.ts

Code examples

Configuration pattern

export class ComponentConfiguration {
enableFeature = false;
backgroundColor: ColorValue = '#FBFBFB';
buttonLabel = 'Click me';
border: BorderConfiguration = new BorderConfiguration();
padding: PaddingConfiguration = new PaddingConfiguration();
margin: MarginConfiguration = new MarginConfiguration();
modal: ModalConfig = new ModalConfig();
}

class ModalConfig {
headingText = 'Modal Heading';
headingFont: FontConfiguration = new FontConfiguration();
}

Preset Pattern (mirrors Configuration)

import { object, boolean, color, standardText, remoteComponentConfig, EmbeddedComponentParameterFormat } from '@zoovu/theme-editor-parameter-types';
import { border, padding, margin, fontParameters } from './helpers/presets';

const componentPreset = remoteComponentConfig(
object(
{
enableFeature: boolean({ default: false, label: 'Enable Feature' }),
backgroundColor: color({ default: '#FBFBFB', label: 'Background color' }),
buttonLabel: standardText({ default: 'Click me', label: 'Button Label' }),
border: border(undefined, undefined, '#EBEBEB', 1, 0),
padding: padding(0, 0, 0, 0, 'px', 'Padding'),
margin: margin(0, 0, 0, 0, 'px', 'Margin', undefined, true),
modal: object(
{
headingText: standardText({ default: 'Modal Heading', label: 'Heading Text' }),
headingFont: fontParameters('#000000', 20, 400, 'left'),
},
{ label: 'Modal Settings', format: EmbeddedComponentParameterFormat.ACCORDION }
),
},
{ label: 'Component Configuration' }
)
);

export default componentPreset;

Note: Preset must be exported as export default.

Translation usage

In configuration:

buttonLabel = 'Click me';
modal.headingText = 'Modal Heading';

In preset (use standardText for translatable strings):

buttonLabel: standardText({ default: 'Click me', label: 'Button Label' }),
modal.headingText: standardText({ default: 'Modal Heading', label: 'Heading Text' }),

In Vue template:

<template>
<button>{{ $t(componentConfiguration.buttonLabel) }}</button>
<h1>{{ $t(componentConfiguration.modal.headingText) }}</h1>
</template>

$t() makes text translatable across locales. Always use standardText() in preset for user-facing text.

Style pattern

import { ComponentStyleDefinition } from '@zoovu/runner-browser-api';
import { createPaddingStyles, createMarginStyles, createBorderStyle } from '@zoovu/theme-editor-parameter-types';
import { ComponentConfiguration } from './component.configuration';

export const componentStyle: ComponentStyleDefinition<'container' | 'button'> = {
container: (config: ComponentConfiguration) => ({
'& .element': {
background: config.backgroundColor,
extend: {
...createPaddingStyles(config.padding),
...createMarginStyles(config.margin),
...createBorderStyle(config.border),
},
},
}),
button: (config: ComponentConfiguration) => ({
backgroundColor: config.modal.backgroundColor,
extend: [
createPaddingStyles(config.modal.padding),
createMarginStyles(config.modal.margin),
createBorderStyle(config.modal.border),
],
}),
};

export type ComponentNameStyle = Record<keyof typeof componentStyle, string>;

Key points:

  • Use union type for multiple style keys: ComponentStyleDefinition<'container' | 'button'>
  • extend can be object { ...createPaddingStyles() } or array [ createPaddingStyles() ]
  • Type export is required: Record<keyof typeof componentStyle, string>

Vue component pattern

<template>
<div :class="styles.container">
<h1>{{ $t(componentConfiguration.buttonLabel) }}</h1>
<button :class="styles.button" @click="handleClick">Click me</button>
<NotifyMeModalComponent v-if="showModal" @close="showModal = false" />
</div>
</template>

<script lang="ts">
import { Component, ComponentConfig, ComponentStyle, Mixins, Advisor, ModelProp, AdvisorViewContext, Inject } from '@zoovu/runner-browser-api';
import { ComponentConfiguration } from './component.configuration';
import { componentStyle, ComponentNameStyle } from './component.style';
import NotifyMeModalComponent from './components/notify-me-modal/notify-me-modal.component.vue';

@Component({ components: { NotifyMeModalComponent } })
export default class ComponentName {
@ComponentConfig(ComponentConfiguration)
componentConfiguration: ComponentConfiguration;

@ComponentStyle(componentStyle)
styles: ComponentNameStyle;

@Inject("context")
context: AdvisorViewContext;

public get advisor(): Advisor {
return this.advisorViewContext.advisor;
}

showModal = false;

mounted() {
console.log("Hello world, advisor model: ", this.advisor);
console.log("Available props", this.$parent.$options.propsData.props)
}

handleClick() {
this.showModal = true;
}
}
</script>

Key points:

  • Import child Vue components from relative path
  • Register components in @Component({ components: { ComponentName } }) decorator
  • Use registered components in template: <NotifyMeModalComponent />
  • Pass props and listen to events: v-if, @close
  • Use @ComponentConfig decorator to inject configuration
  • Use @ComponentStyle decorator to inject styles
  • Use @ModelProp decorator to inject advisor model

Manifest file (zoovu.manifest.js)

module.exports = {
// Name of the main Vue component
name: 'customPaymentButton',

// Type and SubType define where component appears in EXD
type: 'BUTTON',
subType: 'PRODUCT_CUSTOM_BUTTON',

// SVG icon displayed in EXD next to component name
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">...</svg>',

// Label shown in EXD component tile and configuration panel
label: 'Custom Payment Button',
};

Key points:

  • Located at component root: components/[component-name]/zoovu.manifest.js
  • name must match Vue component filename (camelCase)
  • type and subType together determine where component appears in EXD UI
  • subType is optional, creates variant of existing component type
  • icon must be valid SVG string
  • label is user-facing name in EXD

Function writing standards

Every function must follow these rules:

  1. Single responsibility: Focus on one thing only
  2. Max 10 lines: Function body cannot exceed 10 lines
  3. Max 1 argument: Functions should accept at most 1 parameter
  4. Composition over complexity: Break complex logic into multiple small functions

Example of compliant function

// ✓ GOOD: Single responsibility, <10 lines, 1 argument
const createClassName = (config) => {
return `component-${config.variant}`;
};

// ✗ BAD: Multiple arguments
const createClassName = (variant, size, theme) => {
return `component-${variant}-${size}-${theme}`;
};

// ✓ GOOD: Composition to handle multiple concerns
const createClassName = (config) => {
const variant = getVariant(config);
const size = getSize(config);
return combineClasses({ variant, size });
};

Development workflow

Creating EXD extension

  1. Create directory structure in components/[component-name]/src/
  2. Create subcomponents in components/[component-name]/src/components/
  3. Generate required files using custom-payment-button as template
  4. Ensure configuration ↔ preset structural parity
  5. Inject configuration + styles into Vue component

package.json

Make sure you are using latest libs:

{  
"name": "SampleFlowStepExtension",
"version": "0.0.1",
"private": true,
"main": "./src/index.ts",
"scripts": {
"dev": "zoovu-experience-designer-scripts remote-component dev",
"bump-version": "zoovu-experience-designer-scripts remote-component bump-version",
"build": "zoovu-experience-designer-scripts remote-component build"
},
"peerDependencies": {
"@zoovu/runner-browser-api": "5.162.3"
},
"devDependencies": {
"@zoovu/exd-scripts": "2.10.0"
},
"dependencies": {
"@zoovu/theme-editor-parameter-types": "5.31.1",
"@zoovu/exd-api": "0.7.0"
}
}

Quality checklist

Before considering any component complete, verify:

  • Configuration and preset have identical structure
  • Styles consume configuration object correctly
  • Vue component injects both configuration and styles
  • All functions are ≤10 lines with ≤1 argument
  • Each function has single responsibility

File naming conventions

  • Manifest: zoovu.manifest.js (at component root)
  • Vue files: [component-name].component.vue
  • Configuration: [component-name].configuration.ts
  • Preset: [component-name].preset.ts
  • Styles: [component-name].style.ts
  • Types: types/enums.ts (or relevant type file)

Eliminate code duplication systematically

  • When fixing duplicated code, always scan the entire codebase for similar patterns
  • Create helper functions to eliminate repetitive setup patterns
  • Look beyond the immediate issue to find all related duplication instances