Renderer Customization
English Documentation Status
This English documentation is currently under development. The content may be incomplete or subject to change. For the most complete and up-to-date information, please refer to the Chinese documentation. We appreciate your patience as we work to provide comprehensive English documentation.
Custom Renderer Development
EasyEditor provides a flexible renderer architecture that allows developers to create custom renderers to support different frameworks or specific scenario requirements. This guide will walk you through how to develop custom renderers.
Renderer Architecture Overview
Working Principle
In EasyEditor, renderers are responsible for converting Schema into actual UI interfaces. Renderers follow this workflow:
- Parse Schema - Parse JSON descriptions into internal data structures
- Map Components - Map component names to actual component implementations
- Process Properties - Handle component properties such as event binding, style computation, etc.
- Render Components - Render components to the DOM
Architecture Composition
The renderer architecture is a layered structure that starts with Schema, processes it through the rendering adapter, and then distributes it to different renderer collections. The renderer collection contains three types: base renderer, page renderer, and component renderer. The base renderer is enhanced through higher-order components (including component wrappers and leaf node wrappers), while page renderers and component renderers interact directly with the final rendering engine. The final rendering engine renders components from the component library into user interfaces.
The rendering flow is as follows:
- Schema data first enters the rendering adapter
- The rendering adapter distributes data to the appropriate renderer (base renderer, page renderer, or component renderer)
- The base renderer is enhanced through higher-order components and then passed to the final rendering engine
- Page renderers and component renderers directly pass processed data to the final rendering engine
- The final rendering engine renders components from the component library into user interfaces
Base Renderer Development
Understanding the Renderer Factory Pattern
EasyEditor's renderers use the factory pattern design, which mainly includes the following parts:
- Renderer Factory (rendererFactory): Creates the final renderer component
- Base Renderer Factory (baseRendererFactory): Creates basic rendering capabilities
- Page Renderer Factory (pageRendererFactory): Creates page renderers based on base renderers
- Component Renderer Factory (componentRendererFactory): Creates component renderers based on base renderers
These factory functions work together to build a complete rendering system:
import {
adapter,
rendererFactory,
componentRendererFactory,
pageRendererFactory,
baseRendererFactory
} from '@easy-editor/react-renderer'
// Custom base renderer factory
export const customBaseRendererFactory = () => {
// Get original base renderer
const OriginBase = baseRendererFactory();
// Return extended base renderer class
return class BaseRenderer extends OriginBase {
// Custom component higher-order component chain
get __componentHOCs() {
if (this.__designModeIsDesign) {
// HOC chain in design mode
return [customWrapper, leafWrapper, compWrapper];
}
// HOC chain in runtime mode
return [customWrapper, compWrapper];
}
}
}
// Register to adapter
adapter.setBaseRenderer(customBaseRendererFactory());
// Register page renderer and component renderer
adapter.setRenderers({
PageRenderer: pageRendererFactory(), // Use default page renderer
ComponentRenderer: componentRendererFactory(), // Use default component renderer
});
// Create final renderer
export const CustomRenderer = rendererFactory();
Relationship Between Base Renderer, Page Renderer, and Component Renderer
The Base Renderer provides core rendering capabilities, including:
- Property parsing and transformation
- Component tree traversal
- Lifecycle management
- Higher-order component processing
Page Renderer (PageRenderer) and Component Renderer (ComponentRenderer) both inherit from the base renderer and are used to handle specific types of Schema:
// Page renderer factory
export function pageRendererFactory(): BaseRenderComponent {
// Get base renderer
const BaseRenderer = baseRendererFactory()
// Extend base renderer, add page-specific logic
return class PageRenderer extends BaseRenderer {
static displayName = 'PageRenderer'
__namespace = 'page'
// Page renderer specific initialization logic
__afterInit(props: BaseRendererProps, ...rest: unknown[]) {
const schema = props.__schema || {}
this.state = this.__parseData(schema.state || {})
this.__initDataSource(props)
this.__executeLifeCycleMethod('constructor', [props, ...rest])
}
// Page renderer specific rendering logic
render() {
const { __schema } = this.props
// Page-specific rendering logic
// ...
// Get component view
const Comp = this.__getComponentView()
// Render component
return this.__renderComp(Comp, { pageContext: this })
}
}
}
// Component renderer factory
export function componentRendererFactory(): BaseRenderComponent {
// Get base renderer
const BaseRenderer = baseRendererFactory()
// Extend base renderer, add component-specific logic
return class CompRenderer extends BaseRenderer {
static displayName = 'CompRenderer'
__namespace = 'component'
// Component renderer specific initialization logic
__afterInit(props: BaseRendererProps, ...rest: any[]) {
this.__generateCtx({
component: this,
})
const schema = props.__schema || {}
this.state = this.__parseData(schema.state || {})
this.__initDataSource(props)
this.__executeLifeCycleMethod('constructor', [props, ...rest])
}
// Component renderer specific rendering logic
render() {
// Component-specific rendering logic
// ...
const Comp = this.__getComponentView()
// Render component
return this.__renderComp(Comp, { compContext: this })
}
}
}
Main Renderer Implementation
The main renderer is created through rendererFactory and is responsible for coordinating different types of renderers:
export function rendererFactory(): RenderComponent {
// Get registered renderers
const RENDERER_COMPS = adapter.getRenderers()
// Return renderer class
return class Renderer extends Component<RendererProps> {
static displayName = 'Renderer'
// Rendering logic
render() {
const { schema, designMode, appHelper, components } = this.props
// Merge components
const allComponents = { ...components, ...RENDERER_COMPS }
// Select appropriate renderer (use PageRenderer by default)
const Comp = allComponents.PageRenderer
// Create rendering context
return (
<RendererContext.Provider
value={{
appHelper,
components: allComponents,
engine: this,
}}
>
<Comp
__appHelper={appHelper}
__components={allComponents}
__schema={schema}
__designMode={designMode}
{...this.props}
/>
</RendererContext.Provider>
)
}
}
}
Advanced Renderer Features
Custom Component Wrapper (HOC)
Base renderers process components through higher-order component chains, and you can customize these wrappers:
import { type ComponentHocInfo, createForwardRefHocElement } from '@easy-editor/react-renderer'
import React, { Component } from 'react'
export function customWrapper(Comp: any, { schema, baseRenderer, componentInfo }: ComponentHocInfo) {
// Get context information
const host = baseRenderer.props?.__host
const isDesignMode = host?.designMode === 'design'
// Define wrapper component class
class Wrapper extends Component<any> {
render() {
const { forwardRef, children, className, ...rest } = this.props
// Handle special cases
if (schema.isRoot) {
return (
<Comp ref={forwardRef} {...rest}>
{children}
</Comp>
)
}
// Regular component rendering logic
return (
<div className="custom-component-container">
<Comp
ref={forwardRef}
className={`custom-component ${className || ''}`}
{...rest}
>
{children}
</Comp>
</div>
)
}
}
// Set display name
(Wrapper as any).displayName = Comp.displayName
// Create forward ref HOC element
return createForwardRefHocElement(Wrapper, Comp)
}
Error Boundary Handling
To improve renderer robustness, we can add error boundaries:
// Error boundary component
class ErrorBoundary extends React.Component<
{ componentName: string; children: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error(`Error rendering ${this.props.componentName}:`, error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="easy-editor-error-boundary">
<h3>Component Rendering Error</h3>
<p>Component: {this.props.componentName}</p>
<p>Error: {this.state.error?.message}</p>
</div>
);
}
return this.props.children;
}
}
// Use error boundary when rendering components
function renderComponent(
schema: Schema,
components: Record<string, React.ComponentType<any>>,
context: any
) {
const { componentName } = schema;
// ...other code
return (
<ErrorBoundary componentName={componentName}>
{/* Component rendering code */}
</ErrorBoundary>
);
}
Component Communication Mechanism
Implement inter-component communication mechanism:
// Create event bus
class EventBus {
private listeners: Record<string, Array<(...args: any[]) => void>> = {};
// Subscribe to events
on(event: string, callback: (...args: any[]) => void) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
return () => this.off(event, callback);
}
// Unsubscribe
off(event: string, callback: (...args: any[]) => void) {
if (!this.listeners[event]) return;
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
}
// Emit events
emit(event: string, ...args: any[]) {
if (!this.listeners[event]) return;
this.listeners[event].forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`Error in event listener for ${event}:`, error);
}
});
}
}
// Create context
const createRendererContext = () => {
const eventBus = new EventBus();
return {
// Event system
event: {
on: eventBus.on.bind(eventBus),
off: eventBus.off.bind(eventBus),
emit: eventBus.emit.bind(eventBus)
},
// Shared state
shared: new Map(),
// Set/get shared state
setShared: (key: string, value: any) => {
context.shared.set(key, value);
eventBus.emit(`shared:${key}:change`, value);
},
getShared: (key: string) => context.shared.get(key)
};
};
Specific Scenario Renderer Cases
Dashboard Renderer Implementation
The following is a dashboard renderer implementation case, showing how to extend scenario-specific renderers based on the basic architecture:
import { useRef } from 'react'
import { adapter, componentRendererFactory, pageRendererFactory, rendererFactory } from '@easy-editor/react-renderer'
import { useResizeObserver } from '../hooks/useResizeObserver'
// 1. Custom dashboard base renderer
const dashboardBaseRendererFactory = () => {
// Get original base renderer
const BaseRenderer = baseRendererFactory();
return class DashboardBaseRenderer extends BaseRenderer {
// Custom component higher-order component chain, add dashboard-specific wrappers
get __componentHOCs() {
if (this.__designModeIsDesign) {
// HOC chain in design mode
return [dashboardWrapper, leafWrapper, compWrapper];
}
// HOC chain in runtime mode
return [dashboardWrapper, compWrapper];
}
}
}
// 2. Register dashboard renderer
adapter.setBaseRenderer(dashboardBaseRendererFactory());
adapter.setRenderers({
PageRenderer: pageRendererFactory(),
ComponentRenderer: componentRendererFactory(),
});
// 3. Create dashboard renderer
const DashboardRenderer = rendererFactory();
// 4. Wrap dashboard renderer, handle scaling and other special logic
const DashboardRendererWrapper = (props) => {
const { viewport, ...rendererProps } = props
const { width: viewportWidth = 1920, height: viewportHeight = 1080 } = viewport || {}
// Reference DOM elements
const canvasRef = useRef<HTMLDivElement>(null)
const viewportRef = useRef<HTMLDivElement>(null)
// Listen for canvas size changes, automatically adjust scale ratio
useResizeObserver({
elem: canvasRef,
onResize: entries => {
const { width, height } = entries[0].contentRect
const ww = width / viewportWidth
const wh = height / viewportHeight
viewportRef.current!.style.transform = `scale(${Math.min(ww, wh)}) translate(-50%, -50%)`
},
})
return (
<div className='easy-editor'>
{/* Canvas container */}
<div ref={canvasRef} className='easy-editor-canvas'>
{/* Viewport container */}
<div
ref={viewportRef}
className='easy-editor-viewport'
style={{
width: viewportWidth,
height: viewportHeight,
}}
>
{/* Content area */}
<div className='easy-editor-content'>
{/* Use customized dashboard renderer */}
<DashboardRenderer {...rendererProps} />
</div>
</div>
</div>
</div>
)
}
export default DashboardRendererWrapper
Dashboard Component Wrapper Implementation
Components in dashboard scenarios require special positioning and coordinate system handling:
import type { NodeSchema } from '@easy-editor/core'
import { type ComponentHocInfo, createForwardRefHocElement } from '@easy-editor/react-renderer'
import { Component } from 'react'
export function dashboardWrapper(Comp: any, { schema, baseRenderer }: ComponentHocInfo) {
// Get context information
const host = baseRenderer.props?.__host
const isDesignMode = host?.designMode === 'design'
// Dashboard configuration information
let { mask = true } = host?.dashboardStyle || {}
// In non-design mode, don't show mask
if (!isDesignMode) {
mask = false
}
class Wrapper extends Component<any> {
render() {
const { forwardRef, children, className, ...rest } = this.props
// Calculate node position and size
const rect = computeRect(schema)
if (!rect) {
return null
}
// Special handling for root nodes
if (schema.isRoot) {
return (
<Comp ref={forwardRef} {...rest}>
{children}
</Comp>
)
}
// Regular node rendering, including coordinate positioning
return (
// Container layer
<div
className={`easy-editor-component-container ${mask ? 'mask' : ''}`}
style={{
left: rect.x,
top: rect.y,
width: rect.width,
height: rect.height,
}}
>
{/* Reset coordinate system */}
<div
style={{
position: 'absolute',
left: -rect.x!,
top: -rect.y!,
}}
>
{/* Component coordinate positioning */}
<div
ref={forwardRef}
className='easy-editor-component-mask'
style={{
left: rect.x!,
top: rect.y!,
width: rect.width,
height: rect.height,
}}
>
{/* Component rendering */}
<Comp className={`easy-editor-component ${mask ? 'mask' : ''} ${className || ''}`} {...rest}>
{children && (
// Reset coordinate system again for internal component positioning
<div
style={{
position: 'absolute',
left: -rect.x!,
top: -rect.y!,
}}
>
{children}
</div>
)}
</Comp>
</div>
</div>
</div>
)
}
}
;(Wrapper as any).displayName = Comp.displayName
return createForwardRefHocElement(Wrapper, Comp)
}
Adapting Other Frameworks
Under Development
Support for adapting other frameworks (such as Vue, Angular, etc.) is actively under development. We plan to provide support for more frameworks in future versions, allowing EasyEditor to better serve projects with different technology stacks.
If you have specific framework adaptation needs, please submit an Issue on GitHub or participate in discussions.
Tips
Tips
EasyEditor provides a highly flexible and extensible renderer architecture that allows you to build customized rendering solutions that fully meet project requirements. The above implementations are for reference only, and you can freely develop according to actual scenarios to create your own renderers.