Builder

repository·main·Indexed 41 days ago

https://github.com/coltorapps/builder

A headless, type-safe library for creating dynamic builders such as form, dashboard, or website builders. It works across React, React Native, and server environments, utilizing a hierarchical composition model of Attributes, Entities, and Builders to create reusable configuration schemas.

Tokens
56.6K
Snippets
126
Records
172
Agent score
52%

What's inside Builder

  1. Overview of Builder

    main

    Builder is a versatile library designed for crafting drag and drop form builders and JSON schema forms. It can also be used to generate dynamic UIs from JSON schemas or any other custom builder logic.

    Key Characteristics:

    • Headless: You bring your own components and design the user experience.
    • Full-stack: Provides tools for both building the UI and validating schemas on the back-end.
    • Typesafe: Full type safety is provided by default.
    • Multi-platform: Supports web (React), mobile (React Native), servers, serverless, and workers.
    • Framework agnostic: The core is independent of any specific front-end framework.
    • Zero dependencies: Lightweight and streamlined.
    • Unopinionated: Offers freedom to build custom implementations.
    • Progressively Enhanceable: Embraces web standards.
  2. What is a Schema in Coltorapps Builder?

    main

    A schema is a JSON structure generated by a builder that defines a collection of entity instances and their hierarchical order.

    It consists of two primary components:

    1. entities: A map where each key is a unique ID and the value is an object representing an entity instance. Each entity object includes:
      • type: The name of the entity definition.
      • attributes: The values for that entity's attributes.
      • parentId (optional): The ID of the parent entity.
      • children (optional): An array of entity IDs representing the ordered children within a parent.
    2. root: An array of IDs representing the top-level entities in the hierarchy, determining their starting order.

    This structure allows for complex, nested entity relationships (like sections containing text fields) to be represented in a flat, searchable map.

    {
      "entities": {
        "6e0035c3-0d4c-445f-a42b-2d971225447c": {
          "type": "textField",
          "attributes": {
            "label": "First Name",
            "required": true
          },
          "parentId": "51324b32-adc3-4d17-a90e-66b5453935bd"
        },
        "51324b32-adc3-4d17-a90e-66b5453935bd": {
          "type": "section",
          "attributes": {
            "title": "Personal Information"
          },
          "children": [
            "6e0035c3-0d4c-445f-a42b-2d971225447c"
          ]
        }
      },
      "root": ["51324b32-adc3-4d17-a90e-66b5453935bd"]
    }
  3. What is an interpreter store?

    main

    An interpreter store is a central state management unit responsible for populating entity values based on a provided schema and a builder definition. It handles setting entity values, performing validation, and managing errors.

    While you can create one manually using createInterpreterStore, most React users should use the useInterpreterStore hook from @coltorapps/builder-react, which automates the creation process.

  4. What is a builder store and how to create one

    main

    A builder store is the central component used for constructing a schema based on a builder definition. It manages tasks such as adding, removing, and reordering entities, as well as performing entity validation.

    You can create a store using the createBuilderStore method from @coltorapps/builder. However, if you are using React, it is recommended to use the useBuilderStore hook from @coltorapps/builder-react instead, as it handles store creation for you.

    Basic Usage

    import { createBuilderStore } from "@coltorapps/builder";
    import { formBuilder } from "./form-builder";
    
    const formBuilderStore = createBuilderStore(formBuilder);
    
    // Adding entities
    const section = formBuilderStore.addEntity({
      type: "section",
      attributes: {
        tile: "Personal Information",
      },
    });
    
    formBuilderStore.addEntity({
      type: "textField",
      attributes: {
        label: "First Name",
        required: true,
      },
      parentId: section.id,
    });
    
    // Modifying entities
    formBuilderStore.setEntityIndex(entityId, 0);
    formBuilderStore.setEntityAttribute(entityId, "required", true);
    
    // Retrieving the schema
    const schema = formBuilderStore.getSchema();
    import { createBuilderStore } from "@coltorapps/builder";
    
    import { formBuilder } from "./form-builder";
    
    const formBuilderStore = createBuilderStore(formBuilder);
    
    const personalInformationSection = formBuilderStore.addEntity({
      type: "section",
      attributes: {
        tile: "Personal Information",
      },
    });
    
    formBuilderStore.addEntity({
      type: "textField",
      attributes: {
        label: "First Name",
        required: true,
      },
      parentId: personalInformationSection.id,
    });
    
    const lastNameField = formBuilderStore.addEntity({
      type: "textField",
      attributes: {
        label: "Last Name",
        required: false,
      },
      parentId: personalInformationSection.id,
    });
    
    formBuilderStore.setEntityIndex(lastNameField.id, 0);
    
    formBuilderStore.setEntityAttribute(lastNameField.id, "required", true);
    
    const schema = formBuilderStore.getSchema();
  5. Extend or override entities using entitiesExtensions

    main

    You can use the entitiesExtensions property in createBuilder to modify how specific entities behave or how their attributes are validated. This is a deeply partial object where the top-level keys are the names of your entities.

    Extension Structure

    {
      // The key is the name of one of the entities.
      entityName?: {
        // Determines whether the entity can or cannot have any child entities.
        // Can be a boolean or an array of entity names.
        childrenAllowed?: boolean | string[];
        // Determines whether the entity must always have a parent entity.
        parentRequired?: boolean;
        // An array of entities names that are allowed to be the parent of this entity.
        allowedParents?: string[];
        // Attributes extensions.
        attributes?: {
          // The key corresponds to the name of an attribute.
          attributeName?: {
            // The logic for validating the attribute's value.
            validate?: (value, context) => TValue
          }
        }
      }
    }

    Custom Attribute Validation

    The validate method in an attribute extension receives the same arguments as the base validation, but the context object includes an additional context.validate(value) method.

    • To augment validation: Call context.validate(value) within your custom function to run the original/base validation logic.
    • To replace validation: Simply do not call context.validate(value) and implement your own logic.

    Note: The return type of your custom validate function must match the return type of the attribute's base validate method.

  6. Use attributes during entity validation

    main

    Entities are configurable via attributes. You can access an entity's current attribute values through the context object passed to the validate method. This allows validation logic to change dynamically based on the configuration of the entity instance.

    import { z } from "zod";
    import { createAttribute, createEntity } from "@coltorapps/builder";
    
    export const requiredAttribute = createAttribute({
      name: "required",
      validate(value) {
        return z.boolean().parse(value);
      },
    });
    
    export const textFieldEntity = createEntity({
      name: "textField",
      attributes: [requiredAttribute],
      validate(value, context) {
        const schema = z.string();
    
        if (context.entity.attributes.required) {
          return schema.min(1).parse(value);
        }
    
        return schema.optional().parse(value);
      },
    });
  7. Understand the context object in createEntity methods

    main

    The context object is provided as an argument to the validate, defaultValue, and shouldBeProcessed methods defined within createEntity. It allows your logic to access the current state of the schema.

    PropertyTypeDescription
    entityobjectThe current entity instance.
    entitiesValuesobjectThe values of all entities during schema interpretation.
  8. Understand the builder store data structure

    main

    You can retrieve the full state of a builder store using the getData() method. The returned object contains three main parts:

    1. schema: Contains the collection of all entity instances and their order. It is modified by methods like addEntity, deleteEntity, setEntityParent, unsetEntityParent, setEntityIndex, setEntityAttribute, and cloneEntity.
    2. entitiesAttributesErrors: Contains validation errors for specific entity attributes. These are managed by methods like validateEntityAttribute, validateEntityAttributes, validateEntitiesAttributes, resetEntityAttributeError, setEntityAttributeError, resetEntityAttributesErrors, setEntityAttributesErrors, resetEntitiesAttributesErrors, setEntitiesAttributesErrors, and validateSchema. Errors are automatically removed when an entity is deleted.
    3. schemaError: Represents a high-level schema validation error (from the builder's validateSchema method). It can be managed via validateSchema, setSchemaError, and resetSchemaError.

    Example Data Output

    {
      "schema": {
        "entities": {
          "51324b32-adc3-4d17-a90e-66b5453935bd": {
            "type": "textField",
            "attributes": {
              "label": ""
            }
          }
        },
        "root": ["51324b32-adc3-4d17-a90e-66b5453935bd"]
      },
      "entitiesAttributesErrors": {
        "51324b32-adc3-4d17-a90e-66b5453935bd": {
          "label": "Must be at least one character in length"
        }
      },
      "schemaError": "Custom schema error"
    }
  9. How Attributes, Entities, and Builders work together

    main

    Builder uses a hierarchical composition model to define complex structures like form builders or website builders:

    1. Attributes: The atomic building blocks. They act like props for entities (e.g., a label or maxLength). They include validation logic, typically using zod.
    2. Entities: Components that hold attributes. An entity represents a single functional unit, such as a textField. It defines which attributes it accepts and how to validate its own value.
    3. Builders: The top-level collection. A builder defines the set of allowed entities that can be used within a specific context (e.g., a formBuilder that only allows text fields and checkboxes).

    This hierarchy allows you to create highly reusable and type-safe configuration schemas.

    import { z } from "zod";
    import { createAttribute, createEntity, createBuilder } from "@coltorapps/builder";
    
    // 1. Define an Attribute
    const labelAttribute = createAttribute({
      name: "label",
      validate(value) {
        return z.string().min(1).parse(value);
      },
    });
    
    // 2. Define an Entity using that Attribute
    const textFieldEntity = createEntity({
      name: "textField",
      attributes: [labelAttribute],
      validate(value) {
        return z.string().optional().parse(value);
      },
    });
    
    // 3. Define a Builder containing that Entity
    const formBuilder = createBuilder({
      entities: [textFieldEntity],
    });
  10. Understand the structure of Entities values

    main

    Entities values represent the result of user input within the context of an interpreted schema. They are always structured as a single-level flat format, mirroring the structure of FormData. In this format, the keys are the entity IDs (UUIDs) and the values are the user-provided data for those entities.

    {
      "51324b32-adc3-4d17-a90e-66b5453935bd": "Alexandru",
      "d5ae8682-156c-4511-b972-98c6c3b7c41b": "Stratulat"
    }
  11. Extend entity relationship constraints in a builder

    main

    While entities can define if children are allowed via childrenAllowed, you can define specific relationship constraints at the builder level using entitiesExtensions. This allows for type-safe constraints that depend on the combination of entities within a specific builder.

    Use the following properties within entitiesExtensions:

    • childrenAllowed: An array of entity names (e.g., ['textField']) that are permitted as children. Setting this to true allows any children.
    • allowedParents: An array of entity names permitted to be parents of the current entity.
    • parentRequired: A boolean indicating if the entity must have a parent.

    This is useful for enforcing structures like: "Sections can only contain text fields, and text fields must be inside a section."

    import { createBuilder } from "@coltorapps/builder";
    
    import { sectionEntity } from "./section-entity";
    import { textFieldEntity } from "./text-field-entity";
    
    export const formBuilder = createBuilder({
      entities: [sectionEntity, textFieldEntity],
      entitiesExtensions: {
        section: {
          // Can also be set to `true` to allow any children.
          childrenAllowed: ["textField"],
        },
        textField: {
          allowedParents: ["section"],
          parentRequired: true,
        },
      },
    });
  12. Interpret and Render a Form Schema

    main

    To display a completed form to a user (the 'interpreter' phase), use the useInterpreterStore hook from @coltorapps/builder-react. This store fills entity values based on a provided schema and builder definition.

    Key components:

    • InterpreterEntities: Renders the tree of entities from the schema. It accepts a components object mapping entity types to their React components.

    To handle user input, you can use the onEntityValueUpdated event in the store configuration to trigger validation via interpreterStore.validateEntityValue(entityId) whenever a value changes.

    "use client";
    
    import { InterpreterEntities, useInterpreterStore } from "@coltorapps/builder-react";
    
    export function FormInterpreter({ schema }: { schema: any }) {
      const interpreterStore = useInterpreterStore(formBuilder, schema, {
        events: {
          onEntityValueUpdated(payload) {
            void interpreterStore.validateEntityValue(payload.entityId);
          },
        },
      });
    
      return (
        <form onSubmit={...}>
          <InterpreterEntities
            interpreterStore={interpreterStore}
            components={{ textField: TextFieldEntity }}
          />
          <button type="submit">Submit</button>
        </form>
      );
    }