Lantern

Custom Form Implementation

Learn how to build custom forms using Lantern's form builder and integrate them into your Shopify theme

For developers who want to create custom form experiences using Lantern's form builder, this guide shows you how to implement forms using Lantern's API endpoints and integrate them seamlessly into your Shopify theme.

Prerequisites

Before building custom forms, ensure you have:

  • Forms functionality enabled in your Lantern app
  • A form created in the Lantern admin with your desired fields
  • Basic knowledge of JavaScript, HTML, and Liquid templating
  • Access to your theme's code

How Lantern Forms Work

Lantern forms follow a simple workflow:

  1. Create a form in the Lantern admin with your desired fields
  2. Build an HTML form in your Shopify theme that matches the form structure
  3. Submit form data via Lantern's app proxy endpoint
  4. Handle responses and display success/error messages

Forms can include various field types including text inputs, select dropdowns, checkboxes, and can update both customer profile fields and custom Lantern properties.

API Access

Lantern forms can be accessed in multiple ways:

Direct API Access

Use your store's direct URL format for form submissions and configuration:

https://your-store.myshopify.com/a/lantern/form/{handle}

Shopify Metaobjects

Access form configuration through Shopify's native metaobject system using Liquid templates or GraphQL. This is ideal for theme developers who want to avoid external API calls and leverage Shopify's caching.

Accessing Forms Through Shopify Metaobjects

Lantern automatically creates Shopify metaobjects for each form, allowing you to access form configuration directly through Shopify's native APIs and Liquid templating. This is particularly useful for theme developers who want to access form data without making external API calls.

Metaobject Structure

Each form is stored as a metaobject of type lantern_forms with the following fields:

  • configuration: JSON field containing the complete form schema (same as API response)
  • name: The display name of the form

Accessing in Liquid Templates

You can access form configuration directly in your Liquid templates:

{% assign form_metaobject = shop.metaobjects.lantern_forms['sample-form'] %}

{% if form_metaobject %}
  <h2>{{ form_metaobject.name.value }}</h2>

  {% comment %} Parse the configuration JSON {% endcomment %}
  {% assign form_config = form_metaobject.configuration.value | parse_json %}

  <form class="lantern-form" data-handle="{{ form_config.handle }}" data-store="{{ shop.permanent_domain }}">
    {% for field in form_config.fields %}
      <div class="form-group">
        <label for="{{ field.handle }}">
          {{ field.label }}{% if field.required %} *{% endif %}
        </label>

        {% case field.fieldType %}
          {% when 'Text' %}
            <input
              type="{{ field.inputType | downcase | replace: 'input', '' }}"
              id="{{ field.handle }}"
              name="{{ field.handle }}"
              {% if field.required %}required{% endif %}
              {% if field.placeholder %}placeholder="{{ field.placeholder }}"{% endif %}
            >

          {% when 'Textarea' %}
            <textarea
              id="{{ field.handle }}"
              name="{{ field.handle }}"
              {% if field.required %}required{% endif %}
              {% if field.placeholder %}placeholder="{{ field.placeholder }}"{% endif %}
            ></textarea>

          {% when 'SingleChoice' %}
            {% if field.inputType == 'Select' %}
              <select id="{{ field.handle }}" name="{{ field.handle }}" {% if field.required %}required{% endif %}>
                {% for choice in field.choices %}
                  <option value="{{ choice.value }}" {% if choice.selected %}selected{% endif %}>
                    {{ choice.label }}
                  </option>
                {% endfor %}
              </select>
            {% else %}
              {% for choice in field.choices %}
                <label class="radio-label">
                  <input type="radio" name="{{ field.handle }}" value="{{ choice.value }}" {% if choice.selected %}checked{% endif %}>
                  {{ choice.label }}
                </label>
              {% endfor %}
            {% endif %}

          {% when 'Boolean' %}
            <label class="checkbox-label">
              <input type="checkbox" name="{{ field.handle }}" value="true">
              {{ field.label }}
            </label>

          {% when 'Number' %}
            <input
              type="number"
              id="{{ field.handle }}"
              name="{{ field.handle }}"
              {% if field.required %}required{% endif %}
            >

          {% when 'Date' %}
            <input
              type="date"
              id="{{ field.handle }}"
              name="{{ field.handle }}"
              {% if field.required %}required{% endif %}
            >
        {% endcase %}

        <span class="error-message" data-field="{{ field.handle }}"></span>
      </div>
    {% endfor %}

    <button type="submit">Submit</button>

    <div class="form-messages">
      <div class="success-message" style="display: none;">Form submitted successfully!</div>
      <div class="general-error" style="display: none;">Please correct the errors below.</div>
    </div>
  </form>

  {% comment %} Add the same JavaScript form handling as other examples {% endcomment %}
  <script>
    // Use the same form submission logic from previous examples
    // The form will submit to https://{{ shop.permanent_domain }}/a/lantern/form/{{ form_config.handle }}
  </script>
{% else %}
  <p>Form not found.</p>
{% endif %}

Benefits of Metaobject Access

Using metaobjects provides several advantages:

  • No external API calls: Access form configuration directly through Shopify's native systems
  • Caching: Leverage Shopify's caching for better performance
  • Theme integration: Seamlessly integrate with existing Liquid workflows
  • Real-time updates: Changes to forms are automatically reflected in metaobjects

Getting Form Configuration

Fetching Form Schema

You can also fetch the form configuration and field definitions using the GET endpoint:

// Function to fetch form configuration directly
async function fetchFormConfigDirect(handle, storeDomain) {
  const response = await fetch(`https://${storeDomain}/a/lantern/form/${handle}`, {
    headers: {
      Accept: 'application/json'
    }
  });

  if (response.ok) {
    return await response.json();
  }
  throw new Error('Failed to fetch form configuration');
}

// Example usage
fetchFormConfigDirect('newsletter-signup', 'your-store.myshopify.com')
  .then((form) => {
    console.log('Form configuration:', form);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

Form Structure

The form configuration returns the following structure:

{
  "id": "40",
  "handle": "test-form",
  "name": "Test Form",
  "fields": [
    {
      "handle": "profile:email",
      "label": "Email",
      "required": false,
      "valueType": "String",
      "fieldType": "Text",
      "inputType": "TextInput"
    },
    {
      "handle": "profile:phone",
      "label": "Phone",
      "required": false,
      "valueType": "String",
      "fieldType": "Text",
      "inputType": "PhoneInput"
    },
    {
      "handle": "profile:first-name",
      "label": "First Name",
      "required": true,
      "valueType": "String",
      "fieldType": "Text",
      "inputType": "TextInput"
    },
    {
      "handle": "attribute:where-did-you-here-about-us",
      "label": "Where did you here about us?",
      "required": true,
      "valueType": "String",
      "fieldType": "SingleChoice",
      "inputType": "Select",
      "choices": [
        {
          "id": "cd4bc661-bd1d-4f0d-8e2b-e3379a056fab",
          "label": "Friend",
          "value": "friend",
          "selected": false
        },
        {
          "id": "b2c41fe1-bf74-4d79-a39f-ed7d3966b80a",
          "label": "Google",
          "value": "google",
          "selected": false
        }
      ]
    },
    {
      "handle": "attribute:would-you-recommend-us",
      "label": "Would you recommend us?",
      "required": false,
      "valueType": "Boolean",
      "fieldType": "Boolean",
      "inputType": "Checkbox"
    }
  ]
}

Submitting a Form

You can submit forms using a POST request the provided app proxy endpoint:

async function submitForm(handle, formData, storeDomain) {
  const url = `https://${storeDomain}/a/lantern/form/${handle}`;

  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json'
      },
      body: JSON.stringify(formData)
    });

    const data = await response.json();
    console.log('Response:', data);

    if (response.ok && data.success) {
      console.log('Form submitted successfully:', data.eventId);
      return data;
    } else {
      console.error('Form submission failed:', data.errors);
      throw new Error('Form submission failed');
    }
  } catch (error) {
    console.error('Request failed:', error);
    throw error;
  }
}

// Example usage
const formData = {
  'profile:first-name': 'John',
  'profile:last-name': 'Doe',
  'profile:phone': '18001231234',
  'profile:email': 'john@example.com',
  'profile:birth-date': '1990-01-01',
  'attribute:where-did-you-here-about-us': 'friend',
  'attribute:would-you-recommend-us': true
};

submitForm('demo-form', formData, 'your-store.myshopify.com')
  .then((result) => {
    console.log('Success!', result);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

Field Types and Validation

Supported Field Types

Lantern forms support various field types:

  • Text: Text-based inputs with various input types (TextInput, PhoneInput, etc.)
  • Textarea: For longer text content
  • SingleChoice: Single selection from predefined options (Select dropdown or Radio buttons)
  • Boolean: Boolean values (Checkbox)
  • Number: Numeric inputs
  • Date: Date selection inputs

Profile vs. Property Fields

Lantern forms can update two types of data:

  1. Profile fields (prefix: profile:): Update Shopify customer profile data

    • profile:email - Customer email
    • profile:first-name - Customer first name
    • profile:last-name - Customer last name
    • profile:phone - Customer phone number
    • profile:birth-date - Customer birth date (YYYY-MM-DD format)
    • profile:accepts-marketing - Marketing consent (boolean)
  2. Attribute fields (prefix: attribute:): Update custom Lantern attributes. Examples:

    • attribute:where-did-you-here-about-us - Custom attribute for referral source
    • attribute:would-you-recommend-us - Custom attribute for recommendation

Validation

Lantern performs comprehensive server-side validation on all form submissions. When validation fails, the API returns a 422 Unprocessable Entity status with detailed error information.

Validation Types

Lantern validates several aspects of form submissions:

  1. Required Fields: Ensures all fields marked as required: true have values
  2. Data Types: Validates that values match the expected valueType (String, Number, Boolean, Date)
  3. Email Format: Validates email addresses for profile email fields
  4. Phone Format: Validates phone numbers for profile phone fields
  5. Date Format: Ensures dates are in valid ISO format (YYYY-MM-DD)
  6. Choice Values: Ensures selected values exist in the field's choice options

Error Response Format

When validation fails, you'll receive a response like this:

{
  "success": false,
  "errors": {
    "profile:email": "Please enter a valid email address",
    "profile:first-name": "First Name is required",
    "attribute:where-did-you-here-about-us": "Please select a valid option"
  }
}

Common Validation Scenarios

Missing Required Fields:

{
  "success": false,
  "errors": {
    "profile:first-name": "First Name is required",
    "attribute:where-did-you-here-about-us": "Where did you here about us? is required"
  }
}

Invalid Email Format:

{
  "success": false,
  "errors": {
    "profile:email": "Please enter a valid email address"
  }
}

Invalid Choice Value:

{
  "success": false,
  "errors": {
    "attribute:where-did-you-here-about-us": "Selected option is not valid"
  }
}

Type Mismatch:

{
  "success": false,
  "errors": {
    "attribute:how-many-cats-do-you-have": "Please enter a valid number",
    "profile:birth-date": "Please enter a valid date (YYYY-MM-DD)"
  }
}

Handling Validation Errors

Process validation errors in your form submission handler:

if (!response.ok && response.status === 422) {
  const result = await response.json();

  // Display field-specific errors
  for (const [fieldHandle, errorMessage] of Object.entries(result.errors)) {
    const errorElement = document.querySelector(`[data-field="${fieldHandle}"]`);
    if (errorElement) {
      errorElement.textContent = errorMessage;
      errorElement.style.display = 'block';
    }
  }
}

Troubleshooting

Common Issues

  • CORS errors: Ensure requests are made from the same domain or configure CORS properly
  • 404 Not Found: Verify the form handle exists and is associated with the correct shop
  • 422 Validation Error: Check that required fields are provided and data types match
  • Form not submitting: Ensure the form action points to the correct endpoint