Skip to content

Latest commit

 

History

History
265 lines (222 loc) · 5.77 KB

File metadata and controls

265 lines (222 loc) · 5.77 KB

Dynamic Forms Guide

ActiveDataSource's schema endpoint enables fully dynamic form generation on any frontend.


Schema Endpoint

Every resource exposes:

GET /users/schema

Response:

{
  "resource": "UserResource",
  "model": "User",
  "fields": [
    {
      "name": "name",
      "type": "string",
      "required": true,
      "placeholder": "Full name"
    },
    {
      "name": "email",
      "type": "string",
      "required": true,
      "validations": { "format": "email" }
    },
    {
      "name": "status",
      "type": "select",
      "options": ["active", "inactive", "pending"],
      "required": true,
      "default": "active"
    },
    {
      "name": "salary",
      "type": "decimal",
      "read_only": false,
      "write_only": false,
      "description": "Annual salary in USD"
    }
  ],
  "relations": [
    {
      "name": "organization",
      "kind": "belongs_to",
      "resource": "OrganizationResource",
      "optional": false
    }
  ],
  "filterable": ["status", "organization_id"],
  "sortable": ["name", "email", "created_at"],
  "pagination": {
    "default_page_size": 25,
    "maximum_page_size": 100
  },
  "flags": {
    "exportable": true,
    "importable": false,
    "readonly": false
  }
}

Defining Field Metadata

class UserResource < ActiveDataSource::Resource
  model User

  field :name do
    type        :string
    required    true
    placeholder "Enter full name"
    description "User's display name"
    validates   presence: true, length: { minimum: 2 }
  end

  field :email do
    type     :string
    required true
    validates format: { with: URI::MailTo::EMAIL_REGEXP }
  end

  field :status do
    type    :select
    options User.statuses.keys
    required true
    default "active"
  end

  field :role do
    type    :radio
    options %w[admin manager member]
  end

  field :permissions do
    type    :multi_select
    options %w[read write delete admin]
  end

  field :avatar do
    type :image
  end

  field :bio do
    type :text
    placeholder "Tell us about yourself"
    hidden false
  end

  field :salary do
    type      :decimal
    read_only false
  end
end

Supported Field Types

Type UI Component
string Text input
text Textarea
integer Number input (step=1)
float Number input
decimal Number input
boolean Checkbox/toggle
date Date picker
datetime Datetime picker
time Time picker
select Dropdown (single)
multi_select Multi-select dropdown
radio Radio button group
checkbox Checkbox group
file File upload
image Image upload with preview
json JSON editor
array Tag/chip input

TypeScript — Dynamic Form from Schema

import { DataSource } from "@active-data-source/client";

const users = new DataSource("/users");
const schema = await users.schema();

// schema.fields contains everything needed to render a form
for (const field of schema.fields) {
  console.log(field.name, field.type, field.options);
}

Stimulus — Dynamic Form Controller

// app/javascript/controllers/dynamic_form_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { schemaUrl: String }

  async connect() {
    const response = await fetch(this.schemaUrlValue, {
      headers: { Accept: "application/json" }
    });
    const schema = await response.json();
    this.renderForm(schema.fields);
  }

  renderForm(fields) {
    const form = document.createElement("form");
    fields
      .filter(f => !f.hidden && !f.read_only)
      .forEach(field => {
        form.appendChild(this.buildField(field));
      });
    this.element.appendChild(form);
  }

  buildField(field) {
    const wrapper = document.createElement("div");
    wrapper.className = "field";

    const label = document.createElement("label");
    label.textContent = field.name.replace(/_/g, " ");
    wrapper.appendChild(label);

    if (field.type === "select") {
      const select = document.createElement("select");
      select.name = field.name;
      (field.options || []).forEach(opt => {
        const option = document.createElement("option");
        option.value = opt;
        option.textContent = opt;
        select.appendChild(option);
      });
      wrapper.appendChild(select);
    } else {
      const input = document.createElement("input");
      input.name = field.name;
      input.type = field.type === "integer" || field.type === "decimal" ? "number" : "text";
      input.placeholder = field.placeholder || "";
      input.required = field.required || false;
      wrapper.appendChild(input);
    }

    return wrapper;
  }
}
<div data-controller="dynamic-form"
     data-dynamic-form-schema-url-value="/users/schema">
</div>

Vue 3 — Dynamic Form Component

<script setup>
import { ref, onMounted } from "vue";
import { DataSource } from "@active-data-source/client";

const fields = ref([]);
const users = new DataSource("/users");

onMounted(async () => {
  const schema = await users.schema();
  fields.value = schema.fields.filter(f => !f.hidden && !f.read_only);
});
</script>

<template>
  <form @submit.prevent="submit">
    <div v-for="field in fields" :key="field.name" class="form-group">
      <label>{{ field.name }}</label>
      <select v-if="field.type === 'select'" :name="field.name" :required="field.required">
        <option v-for="opt in field.options" :key="opt" :value="opt">{{ opt }}</option>
      </select>
      <input v-else :type="inputType(field)" :name="field.name"
             :placeholder="field.placeholder" :required="field.required" />
    </div>
    <button type="submit">Save</button>
  </form>
</template>