Skip to content

Instantly share code, notes, and snippets.

@panoply
Created March 16, 2023 07:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save panoply/f7b27709fa6e8b525317b8741e60553f to your computer and use it in GitHub Desktop.
Save panoply/f7b27709fa6e8b525317b8741e60553f to your computer and use it in GitHub Desktop.
{% if type == 'heading' %}
<div class="col-12 mt-{{ block.mt }} mb-{{ block.mb }} tc">
<h1 class="mb-4">
{{ block.title }}
</h1>
<h4 class="my-3 fc-dark-gray italic uncase">
{{ block.subtitle }}
</h4>
<div class="{{ block.fs }} {{ block.ta }}">
{{ block.description }}
</div>
</div>
{% elsif type == 'hidden' %}
<input
class="d-none"
type="hidden"
name="{{ block.name }}"
value="{{ block.value }}"
data-form-target="field">
{% elsif type == 'text' %}
<div class="col-{{ block.xs }} col-md-{{ block.md }} col-xl-{{ block.xl }} mt-{{ block.mt }} mb-{{
block.mb }}">
<div class="fm-float">
<input
class="fm-input"
data-form-target="field"
data-action="form#onInput"
type="{{ type }}"
id="{{ block.name }}"
name="{{ block.name }}"
placeholder="{{ block.placeholder }}"
autocomplete="{{ block.autocomplete }}"
minlength="{{ block.minlength }}"
maxlength="{{ block.maxlength }}"
required="{{ block.required }}">
<label
class="fm-label"
for="{{ block.name }}">
{{ block.placeholder }}
</label>
</div>
</div>
{% elsif type == 'file' %}
<div class="col-{{ block.xs }} col-md-{{ block.md }} col-xl-{{ block.xl }}">
<input
class="fm-input"
data-form-target="field"
data-action="form#onInput"
type="{{ type }}"
id="{{ block.name }}"
name="{{ block.name }}"
multiple="{{ block.multiple }}"
required="{{ block.required }}">
<label
for="{{ block.name }}"
class="fm-label">
{{ block.placeholder }}
</label>
</div>
{% elsif type == 'email' %}
<div class="col-{{ block.xs }} col-md-{{ block.md }} col-xl-{{ block.xl }} mt-{{ block.mt }} mb-{{
block.mb }}">
<div class="fm-float">
<input
class="fm-input"
data-form-target="field"
data-action="form#onInput"
type="{{ type }}"
name="{{ block.name }}"
id="{{ block.name }}"
placeholder="{{ block.placeholder }}"
required="{{ block.required }}"
autocomplete="{{ block.autocomplete }}">
<label
class="fm-label"
for="{{ block.name }}">
{{ block.placeholder }}
</label>
</div>
</div>
{% elsif type == 'tel' %}
<div class="col-{{ block.xs }} col-md-{{ block.md }} col-xl-{{ block.xl }} mt-{{ block.mt }} mb-{{
block.mb }}">
<div class="fm-float">
<input
class="fm-input"
data-form-target="field"
data-action="form#onInput"
type="{{ type }}"
id="{{ block.name }}"
name="{{ block.name }}"
required="{{ block.required }}"
autocomplete="{{ block.autocomplete }}">
<label
class="fm-label"
for="{{ block.name }}">
{{ block.placeholder }}
</label>
</div>
</div>
{% elsif type == 'textarea' %}
<div class="col-12 mt-{{ block.mt }} mb-{{ block.mb }}">
<div class="fm-float">
<textarea
class="fm-input"
data-form-target="field"
data-action="form#onInput"
style="height: {{ block.rows }};"
name="{{ block.name }}"
id="{{ block.name }}"
required="{{ block.required }}"></textarea>
<label
class="fm-label"
for="{{ block.name }}">
{{ block.placeholder }}
</label>
</div>
</div>
{% elsif type == 'dropdown' %}
<div class="col-12 mt-{{ block.mt }} mb-{{ block.mb }}">
<div
class="fm-dropdown fm-float"
data-controller="dropdown"
data-form-target="dropdown">
<button
type="button"
class="btn btn-dropdown fw-light"
data-action="dropdown#toggle"
data-dropdown-target="button">
{{ block.placeholder }}
</button>
<fieldset>
{% assign items = block.items | newline_to_br | split: '<br />' %}
{% for item in items %}
{% liquid
# STRIP + CLEAN
assign value = item | strip_html | strip
assign id = value | handle
%}
<label
class="upper"
for="{{ id }}">
{{ value }}
</label>
<input
class="d-none"
type="radio"
name="{{ block.name }}"
id="{{ id }}"
value="{{ value }}"
aria-label="{{ value }}"
data-form-target="field"
data-action="dropdown#select form#onInput"
{% if forloop.first and block.required %}
required
{% endif %}>
{% endfor %}
</fieldset>
</div>
</div>
{% elsif type == 'switch' %}
<div class="col-12 mt-{{ block.mt }} mb-{{ block.mb }}">
<div class="fm-switch">
{% capture name %}
{% if block.consent == true %}
{{ 'forms.field.accept_marketing' | t }}
{% else %}
{{ block.name }}
{% endif %}
{% endcapture %}
<input
type="checkbox"
data-form-target="field"
data-action="form#onInput"
class="fm-check-input"
id="{{ name }}"
name="{{ name }}"
required="{{ block.required }}"
{% if block.checked %}
checked
{% endif %}>
<label
for="{{ name }}"
class="fm-check-label upper fs-sm">
{{ block.label }}
</label>
</div>
</div>
{% elsif type == 'paragraph' %}
{% capture class %}
{{ block.fc }}
{{ block.ff }}
{{ block.fs }}
{{ block.ta }}
{% endcapture %}
<div class="col-{{ block.xs }} col-md-{{ block.md }} col-xl-{{ block.xl }}">
<div class="fs-xs fw-light fc-mute {{ class }} mt-{{ block.mt }} mb-{{ block.mb }}">
{{ block.content }}
</div>
</div>
{% endif %}
<div
class="col-{{ section.settings.xs }} col-md-{{ section.settings.md }} col-xl-{{ section.settings.xl }}"
data-controller="form"
data-form-type-value="newsletter:advert"
data-form-response-value="toggle"
data-form-success-value="{{ section.settings.response }}"
data-form-klaviyo-value="{{ section.settings.action }}">
{{ product.collections | append: '' | append: article.image.src }}
{% if x == customer.email %}{% endif %}
<form
class="{{ section.settings.row }} ac-center py-{{ section.settings.py }}"
method="post"
accept-charset="UTF-8"
data-action="form#onSubmit"
data-form-target="form"
action="{{ request.path }}"
novalidate>
{% for block in section.blocks %}
{% render 'form-fields' with block.settings as block, type: block.type %}
{% endfor %}
{% if section.blocks and section.settings.btn_label %}
<div class="col-12 {{ section.settings.btn_position }}">
<button
class="btn btn-black mt-4 py-3 w-100 tc fw-light"
type="submit"
data-form-target="submit">
{{ section.settings.btn_label }}
</button>
</div>
{% endif %}
</form>
<div class="tc fs-lg ac-center d-none">
<div data-form-target="response">
{% # FORM RESPONSE > DYNAMICALLY POPULATED %}
</div>
{% if section.settings.cta_label and section.settings.cta %}
<a
href="{{ section.settings.cta }}"
data-spx-disable="true"
class="btn btn-link btn-black fw-light my-3">
{{ section.settings.cta_label }}
</a>
{% endif %}
{% if section.settings.help %}
<p class="fs-xs my-3">
If you did not receive the discount code, check your spam mailbox.
Contact
<a href="mailto:webshop@brixtoltextiles.com">
support
</a>
if you need assistance.
</p>
{% endif %}
</div>
</div>
{% schema %}
{
"name": "Form",
"tag": "section",
"class": "row g-0 jc-center",
"settings": [
{
"type": "header",
"content": "Form Type",
"info": "Define the type of form to generate. This required."
},
{
"type": "text",
"id": "action",
"label": "Action",
"info": "The action of the form - Define Klaviyo List ID here (if required)."
},
{
"type": "select",
"id": "type",
"label": "Type",
"options": [
{
"value": "klaviyo",
"label": "Klaviyo"
},
{
"value": "contact",
"label": "Contact"
},
{
"value": "other",
"label": "Other"
}
]
},
{
"type": "header",
"content": "Response",
"info": "The response to show after the form was submitted successfully."
},
{
"type": "richtext",
"id": "response",
"label": "Response",
"info": "Shown when form was successful. Use template ${} literals to target form values."
},
{
"type": "text",
"id": "cta_label",
"label": "CTA Label",
"placeholder": "Continue Shopping",
"info": "Add a CTA Button to the end of the response"
},
{
"type": "url",
"id": "cta",
"label": "CTA Link",
"info": "CTA Link in the response"
},
{
"type": "checkbox",
"id": "private_sale",
"label": "Private Sale",
"info": "Whether or not this form should show private sale links",
"default": false
},
{
"type": "checkbox",
"id": "help",
"label": "Show Help",
"info": "Whether or not to append help text below response."
},
{
"type": "header",
"content": "Submit Button",
"info": "The Submit button which sends the form"
},
{
"type": "text",
"label": "Label",
"id": "btn_label"
},
{
"type": "select",
"id": "btn_position",
"label": "Position",
"default": "tr",
"options": [
{
"value": "tl",
"label": "Left"
},
{
"value": "tc",
"label": "Center"
},
{
"value": "tr",
"label": "Right"
}
]
},
{
"type": "header",
"content": "Grid Layout",
"info": "Control layout and alignment of the grid system"
},
{
"type": "select",
"id": "align",
"label": "Alignment",
"default": "m-auto",
"options": [
{
"value": "mr-auto",
"label": "Left"
},
{
"value": "m-auto",
"label": "Center"
},
{
"value": "ml-auto",
"label": "Right"
}
]
},
{
"type": "range",
"id": "xl",
"label": "Desktop Columns",
"info": "The amount of columns in desktop",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "md",
"label": "Tablet Columns",
"info": "The amount of columns in tablet",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "xs",
"label": "Mobile Columns",
"info": "The amount of columns in mobile",
"min": 1,
"max": 12,
"step": 1,
"default": 12
}
],
"blocks": [
{
"type": "heading",
"name": "Heading",
"limit": 1,
"settings": [
{
"type": "header",
"content": "Heading",
"info": "Provide a heading and description paragraph."
},
{
"type": "text",
"id": "title",
"label": "Title"
},
{
"type": "text",
"id": "subtitle",
"label": "SubTitle"
},
{
"type": "richtext",
"id": "description",
"label": "Description"
},
{
"type": "header",
"content": "Gutter Y",
"info": "Control the margin top and bottom spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 0
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
}
]
},
{
"type": "paragraph",
"name": "Paragraph",
"settings": [
{
"type": "header",
"content": "Paragraph",
"info": "Renders a block of text content"
},
{
"type": "richtext",
"id": "content",
"label": "Content",
"info": "The text to render"
},
{
"type": "header",
"content": "Gutter",
"info": "Control gutter spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 3
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
},
{
"type": "header",
"content": "Layout",
"info": "Control layout and alignment of the grid system"
},
{
"type": "range",
"id": "xl",
"label": "Desktop Columns",
"info": "The amount of columns in desktop",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "md",
"label": "Tablet Columns",
"info": "The amount of columns in tablet",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "xs",
"label": "Mobile Columns",
"info": "The amount of columns in mobile",
"min": 1,
"max": 12,
"step": 1,
"default": 12
}
]
},
{
"type": "textarea",
"name": "Textarea",
"settings": [
{
"type": "header",
"content": "Textarea Input",
"info": "Renders a Textarea container"
},
{
"type": "text",
"id": "label",
"label": "Label",
"info": "Defines the label of the textarea"
},
{
"type": "text",
"id": "placeholder",
"label": "Placeholder",
"info": "Describes an expected value for the textarea"
},
{
"type": "checkbox",
"id": "require",
"label": "Require",
"default": true,
"info": "Whether or input is required"
},
{
"type": "checkbox",
"id": "required",
"info": "The form requires this input to be filled",
"label": "Required"
},
{
"type": "header",
"content": "Layout",
"info": "Control layout and alignment of the grid system."
},
{
"type": "range",
"id": "height",
"label": "Height",
"info": "The height of the textarea input",
"min": 1,
"max": 100,
"step": 1,
"default": 25
},
{
"type": "header",
"content": "Gutter",
"info": "Control gutter spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 0
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
}
]
},
{
"type": "text",
"name": "Input",
"settings": [
{
"type": "header",
"content": "Input Field",
"info": "A text input field to render in the form."
},
{
"type": "text",
"id": "placeholder",
"label": "Placeholder",
"info": "Describes an expected value of the input"
},
{
"type": "text",
"id": "name",
"label": "Name",
"info": "Defines the name of the input field"
},
{
"type": "select",
"id": "autocomplete",
"label": "Autocomplete",
"info": "See [Values](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values) for reference.",
"default": "off",
"options": [
{
"value": "off",
"label": "Disabled"
},
{
"value": "on",
"label": "Enabled"
},
{
"value": "name",
"label": "Name"
},
{
"value": "given-name",
"label": "First Name"
},
{
"value": "additional-name",
"label": "Middle name"
},
{
"value": "family-name",
"label": "Last Name"
},
{
"value": "nickname",
"label": "Nickname or Handle"
},
{
"value": "organization",
"label": "Company or Oganization name"
},
{
"value": "street-address",
"label": "Street Address"
},
{
"value": "postal-code",
"label": "Postal Code"
}
]
},
{
"type": "checkbox",
"id": "required",
"label": "Required",
"default": false,
"info": "Whether or not the value is required"
},
{
"type": "header",
"content": "Validation",
"info": "Validation control for expected input text value"
},
{
"type": "range",
"id": "minlength",
"label": "Minimum Length",
"info": "The minimum number of characters required",
"min": 1,
"max": 75,
"step": 1,
"default": 1
},
{
"type": "range",
"id": "maxlength",
"label": "Maximum Length",
"info": "The maximum number of characters required",
"min": 1,
"max": 75,
"step": 1,
"default": 50
},
{
"type": "header",
"content": "Gutter",
"info": "Control gutter spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 0
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
},
{
"type": "header",
"content": "Layout",
"info": "Control layout and alignment of the grid system"
},
{
"type": "range",
"id": "xl",
"label": "Desktop Columns",
"info": "The amount of columns in desktop",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "md",
"label": "Tablet Columns",
"info": "The amount of columns in tablet",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "xs",
"label": "Mobile Columns",
"info": "The amount of columns in mobile",
"min": 1,
"max": 12,
"step": 1,
"default": 12
}
]
},
{
"type": "email",
"name": "Email",
"limit": 1,
"settings": [
{
"type": "header",
"content": "E-mail Address",
"info": "An E-mail address input field to render in the form."
},
{
"type": "text",
"id": "placeholder",
"label": "Placeholder",
"info": "The placeholder (label) value",
"default": "Email Address"
},
{
"type": "text",
"id": "name",
"label": "Name",
"info": "Defines the name of the input field",
"default": "email"
},
{
"type": "checkbox",
"id": "autocomplete",
"label": "Autocomplete",
"default": false,
"info": "Whether or not to allow autocompletion"
},
{
"type": "checkbox",
"id": "required",
"label": "Required",
"default": true,
"info": "Whether or not the value is required"
},
{
"type": "checkbox",
"id": "multiple",
"label": "Multiple",
"default": false,
"info": "Whether to allow multiple emails be defined"
},
{
"type": "header",
"content": "Gutter",
"info": "Control gutter spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 0
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
},
{
"type": "header",
"content": "Layout",
"info": "Control layout and alignment of the grid system"
},
{
"type": "range",
"id": "xl",
"label": "Desktop Columns",
"info": "The amount of columns in desktop",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "md",
"label": "Tablet Columns",
"info": "The amount of columns in tablet",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "xs",
"label": "Mobile Columns",
"info": "The amount of columns in mobile",
"min": 1,
"max": 12,
"step": 1,
"default": 12
}
]
},
{
"type": "tel",
"name": "Telephone",
"settings": [
{
"type": "header",
"content": "Phone Number Field",
"info": "A telephone number input field to render in the form."
},
{
"type": "text",
"id": "name",
"label": "Name",
"info": "Defines the name of the telephone field",
"default": "telephone"
},
{
"type": "text",
"id": "placeholder",
"label": "Placeholder",
"info": "Describes an expected value of the input"
},
{
"type": "checkbox",
"id": "autocomplete",
"label": "Autocomplete",
"default": false,
"info": "Whether or not to allow autocompletion"
},
{
"type": "checkbox",
"id": "code",
"label": "Country Code",
"default": false,
"info": "Whether or not to provide a country calling code"
},
{
"type": "checkbox",
"id": "require",
"label": "Require",
"default": false,
"info": "Whether or not the value is required"
},
{
"type": "header",
"content": "Gutter",
"info": "Control gutter spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 0
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
},
{
"type": "header",
"content": "Layout",
"info": "Control layout and alignment of the grid system"
},
{
"type": "range",
"id": "xl",
"label": "Desktop Columns",
"info": "The amount of columns in desktop",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "md",
"label": "Tablet Columns",
"info": "The amount of columns in tablet",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "xs",
"label": "Mobile Columns",
"info": "The amount of columns in mobile",
"min": 1,
"max": 12,
"step": 1,
"default": 12
}
]
},
{
"type": "number",
"name": "Number",
"settings": [
{
"type": "header",
"content": "Number Input",
"info": "Renders a Numeric input field"
},
{
"type": "text",
"id": "name",
"label": "Name",
"info": "Defines the name of the number field"
},
{
"type": "text",
"id": "placeholder",
"label": "Placeholder",
"info": "The placeholder to render"
},
{
"type": "checkbox",
"id": "required",
"info": "The form requires this input to be filled",
"label": "Required"
},
{
"type": "header",
"content": "Gutter",
"info": "Control gutter spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 0
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
}
]
},
{
"type": "switch",
"name": "Switch",
"settings": [
{
"type": "header",
"content": "Switch Input",
"info": "A toggle switch (checkbox) input field"
},
{
"type": "text",
"id": "name",
"label": "Name",
"info": "Defines the name of the switch field"
},
{
"type": "text",
"id": "label",
"label": "Label",
"info": "The label which describes the switch"
},
{
"type": "checkbox",
"id": "consent",
"info": "Whether or not the switch is a consent",
"label": "Consent (GDRP)",
"default": false
},
{
"type": "checkbox",
"id": "default",
"info": "The default state to use",
"label": "on/off",
"default": false
},
{
"type": "header",
"content": "Layout and Sizing",
"info": "Control layout, style and/or alignment of the switch"
},
{
"type": "select",
"id": "row",
"label": "Position",
"default": "jc-between",
"options": [
{
"value": "jc-between",
"label": "Between"
},
{
"value": "jc-start",
"label": "Left"
},
{
"value": "jc-center",
"label": "Center"
},
{
"value": "jc-end",
"label": "Right"
}
]
},
{
"type": "select",
"id": "size",
"label": "Sizing",
"default": "fm-switch",
"options": [
{
"value": "fm-switch-sm",
"label": "Between"
},
{
"value": "fm-switch",
"label": "Medium"
},
{
"value": "fm-switch-lg",
"label": "Large"
}
]
},
{
"type": "header",
"content": "Gutter",
"info": "Control gutter spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 0
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
}
]
},
{
"type": "checkbox",
"name": "Checkbox",
"settings": [
{
"type": "header",
"content": "Checkbox Input",
"info": "Renders a traditional Checkout input field"
},
{
"type": "text",
"id": "label",
"label": "Label",
"info": "The Label to render atop of the input (optional)"
},
{
"type": "checkbox",
"id": "required",
"info": "The form requires this input to be filled",
"label": "Required"
},
{
"type": "checkbox",
"id": "validate",
"info": "Whether or not this input should be validated",
"label": "Validate"
},
{
"type": "header",
"content": "Gutter",
"info": "Control gutter spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 0
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
}
]
},
{
"type": "dropdown",
"name": "Dropdown",
"settings": [
{
"type": "header",
"content": "Option Dropdown",
"info": "A list of options to select from"
},
{
"type": "textarea",
"id": "items",
"label": "items",
"default": "Item 1\nItem 2\nItem 3\netc etc",
"info": "Separate each item to render on newline"
},
{
"type": "text",
"id": "name",
"label": "Name",
"info": "Defines the name of the dropdown."
},
{
"type": "text",
"id": "placeholder",
"label": "Placeholder",
"info": "Defines the placeholder of the dropdown."
},
{
"type": "checkbox",
"id": "required",
"label": "Required",
"default": true,
"info": "Whether or not to require a selection"
},
{
"type": "header",
"content": "Gutter",
"info": "Control gutter spacing"
},
{
"type": "range",
"id": "mt",
"label": "Top",
"info": "The margin to apply from top",
"min": 0,
"max": 5,
"step": 1,
"default": 0
},
{
"type": "range",
"id": "mb",
"label": "Bottom",
"info": "The margin to apply from bottom",
"min": 0,
"max": 5,
"step": 1,
"default": 3
},
{
"type": "header",
"content": "Layout",
"info": "Control layout and alignment of the grid system"
},
{
"type": "range",
"id": "xl",
"label": "Desktop Columns",
"info": "The amount of columns in desktop",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "md",
"label": "Tablet Columns",
"info": "The amount of columns in tablet",
"min": 1,
"max": 12,
"step": 1,
"default": 12
},
{
"type": "range",
"id": "xs",
"label": "Mobile Columns",
"info": "The amount of columns in mobile",
"min": 1,
"max": 12,
"step": 1,
"default": 12
}
]
},
{
"type": "hidden",
"name": "Hidden",
"settings": [
{
"type": "header",
"content": "Hidden Field",
"info": "A hidden text field pre-populated with a value"
},
{
"type": "text",
"id": "value",
"label": "Value",
"info": "The value of the hidden input field"
},
{
"type": "text",
"id": "name",
"label": "Name",
"info": "Defines the name of the input field"
}
]
}
]
}
{% endschema %}
/* eslint-disable no-unused-vars */
import { IForm, Types, ResponseType, IFormCache } from 'types/forms';
import { Controller } from '@hotwired/stimulus';
import * as customer from 'application/customer';
import { keys, values } from 'utils/native';
export class Form extends Controller {
/**
* Data Store
*/
static store: Map<string, { [field: string]: IForm }> = new Map();
/* -------------------------------------------- */
/* STIMULUS METHODS */
/* -------------------------------------------- */
/**
* Stimulus Values
*/
static values = {
id: String,
type: String,
errors: Array,
submit: String,
focus: String,
klaviyo: String,
success: String,
response: {
type: String,
default: 'append'
},
valid: {
type: Boolean,
default: false
},
session: {
type: Boolean,
default: true
}
};
/**
* Stimulus Targets
*/
static targets = [
'form',
'field',
'dropdown',
'response',
'submit'
];
/**
* Stimulus Initialize
*/
initialize () {
if (!this.hasIdValue) {
this.idValue = Math.random().toString(36).slice(2);
}
if (!this.hasResponseTarget) {
console.error('Missing "data-form-target" element');
}
if (this.responseValue === 'toggle' && !this.hasFormTarget) {
console.error('When using "toggle" response, a `data-form-target="form"` target is required');
}
if (!this.hasTypeValue) {
console.error('Missing "data-form-type-value" value');
} else {
if (
this.typeValue === 'newsletter:advert' ||
this.typeValue === 'newsletter:cart' ||
this.typeValue === 'newsletter:footer' ||
this.typeValue === 'notification:instock' ||
this.typeValue === 'notification:restock'
) {
if (!this.hasKlaviyoValue) {
console.error('Missing "data-form-klaviyo-value" attribute');
}
} else if (
this.typeValue === 'shopify:contact' ||
this.typeValue === 'api:discount'
) {
if (this.hasKlaviyoValue) {
console.error('Invalid "data-form-klaviyo-value" attribute provided');
}
}
}
}
/**
* Stimulus Connect
*/
connect (): void {
this.validValue = false;
this.submitValue = this.submitTarget.innerText;
this.submitTarget.addEventListener('click', this.onClickSubmit.bind(this));
this.form = this.setFormState();
this.setSubscribed();
}
/**
* Stimulus Disconnect
*/
disconnect () {
}
/* -------------------------------------------- */
/* GETTERS / SETTERS */
/* -------------------------------------------- */
/**
* Input Element - Returns the currenct active `<input>` element
*/
get field (): HTMLInputElement { return this.fieldTargets[this.state.index]; }
/**
* Form Element - Returns the currenct active `<form>` element
*/
get form () { return Form.store.get(this.idValue); }
/**
* Form Element - Sets the current active `<form>` element
*/
set form (state: { [field: string]: IForm }) { Form.store.set(this.idValue, state); }
/**
* State Reference - Returns the state of the form
*/
get state () { return this.form[this.focusValue]; }
/**
* State Reference - Returns the state of the form
*/
get dropdown () { return this.dropdownTargets[this.state.dropdown]; }
/**
* Set Subscribed state - used when we have known reference of a customer
* session.
*/
setSubscribed () {
if (this.typeValue === 'newsletter:footer') {
if (this.isFormValid()) {
this.submitTarget.textContent = window.i18n.newsletter.subscribed;
this.submitTarget.setAttribute('disabled', 'true');
}
}
}
/**
* Creates a workable store of the form which will maintain
* a persisted state model which will be used to validate and
* determine which actions should take place on each entry.
*
* _Each form stored in the static `store` Map._
*/
setFormState () {
/** The dropdown target index */
let dindex: number = -1;
return this.fieldTargets.reduce((form, field, index) => {
const { name, type } = field;
if (!name) {
console.error('Missing "name" attribute on form element:', field);
return form;
}
if (!type) {
console.error('Missing "type" attribute on form element:', field);
return form;
}
let dropdown: number = -1;
let feedback: HTMLDivElement;
const hidden = type === 'hidden';
if (!hidden) {
if (!field.hasAttribute('data-action')) {
console.error(`Missing "data-action" on form field ${name}`, field);
return form;
}
if (field.autofocus === true) this.focusValue = field.name;
if (field.getAttribute('data-action').indexOf('dropdown#select') > -1) {
if (name in form) return form;
dropdown = dindex = dindex + 1;
feedback = this.setValidators(this.dropdownTargets[dindex]);
} else {
if (name in form) {
console.error(`The ${name} is not unique on form element:`, field);
return form;
} else {
feedback = this.setValidators(field.parentElement);
}
}
}
const required = field.required;
form[name] = {
name,
index,
required,
feedback,
dropdown,
status: 1,
type,
interacted: false,
valid: required === false,
message: '',
value: undefined
};
if (hidden) {
form[name].value = field.value;
}
if (field.hasAttribute('data-form-session')) {
const value = field.getAttribute('data-form-session');
if (value in customer.session && typeof customer.session[value] === 'string') {
field.value = customer.session[value];
form[name].value = field.value;
form[name].valid = true;
}
}
return form;
}, <IFormCache>{});
}
/**
* Generates feedback nodes in each field wrapped element
* of _typically_ this will be the parent grid column. The
* feedback nodes are appended and can be queried using
* `lastElementChild` or alternatively via the `feedback`
* property maintained in the form model.
*
* This can likely be re-thought for input elements which have
* no validation.
*/
setValidators (field: HTMLElement) {
const feedback = document.createElement('div');
// append validation feedback message to the `<label>` node
if (!field.contains(feedback)) {
feedback.classList.add('feedback');
field.appendChild(feedback);
}
return feedback;
}
/**
* Replaces the response with form values which reference
* fields using template literals via the Section theme editor.
*/
setResponse (value?: string) {
if (this.hasSuccessValue && typeof value === 'undefined') {
const regex = new RegExp('\\$\\{\\b(' + keys(this.form).join('|') + ')\\b\\}', 'g');
this.successValue = this.successValue.replace(regex, (value) => {
return this.form[value.slice(2, -1)].value as string;
});
} else {
this.successValue = value;
}
}
/**
* Set and Reset the feedback nodes innerText. This
* gives programmatic control and assigns the validation
* response to field.
*/
setFeedback <T = HTMLElement> (feedback: T, response: string) {
if (feedback instanceof HTMLElement) {
if (response === null || response === undefined) {
feedback.innerText = '';
} else {
feedback.innerText = response;
}
}
}
/**
* Generates feedback nodes in each field wrapped element
* of _typically_ this will be the parent grid column. The
* feedback nodes are appended and can be queried using
* `lastElementChild` or alternatively via the `feedback`
* property maintained in the form model.
*/
setHidden (field: HTMLElement) {
const hidden = document.createElement('input');
// append validation feedback message to the `<label>` node
if (!field.contains(hidden)) {
hidden.classList.add('d-none');
field.appendChild(hidden);
}
return hidden;
}
/* -------------------------------------------- */
/* SETUP */
/* -------------------------------------------- */
/**
* This logic will validate the form upon the submit button
* being clicked. If `validValue` is `true` then validation
* will be skipped and form is passed to `onSubmit`.
*/
onClickSubmit (event: SubmitEvent) {
if (this.typeValue === 'shopify:contact') {
if (this.validValue) {
return;
}
} else {
event.preventDefault();
}
if (this.validValue) return this.onSubmit();
this.submitTarget.setAttribute('disabled', 'true');
if (this.validValue === false) {
for (const field of values(this.form)) {
if (field.valid === false && field.required) {
if (field.dropdown > -1) {
if (!this.dropdownTargets[field.dropdown].classList.contains('is-invalid')) {
this.dropdownTargets[field.dropdown].classList.add('is-invalid');
}
} else {
if (!this.fieldTargets[field.index].classList.contains('is-invalid')) {
this.fieldTargets[field.index].classList.add('is-invalid');
}
}
}
}
}
}
private responseType (value?: string) {
this.setResponse(value);
this.submitTarget.classList.remove('is-loading');
this.responseTarget.innerHTML = this.successValue;
if (this.responseValue === 'toggle') {
this.submitTarget.classList.add('d-none');
this.formTarget.classList.add('d-none');
this.responseTarget.parentElement.classList.remove('d-none');
}
}
/**
* Fires upon an input change event. `onChange()` is used
* on the following input fields:
*
* - checkbox
* - radio
* - select
*/
public onSubmit () {
switch (this.typeValue) {
case 'newsletter:advert':
case 'newsletter:cart':
case 'newsletter:footer':
case 'notification:instock':
case 'notification:restock':
return this.klaviyo();
case 'shopify:contact':
return this.contact();
case 'api:discount':
return this.discount();
}
};
/**
* Checks all form fields are valid. Enables / Disables
* the submit button state.
*/
private isFormValid () {
const valid = values(this.form).every(({ valid }) => valid === true);
if (valid) {
this.submitTarget.removeAttribute('disabled');
this.validValue = true;
} else {
if (!this.submitTarget.hasAttribute('disabled')) {
this.submitTarget.setAttribute('disabled', 'true');
this.validValue = false;
}
}
console.log(this.form);
return this.validValue;
}
/**
* Valid class - Toggles the input valid and invalid class
* name on the the field element. Also sets the `this.state.valid`
* and the hides the feedback node. Passes to `this.isFormValid()`
* as the final callback.
*/
private valid () {
if (!this.state.valid) this.state.valid = true;
const field = this.state.dropdown > -1
? this.dropdown
: this.field;
if (field.classList.contains('is-invalid')) {
field.classList.remove('is-invalid');
}
if (!field.classList.contains('is-valid')) {
field.classList.add('is-valid');
}
if (this.state.feedback.classList.contains('is-invalid')) {
this.state.feedback.classList.remove('is-invalid');
}
if (!this.state.feedback.classList.contains('is-valid')) {
this.state.feedback.classList.add('is-valid');
}
return this.isFormValid();
}
/**
* Invalid class - Toggles the input invalid and valid class
* name on the the field element. Also sets the `this.state.valid`
* and the hides the feedback node. Passes to `this.isFormValid()`
* as the final callback.
*/
private invalid () {
if (this.state.valid === true) this.state.valid = false;
const field = this.state.dropdown > -1
? this.dropdown
: this.field;
if (field.classList.contains('is-valid')) {
field.classList.remove('is-valid');
}
if (!field.classList.contains('is-invalid')) {
field.classList.add('is-invalid');
}
if (this.state.feedback.classList.contains('is-valid')) {
this.state.feedback.classList.remove('is-valid');
}
if (!this.state.feedback.classList.contains('is-invalid')) {
this.state.feedback.classList.add('is-invalid');
}
return this.isFormValid();
}
/**
* Checks all form field inputs are valid. Enables / Disables
* the submit button state.
*
* @this {IForm}
* @param {HTMLInputElement} target
*/
private isInputValid (target: HTMLInputElement) {
if (target.checkValidity()) {
this.valid();
} else {
this.invalid();
}
return this.isFormValid();
}
/* -------------------------------------------- */
/* ACTIONS */
/* -------------------------------------------- */
/**
* Fires upon input entry of text form elements and
* will dispatch to appropriate function in this class.
* for handling and validation.
*/
onInput ({ target }: Event) {
if (target instanceof HTMLLabelElement) {
this.focusValue = target.getAttribute('for');
this.state.valid = true;
this.state.value = target.innerText;
this.field.value = this.state.value;
if (!this.state.interacted) this.state.interacted = true;
return this.isFormValid();
}
if (target instanceof HTMLInputElement) {
const type = target.getAttribute('type');
// show the validation response
this.focusValue = target.name;
this.state.value = type === 'checkbox' ? target.checked : target.value;
if (this.typeValue === 'newsletter:footer') {
if (this.submitTarget.innerText !== this.submitValue) {
this.submitTarget.innerText = this.submitValue;
}
}
if (this.state.status !== 1) this.responseTarget.innerText = '';
if (!this.state.interacted) this.state.interacted = true;
if (type in this) return this[type](target);
}
return this.isFormValid();
}
/* -------------------------------------------- */
/* FORM ELEMENTS */
/* -------------------------------------------- */
/**
* Text Input
*
* Validate text input type
*/
text (target: HTMLInputElement) {
if (this.state.interacted && target.minLength >= 0) {
if (target.minLength > target.value.length) {
return this.invalid();
} else if (target.value.length > target.maxLength) {
return this.invalid();
}
}
this.state.feedback.innerText = target.validationMessage;
return target.checkValidity()
? this.valid()
: this.invalid();
}
/**
* Validates checkbox input fields. When a checkbox
* field contains a `required` attribute the field
* must be checked or form submission will be disabled.
*/
checkbox (target: HTMLInputElement) {
if (!this.state.required) {
if (target.required) this.state.valid = target.checked;
} else {
if (target.hasAttribute('checked')) {
target.removeAttribute('checked');
target.checked = false;
this.state.valid = false;
} else {
target.setAttribute('checked', '');
target.checked = true;
this.state.valid = true;
}
}
if (this.field.classList.contains('is-invalid')) {
this.field.classList.remove('is-invalid');
}
return this.isFormValid();
}
/**
* Validate textarea
*/
textarea (target: HTMLInputElement) {
return this.isInputValid(target);
}
/**
* Validate email type text input form field. Applies an
* extra layer of validation and/or helpers upon user input.
*/
email (target: HTMLInputElement) {
if (this.state.interacted && target.value.length === 0) return this.invalid();
if (target.value.endsWith('@brixtoltextiles.com') || target.value.endsWith('@sunday-seven.com')) {
this.state.feedback.innerText = window.i18n.newsletter.error_team;
return this.invalid();
}
if (target.validationMessage.length === 0) {
if (!/\w+@[0-9a-zA-Z_]+?\.[a-zA-Z]{2,}$/.test(target.value)) {
this.state.feedback.innerText = 'Invalid email, please ensure correct email is provided';
return this.invalid();
} else {
this.state.feedback.innerText = '';
return target.checkValidity() ? this.valid() : this.invalid();
}
} else {
this.state.feedback.innerText = target.validationMessage;
return target.checkValidity() ? this.valid() : this.invalid();
}
}
password () {
}
radio (target: HTMLInputElement) {
const field = this.state.dropdown > -1 ? this.dropdown : this.field;
if (target.checked) {
this.state.value = target.value;
this.state.valid = true;
} else {
this.state.valid = false;
}
if (field.classList.contains('is-invalid')) {
field.classList.remove('is-invalid');
}
if (!field.classList.contains('is-valid')) {
field.classList.add('is-valid');
}
return this.isFormValid();
}
/**
* Validates Phone Number
*/
tel (target: HTMLInputElement) {
if (!target.required) this.state.valid = true;
return this.isInputValid(target);
}
file () {
}
/* -------------------------------------------- */
/* FORM SUMBMISSION */
/* -------------------------------------------- */
async discount () {}
async contact () {}
hasSubscribed = () => {
this.state.feedback.innerText = '';
this.fieldTargets[this.state.index].classList.remove('is-valid');
this.submitTarget.textContent = window.i18n.newsletter.subscribed;
this.submitTarget.setAttribute('disabled', 'true');
};
/**
* Submits the form inputs to Klaviyo.
*/
async klaviyo () {
this.submitTarget.classList.add('is-loading');
if (this.typeValue === 'newsletter:footer') {
this.field.classList.remove('is-valid');
}
const fields = [];
const body = new URLSearchParams();
for (const field in this.form) {
if (field === 'first_name' || field === 'last_name' || field === 'phone_number') {
body.append(`$${field}`, `${this.form[field].value}`);
} else {
fields.push(field);
body.append(field, `${this.form[field].value}`);
}
}
body.append('g', this.klaviyoValue);
body.append('$fields', fields.join(','));
try {
const response = await fetch(window.shop.api.klaviyo, {
body,
method: 'POST',
headers: {
'Access-Control-Allow-Headers': '*',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
}
});
const { data, errors } = await response.json();
if (data.is_subscribed === false && typeof data.email === 'string') {
this.responseType();
this.state.status = 2;
if (this.typeValue === 'newsletter:footer') setTimeout(this.hasSubscribed, 60000);
} else if (data.is_subscribed === true) { // Exists
this.responseType(window.i18n.newsletter.error_exists);
this.state.status = 3;
if (this.typeValue === 'newsletter:footer') this.hasSubscribed();
} else if (errors.length > 0) {
this.responseType(errors.join('<br>'));
this.state.status = 4;
}
} catch (e) {
console.error(e);
this.responseType(window.i18n.newsletter.error_404);
this.state.status = 4;
}
}
/* -------------------------------------------- */
/* VALUE TYPES */
/* -------------------------------------------- */
/**
* Stimulus: The generated form identifer value
*/
idValue: string;
/**
* Stimulus: Whether or not the form passed an id value
*/
hasIdValue: boolean;
/**
* Stimulus: The type of form.
*/
typeValue: Types;
/**
* Stimulus: Whether or not the form has a type value (required)
*/
hasTypeValue: boolean;
/**
* The success response value
*/
successValue: string;
/**
* Whether or not the form has a response value, when `false` then
* the `i18n` locales will be used.
*/
hasSuccessValue: boolean;
/**
* How the response value should be handled. Defaults to `append`
*/
responseValue: ResponseType;
/**
* Whether or not an `responseTypeValue` was provided.
*/
hasResponseValue: boolean;
/**
* Stimulus: The Klaviyo List ID to target
*/
klaviyoValue: string;
/**
* Stimulus: Whether or not the form has a klaviyo list id
*/
hasKlaviyoValue: boolean;
/**
* The submit button label
*/
submitValue: string;
/**
* Stimulus: The input to focus, this identifies the current field
*/
focusValue: string;
/**
* Stimulus: Attach known customer session values to the form submission.
*/
sessionValue: boolean;
/**
* Stimulus: Whether session was asserted, will default to `true` if not passed.
*/
hasSessionValue: boolean;
/* -------------------------------------------- */
/* INTERNAL VALUE TYPES */
/* -------------------------------------------- */
/**
* Stimulus (iternal): Whether or not the form is valid
*/
validValue: boolean;
/* -------------------------------------------- */
/* TARGET TYPES */
/* -------------------------------------------- */
/**
* Stimulus: The response element to append text within
*/
responseTarget: HTMLElement;
/**
* Stimulus: Whether or not response targer exists
*/
hasResponseTarget: boolean;
/**
* Stimulus: The form element
*/
formTarget: HTMLFormElement;
/**
* Stimulus: Whether or not te form target exists
*/
hasFormTarget: boolean;
/**
* Stimulus: Dropdown targets within form
*/
dropdownTargets: HTMLElement[];
/**
* Stimulus: Whether or not dropdown target/s exist
*/
hasDropdownTarget: boolean;
/**
* Stimulus: Field targets
*/
fieldTargets: HTMLInputElement[];
/**
* Stimulus: Submit button Target
*/
submitTarget: HTMLElement;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment