Frontend Development 15 min read

Building a Custom Store Promotion Material Editor with Fabric.js and Formily

This article explains how to design and implement a canvas‑based editor for customizable store promotional materials, covering requirement analysis, system architecture, canvas library selection, component initialization, event handling, drag‑and‑drop, form integration, and future enhancements.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
Building a Custom Store Promotion Material Editor with Fabric.js and Formily

Background

Store promotional materials (flyers, banners, stickers, posters, etc.) need to be customized for each store's marketing requirements.

Requirement Analysis

Key questions include which elements belong to a custom material, which require franchiser customization, how to ensure submitted information follows design specifications, and how franchisers submit customization data.

Business Process and Architecture

The solution uses a canvas‑based template described by JSONSchema, links templates to materials, allows stores to fill custom content via a detail page and preview images, and lets designers review and finalize designs. The overall flow and system architecture are illustrated.

Editor Design

Definitions: Canvas, Poster (working area on the canvas), and offset values. The editor is broken down into components; popular canvas libraries were evaluated and Fabric.js was chosen.

Canvas Technology Selection

Comparison of Fabric.js, Konva, Excalidraw, etc.; Fabric.js offers strong community support and JSON schema support, making it suitable for complex graphic editors.

Poster Component Initialization

Poster extends

Fabric.Canvas

, adding its own properties. Example constructor code:

<code>const poster = new Poster('canvas', {
  posterDefaultWidth: posterState.defaultWidth,
  posterDefaultHeight: posterState.defaultHeight,
});</code>

The constructor calls the parent Canvas with options and initializes the workspace rectangle and default zoom.

<code>const defaultZoom = Math.min(this.width / this.defaultWidth, this.height / this.defaultHeight) - 0.01;
const left = (this.width - this.defaultWidth) / 2;
const top = (this.height - this.defaultHeight) / 2;
const workspace = new Rect({
  left,
  top,
  width: this.defaultWidth,
  height: this.defaultHeight,
  hasControls: false,
  fill: '#fff',
  id: this.posterId,
});
this.add(workspace);
this.zoomToPoint({ x: centerX, y: centerY }, defaultZoom);</code>

Event Handling

Poster inherits

Fabric.Canvas

events. Mouse wheel events are used for panning and zooming.

<code>poster.on('mouse:wheel', function (opt) {
  const { deltaY, deltaX } = opt.e;
  const zoom = poster.getZoom();
  poster.viewportTransform[4] -= deltaX / zoom;
  poster.viewportTransform[5] -= deltaY / zoom;
  poster.setViewportTransform(poster.viewportTransform);
});</code>

Zoom handling distinguishes between scroll and pinch gestures using

metaKey

/

ctrlKey

and adjusts the zoom factor.

<code>poster.on('mouse:wheel', function (opt) {
  const { deltaY, metaKey, ctrlKey, offsetX, offsetY } = opt.e;
  if (metaKey || ctrlKey) {
    const zoom = poster.getZoom();
    const sign = Math.sign(deltaY);
    const MAX_STEP = 0.1 * 100;
    const absDelta = Math.abs(deltaY);
    let delta = deltaY;
    if (absDelta > MAX_STEP) {
      delta = MAX_STEP * sign;
    }
    const newZoom = zoom - delta / 100;
    if (newZoom > 0.2) poster.zoomToPoint({ x: offsetX, y: offsetY }, newZoom);
  }
});</code>

Material Panel Drag‑and‑Drop

Native HTML drag‑and‑drop creates Fabric objects; each object receives a unique id (e.g., via nanoid) for later form binding.

<code>const { width, zoomX, height, left, top } = posterState.poster.getPosterInfo();
const { clientX, clientY } = e;
const right = left + width * zoomX;
const bottom = top + height * zoomX;
const canvasOffset = posterState.poster._offset;
const drawLeft = clientX - canvasOffset.left;
const drawTop = clientY - canvasOffset.top;
if (drawLeft > left && drawLeft < right && drawTop < bottom && drawTop > top) {
  const com = item.create({
    left: drawLeft,
    top: drawTop,
    scaleX: zoomX,
    scaleY: zoomX,
    id: nanoid(10),
  });
  posterState.poster.add(com);
  posterState.poster.setActiveObject(com);
}</code>

Object Selection Events

Selection creates a form schema based on the object's properties, rendered with Formily. Changes to form fields (e.g.,

fontFamily

) are applied back to the canvas object and the canvas is re‑rendered.

<code>posterState.poster.on('selection:created', (event) => {
  const [selected] = event.selected;
  const properties = posterState.poster.getBaseSchemaProperties(selected);
  setJsonData(properties || {});
  setFormData(selected);
});</code>

Example of updating font family:

<code>Poster.settingForm = createForm({
  effects: () => {
    onFieldChange('fontFamily', ({ value: fontOption }) => {
      const activeObject = this.getActiveObject();
      const { value, label } = fontOption;
      if (value && label) {
        const font = new FontFace(label, `url(${value})`);
        font.load().then((res) => {
          document.fonts.add(font);
          if (activeObject) {
            activeObject.set('fontFamily', res.family);
            this.renderAll();
          }
        });
      }
    });
  },
});</code>

Linking to Mini‑Program Custom Form

The unique id of each canvas element serves as the field key in the store’s customization form; after the form is filled, values replace the corresponding canvas object properties (e.g.,

text

) and the final image is generated via Fabric’s

loadFromJSON

.

Conclusion

The project combines Fabric.js and Formily with existing supply‑chain systems to enable end‑to‑end creation, distribution, and customization of store promotional materials, improving operational efficiency while supporting diverse marketing scenarios. Future work includes expanding the asset library, optimizing high‑resolution compositing, and improving large file management.

FrontendCanvasFormilycustomizationeditorfabric.jsjsonschema
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.