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:
- Create a form in the Lantern admin with your desired fields
- Build an HTML form in your Shopify theme that matches the form structure
- Submit form data via Lantern's app proxy endpoint
- 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}
Get a Form by Handle
GET
a form based on its handle.
Submit a Form by Handle
POST
a form based on its 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:
-
Profile fields (prefix:
profile:
): Update Shopify customer profile dataprofile:email
- Customer emailprofile:first-name
- Customer first nameprofile:last-name
- Customer last nameprofile:phone
- Customer phone numberprofile:birth-date
- Customer birth date (YYYY-MM-DD format)profile:accepts-marketing
- Marketing consent (boolean)
-
Attribute fields (prefix:
attribute:
): Update custom Lantern attributes. Examples:attribute:where-did-you-here-about-us
- Custom attribute for referral sourceattribute: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:
- Required Fields: Ensures all fields marked as
required: true
have values - Data Types: Validates that values match the expected
valueType
(String, Number, Boolean, Date) - Email Format: Validates email addresses for profile email fields
- Phone Format: Validates phone numbers for profile phone fields
- Date Format: Ensures dates are in valid ISO format (YYYY-MM-DD)
- 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