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:
- zoovu.manifest.js - component metadata (name, type, icon, label)
- [component].component.vue - Vue component file
- [component].configuration.ts - configuration object definition
- [component].preset.ts - preset object definition (mirrors configuration structure)
- [component].style.ts - CSS class generation using configuration
- 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'> extendcan 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
@ComponentConfigdecorator to inject configuration - Use
@ComponentStyledecorator to inject styles - Use
@ModelPropdecorator 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 namemust match Vue component filename (camelCase)typeandsubTypetogether determine where component appears in EXD UIsubTypeis optional, creates variant of existing component typeiconmust be valid SVG stringlabelis user-facing name in EXD
Function writing standards
Every function must follow these rules:
- Single responsibility: Focus on one thing only
- Max 10 lines: Function body cannot exceed 10 lines
- Max 1 argument: Functions should accept at most 1 parameter
- 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
- Create directory structure in
components/[component-name]/src/ - Create subcomponents in
components/[component-name]/src/components/ - Generate required files using
custom-payment-buttonas template - Ensure configuration ↔ preset structural parity
- 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