DEV Community

canonical
canonical

Posted on

Form layout language in a low-code platform: NopLayout

Forms are the primary means of information presentation in information systems. As early as the OA system era, numerous visual form design tools emerged, allowing completely free design of various peculiar form display styles. However, on the other hand, the capabilities of visual form designers actually exceed the layout requirements of form pages in general information systems. For most information systems, they are overly detailed, leading to much redundant work. This article will introduce the NopLayout form layout language used in the Nop platform, explaining the advantages of this domain-specific language compared to completely free visual design tools.

I. Standardized Requirements for Information System Forms

Information system forms are highly standardized, generally requiring only the following capabilities:

  • Display one or multiple fields per line, with size automatically adjusting responsively based on the browser window size (Manually specifying sizes is often inappropriate)
  • Each field has a Label on its left or top; a question mark icon might appear to the right of the Label for displaying hint information
  • Required fields are marked with an asterisk
  • If field validation fails, an error message is displayed to the right or below the field
  • Fields can be grouped, displayed using styles like FieldSet, Tab, or Panel
  • The controls used for fields are closely related to their field types, and fields with the same business meaning generally use the same display controls
  • Dynamic relationships exist between fields and field groups. Validation conditions, display conditions, candidate value ranges, etc., for fields may need to be dynamically calculated based on the values of other fields
  • Different forms must maintain the same display style; inconsistent display styles are considered BUGs

The Nop platform constructs an automated production pipeline from the data model to the frontend page. Based on the field types and data domain (domain) information defined in the data model, the relevant frontend control types and their property parameters can generally be automatically inferred. In this context, performing a full visual design of the entire form page becomes somewhat redundant: we only need to simply specify the overall layout of the fields and their interaction relationships, without needing to specify the display details for each field individually.

II. NopLayout: A DSL Dedicated to Form Layouts

In the Nop platform, we use NopLayout, a domain-specific language dedicated to form layouts, to separate form layout information from (field-level) content information. For example, for the following form layout

<form id="add" editMode="add" title="Add-User" i18n-en:title="Add User">
  <layout>
    ============>baseInfo[Basic Information]======
    userName[Username] status[User Status]
    nickName[Nickname]
    deptId[Department]

    ===========^extInfo[Extended Information]=========
    idType[ID Type] idNbr[ID Number]
    birthday[Birthday] workNo[Work Number]
    positionId[Position]
    remark[Remarks]
  </layout>
</form>
Enter fullscreen mode Exit fullscreen mode

This renders as:

Group Layout

NopLayout defines the following rules:

  1. Specify fields displayed per line; for example, a b means this line displays fields a and b
  2. Use fieldName[displayName] to specify the field name and its display label
  3. Mark a group with ===groupName[groupLabel]===
  4. If a group is given a label, it is rendered with the FieldSet widget; use ^ to indicate a collapsed group and > for an expanded group
  5. Prefix a field with @ to mark it as read-only; the frontend will use a view-mode widget
  6. Prefix a field with ! to hide its label

For example, to add department-based filtering on the left side of the user list, you essentially introduce a filter form in the sidebar

<form id="asideFilter" submitOnChange="true" editMode="query">
  <layout>
    ==dept[Department]==
    !deptId
  </layout>
  <cells>
    <cell id="deptId">
      <gen-control>
        <input-tree
           source="@query:NopAuthDept__findList/value:id,label:deptName,
              children @TreeChildren(max:5)?filter_parentId=__null"/>
      </gen-control>
    </cell>
  </cells>
</form>
Enter fullscreen mode Exit fullscreen mode

The deptId field is prefixed with !, meaning its label is hidden, resulting in:

Aside Filter

III. Field Control Inference

In most cases, field-level display controls can be inferred from the field type and data domain defined in the data model.

For example, for the gender field, we specify that it uses the auth/gender dictionary, so its values must come from that dictionary. For the email field, its domain is set to email, requiring input to conform to the email format.

When inferring the control to use, we first determine the field's editing mode (usually the same as the form's editMode, but it can be specified per field), then search the control tag library control.xlib in the following order, taking the first match:

  1. Look for a control named {editMode}-{control}, where control is explicitly specified via the control attribute in the cell configuration
  2. Look for {editMode}-{domain} controls, e.g., <edit-email/>
  3. If a dictionary is configured, look for a control named {editMode}-enum
  4. Look for {editMode}-{stdDomain} controls. stdDomain refers to globally registered standard data domains, which refine primitive data types—for example, stdDomain=xml denotes a text field whose content is XML
  5. If it's a foreign-key association, look for a control named {editMode}-to-one and try an associated-object picker
  6. Look for {editMode}-{dataType} controls, such as <edit-string/> and <edit-double/>
  7. If editMode is neither edit nor view, restart the search from step 1 using edit mode
  8. If in view mode, use <view-any/>; otherwise, use <edit-any/>

editMode includes add/view/update/edit/query, among others, and is used to distinguish different usage contexts for a field. Different contexts may use different controls—for example, a single-select control in edit mode versus a multi-select query control in query mode—yet the same field name can be used in the form layout.

Field control inference is an extensible mechanism. When recurring field-level semantics emerge in business, we can assign a distinct domain to them in the data model; the frontend will then automatically render all fields marked with that domain using a uniform control. The steps are:

  1. Define the domain in the data model and assign it to the fields
  2. Add control implementations for different editing modes in control.xlib (for example, for domain=roleId, you can provide <edit-roleId/>, <view-roleId/>, and <query-roleId/>). Alternatively, without modifying control.xlib directly, you can extend the built-in control.xlib via the Delta customization mechanism

The processing of user-defined domains is identical to that of built-in domains.

If a control is specialized and doesn't warrant abstraction into a unified domain, you can directly specify the display control used in the form, for example

<form id="edit">
   <layout>
      ...
   </layout>
   <cells>
      <cell id="fldA">
         <gen-control>
            Here output the specific control description used
         </gen-control>
      </cell>
   </cells>
</form>
Enter fullscreen mode Exit fullscreen mode

gen-control executes the XPL template language to generate a JSON control description.

IV. Field Interactions

Beyond layout information, you can specify inter-field interactions via supplemental cell configuration. For example

<form id="default" >
  <layout>
    sid[Resource ID] siteId[Site ID]
    displayName[Display Name] orderNo[Sort Order]
    resourceType[Resource Type] parentId[Parent Resource ID]
    =====menuProps=============
    icon[Icon] routePath[Frontend Route]
    url[URL] component[Component Name]
    target[Link Target] hidden[Hidden]
    keepAlive[Keep State When Hidden] noAuth[No Permission Check]
    depends[Dependent Resources]
    =====authProps============
    permissions[Permission Identifiers]
    =====otherProps===========
    status[Status] remark[Remarks]
  </layout>

  <cells>
    <cell id="parentId">
      <requiredOn>${resourceType != 'TOPM'}</requiredOn>
    </cell>
    <cell id="menuProps">
      <visibleOn>${resourceType != 'FNPT'}</visibleOn>
    </cell>
    <!--
     If a field group has no label, it only acts as a group and will not be displayed as a FieldSet
    -->
    <cell id="authProps">
      <visibleOn>${resourceType == 'FNPT'}</visibleOn>
    </cell>
  </cells>
</form>
Enter fullscreen mode Exit fullscreen mode

cell can target not only individual fields but also field groups, allowing you to set show/hide conditions for a group. In the example above, menuProps corresponds to a group and is shown only when resourceType != 'FNPT'.

V. Prototype Inheritance

In information systems, it's common for the layouts of create, update, and view forms to be largely or even entirely identical, while the controls used for update and view differ. Conventional techniques struggle to achieve reuse in such partially similar scenarios, but in the Nop platform, leveraging the principles of Reversible Computation, we provide a standardized solution:

  1. Abstract shared, potentially reusable information into a DSL syntax node
  2. Use the built-in x:prototype mechanism in XLang to construct a node from a sibling node as its template
  3. Apply Delta customization via x:prototype-override to modify the template

Applied to frontend forms, the approach is:

  1. Abstract the form layout into a <form/> node
  2. Define <form id="default" x:abstract="true"/> as the template; set x:prototype of the view/edit forms to default
  3. Adjust the display of specific fields via <cell/> nodes
<form id="default" x:abstract="true">
   <layout>
      ....
   </layout>
   <cells>
     <cell id="xx" >
        <visibleOn>yyy</visibleOn>
     </cell>
   </cells>
</form>

<form id="view" x:prototype="default" editMode="view">
</form>

<form id="edit" x:prototype="default" editMode="edit">
</form>
Enter fullscreen mode Exit fullscreen mode

x:abstract is a special attribute defined by XLang; when set to true, the node serves only as a template and will be automatically removed from the final merged output, similar to the abstract attribute in Spring XML configuration.

VI. Extended Layouts

The NopLayout language can also support more complex layout structures. For example, it supports multi-level nested layouts

<form>
  <layout>
      ====#sub1===
      ====##sub1_1===
       a b
      ====##sub1_2===
       c d
  </layout>
</form>
Enter fullscreen mode Exit fullscreen mode

In group markers, # denotes nesting levels, similar to Markdown; multiple # indicate the depth. ## represents a child of #.

Summary

The Nop platform is a new-generation low-code platform built from scratch based on the principles of reversible computation. It adopts a forward-design approach that is DSL-first, model-first, and automated-testing-first, rather than being derived from existing program frameworks combined with partial low-code modifications. This allows it to overcome difficulties present in many publicly known low-code solutions in various aspects.

NopLayout is a simple form layout syntax used within the Nop platform. For general information system forms, it is often unnecessary to use visual design tools for form design, as the display controls for form fields can basically be automatically determined based on field types and business semantics.

For a detailed introduction to the theory of reversible computation, you can refer to my previous article.

Top comments (0)