How Form Submission and Data Collection Work End-to-End in Luban H5

Luban H5 implements form submission through two core plugins—lbp-form-input for rendering fields and lbp-form-button for collecting data and posting to /works/form/submit/:workId.

Understanding how form submission and data collection work in the ly525/luban-h5 editor requires examining both the front-end plugin architecture and the back-end Express API. The system uses a lightweight, attribute-based discovery mechanism where input fields expose metadata via HTML5 data attributes, enabling the submit button to gather values dynamically without tight coupling. This article traces the complete data flow from the designer's canvas to persistent storage in the database.

Front-End Form Collection

The front-end implementation relies on two specialized plugins that communicate through DOM attributes rather than direct component references.

Rendering Input Fields with lbp-form-input

When a designer drags a form input onto the canvas, the editor instantiates the lbp-form-input plugin located at src/components/core/plugins/lbp-form-input.js. This component renders a standard HTML input element while attaching two critical attributes: data-type="lbp-form-input" for discovery and data-uuid containing a unique identifier generated by the editor.

// src/components/core/plugins/lbp-form-input.js
// The render function attaches discovery attributes
<input 
  data-type="lbp-form-input" 
  data-uuid="auto-generated-uuid-123" 
  placeholder="Enter your name"
/>

The data-type prefix serves as a selector hook, allowing the submit button to query all form elements regardless of their specific position in the component tree or nested layout structures.

Submitting Data with lbp-form-button

The lbp-form-button plugin handles the actual data collection and transmission. Located at src/components/core/plugins/lbp-form-button.js, its handleClick method executes a four-step process:

// src/components/core/plugins/lbp-form-button.js
handleClick() {
  // 1. Discover all form inputs using the data-type prefix
  let inputs = document.querySelectorAll("[data-type^='lbp-form-input']")
  
  // 2. Build FormData payload mapping UUID to value
  let formData = new FormData()
  inputs.forEach(input => {
    formData.append(input.dataset.uuid, input.value)
  })
  
  // 3. Acquire work ID from global state
  const workId = window.__work.id
  
  // 4. POST to backend endpoint
  const req = new XMLHttpRequest()
  req.open('post', `/works/form/submit/${workId}`, true)
  req.send(formData)
}

The button reads the current work identifier from window.__work.id, a global shortcut maintained by the Vuex store. Upon receiving the server response, it displays success or failure toasts using Vant's Toast component based on the HTTP status code.

Back-End Processing and Storage

The back-end service resides in back-end/h5-api and processes multipart form-data submissions through an Express route.

Express Route Handling

The submission endpoint is defined in back-end/h5-api/server.js and handles validation and parsing:

// back-end/h5-api/server.js
app.post('/works/form/submit/:workId', async (req, res) => {
  const { workId } = req.params
  
  // Parsed by multer middleware into req.body
  const formData = req.body
  
  // Validation and permission checks
  if (!validateWorkId(workId)) {
    return res.status(400).json({ error: 'Invalid work ID' })
  }
  
  // Persist records and respond
  try {
    await saveFormRecords(workId, formData)
    res.status(200).json({ success: true })
  } catch (err) {
    res.status(500).json({ error: 'Submission failed' })
  }
})

Multer middleware extracts the FormData payload into a standard JavaScript object before it reaches the route handler. The server validates the workId against the works table, ensuring the submission targets an existing project.

Database Persistence

Each form field is stored as a separate record in the form_record table, defined in back-end/h5-api/models/form_record.js. The schema links individual field values to their parent work through foreign key relationships:

  • uuid: Maps to the data-uuid from the front-end input
  • value: The submitted text content
  • workId: References the parent work entity

This structure allows the system to reconstruct the complete form submission later by querying all records associated with a specific workId.

Vuex State Management

The editor's Vuex module maintains the current work context and makes it available to plugins through both reactive state and global window objects.

In src/store/modules/editor.js, the SET_WORK mutation initializes the environment:

// src/store/modules/editor.js
const state = {
  work: {},  // Contains id, name, pages, etc.
  formDetailOfWork: {}
}

const mutations = {
  SET_WORK(state, work) {
    state.work = work
    // Global shortcut for non-Vue contexts (like plugins)
    window.__work = work
  }
}

The formDetailOfWork state property stores previously submitted records retrieved via GET /works/form/query/:workId, enabling analytics views to display submission statistics without redundant API calls.

Complete End-to-End Workflow

The form submission and data collection process follows this chronological sequence:

  1. Designer configures form: Drag lbp-form-input components onto the canvas, configuring placeholders and types (text, email, phone)
  2. UUID assignment: The editor automatically generates unique identifiers and injects them as data-uuid attributes during render
  3. Submit button placement: Add lbp-form-button to the same page, which remains dormant until user interaction
  4. User interaction: End-user fills inputs and clicks the submit button
  5. Data gathering: handleClick queries [data-type^='lbp-form-input'], constructs FormData with UUID-value pairs
  6. Transmission: XMLHttpRequest sends POST /works/form/submit/:workId with multipart payload
  7. Server processing: Express validates work existence, persists each field to form_record table
  8. Feedback loop: HTTP 200 triggers "提交成功" toast; error codes display "提交失败"
  9. Retrieval: Admin users fetch submissions through formDetailOfWork state or direct API queries for analysis

Summary

  • Attribute-based discovery: Form inputs expose data-type="lbp-form-input" and data-uuid attributes for DOM selection
  • Decoupled architecture: The submit button queries the DOM rather than maintaining references to input components
  • Global work context: window.__work.id provides the target identifier for submissions, set by the Vuex SET_WORK mutation
  • RESTful endpoint: POST /works/form/submit/:workId accepts multipart form-data and returns HTTP 200 on success
  • Relational storage: Each field persists as a form_record row linked to the parent work via foreign key

Frequently Asked Questions

How does the submit button find form inputs without direct component references?

The lbp-form-button plugin uses document.querySelectorAll("[data-type^='lbp-form-input']") to discover all input elements carrying the specific data attribute. This DOM-based approach decouples the button from the input components, allowing flexible layouts where inputs might be nested inside groups or repeated lists.

Where does the work ID come from during form submission?

The work ID originates from the Vuex editor module's state.work.id, which gets mirrored to window.__work.id via the SET_WORK mutation. When a designer opens a project, the editor commits this mutation, making the identifier globally accessible to plugins that run outside the Vue instance context.

What format does the back-end expect for form data?

The server expects standard multipart/form-data encoding. The front-end constructs a FormData object where each entry uses the input's data-uuid as the key and the input value as the value. Multer middleware in the Express pipeline parses this into a JavaScript object available on req.body.

Can I customize the submission endpoint for different environments?

Yes. While the default lbp-form-button.js posts to /works/form/submit/${workId}, you can modify the handleClick method in src/components/core/plugins/lbp-form-button.js to target custom endpoints. For example, change req.open('post', /works/form/submit/${workId}, true) to req.open('post', /api/custom/form/submit/${workId}, true) to integrate with external CRM systems or specialized microservices.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →