Skip to content

Instantly share code, notes, and snippets.

@ShinyObjectLabs
Last active June 5, 2025 08:12

Revisions

  1. ShinyObjectLabs revised this gist Jul 2, 2023. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -902,7 +902,7 @@ const Spinner = (props) => {
    )
    }

    export const basePropertyControls = {
    const basePropertyControls = {
    url: {
    title: "Url",
    type: ControlType.String,
    @@ -1483,4 +1483,5 @@ BaseForm.defaultProps = {
    ],
    }

    export default BaseForm
    export default BaseForm
    export { basePropertyControls }
  2. ShinyObjectLabs revised this gist Jul 2, 2023. 1 changed file with 26 additions and 6 deletions.
    32 changes: 26 additions & 6 deletions BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -79,6 +79,7 @@ interface Props extends Omit<HTMLMotionProps<"div">, "layout"> {
    style: CSSProperties
    styles: Styles
    extraHeaders?: Record<string, string>
    extraFields?: Record<string, any>
    onSubmit?: () => void
    }

    @@ -134,6 +135,7 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    button,
    styles,
    extraHeaders,
    extraFields,
    style,
    onSubmit,
    }: Props) {
    @@ -251,6 +253,8 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    }
    }

    const formData = new FormData(event.target)

    let requestOptions = {
    method: method,
    headers: headers,
    @@ -260,7 +264,7 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    // Add form fields to URL for GET requests
    const urlSearchParams = new URLSearchParams()

    for (const [name, value] of new FormData(event.target)) {
    for (const [name, value] of formData) {
    urlSearchParams.append(name, value.toString())
    }

    @@ -273,15 +277,21 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    headers.append("accept", "application/json")
    }

    const formData = new FormData(event.target)

    if (contentType === "application/x-www-form-urlencoded") {
    const urlSearchParams = new URLSearchParams()

    for (const [name, value] of formData) {
    urlSearchParams.append(name, value.toString())
    }

    if (extraFields) {
    for (const [key, value] of Object.entries(
    extraFields
    )) {
    urlSearchParams.append(key, value.toString())
    }
    }

    requestOptions["body"] = urlSearchParams.toString()
    } else if (contentType === "application/json") {
    const bodyObject = {}
    @@ -290,6 +300,14 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    bodyObject[name] = value
    }

    if (extraFields) {
    for (const [key, value] of Object.entries(
    extraFields
    )) {
    bodyObject[key] = value
    }
    }

    requestOptions["body"] = JSON.stringify(bodyObject)
    }
    }
    @@ -884,7 +902,7 @@ const Spinner = (props) => {
    )
    }

    addPropertyControls(BaseForm, {
    export const basePropertyControls = {
    url: {
    title: "Url",
    type: ControlType.String,
    @@ -1363,7 +1381,9 @@ addPropertyControls(BaseForm, {
    },
    },
    },
    })
    }

    addPropertyControls(BaseForm, basePropertyControls)

    const defaultStyle: React.CSSProperties = {
    WebkitAppearance: "none",
    @@ -1463,4 +1483,4 @@ BaseForm.defaultProps = {
    ],
    }

    export default BaseForm
    export default BaseForm
  3. ShinyObjectLabs revised this gist Jun 16, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -519,7 +519,7 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    {label(input)}
    <motion.textarea
    name={input.name}
    value={input.value}
    defaultValue={input.value}
    placeholder={input.placeholder}
    className={`${VERSION} framer-custom-input`}
    onChange={handleChange}
  4. ShinyObjectLabs revised this gist Jun 15, 2023. 1 changed file with 43 additions and 2 deletions.
    45 changes: 43 additions & 2 deletions BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -58,6 +58,7 @@ enum FieldType {
    TextArea = "textarea",
    Select = "select",
    Checkbox = "checkbox",
    Radio = "radio",
    Time = "time",
    Week = "week",
    Month = "month",
    @@ -465,8 +466,9 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    >
    {label(input)}
    <motion.input
    type={input.type}
    name={input.name}
    type={input.type}
    defaultValue={input.value}
    placeholder={input.placeholder}
    className={`${VERSION} framer-custom-input`}
    onChange={handleChange}
    @@ -517,6 +519,7 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    {label(input)}
    <motion.textarea
    name={input.name}
    value={input.value}
    placeholder={input.placeholder}
    className={`${VERSION} framer-custom-input`}
    onChange={handleChange}
    @@ -652,9 +655,42 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    <motion.input
    name={input.name}
    type="checkbox"
    value={input.value || "on"}
    required={input.required}
    style={{
    margin: "0px 8px 0px 4px",
    }}
    />
    {input.label}
    </label>
    </div>
    )
    }

    function radioInput(input) {
    return (
    <div
    style={{
    gridColumn: `span ${getInputSpan(input)}`,
    }}
    >
    <label
    style={{
    display: "flex",
    alignItems: "center",
    fontSize: 16, // Default
    ...styles.label.font,
    background: styles.label.fill,
    color: styles.label.color,
    }}
    >
    <motion.input
    name={input.name}
    type="radio"
    value={input.value || "on"}
    required={input.required}
    style={{
    marginRight: "0.5rem",
    margin: "0px 8px 0px 4px",
    }}
    />
    {input.label}
    @@ -671,6 +707,8 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    inputElement = textareaInput(input)
    } else if (input.type === FieldType.Checkbox) {
    inputElement = checkboxInput(input)
    } else if (input.type === FieldType.Radio) {
    inputElement = radioInput(input)
    } else {
    inputElement = baseInput(input)
    }
    @@ -919,6 +957,9 @@ addPropertyControls(BaseForm, {
    type: ControlType.Number,
    hidden: (props) => !hasMinMaxStep(props.type),
    },
    value: {
    type: ControlType.String,
    },
    required: { type: ControlType.Boolean },
    gridColumn: {
    title: "Grid Col",
  5. ShinyObjectLabs revised this gist Jun 15, 2023. 1 changed file with 9 additions and 12 deletions.
    21 changes: 9 additions & 12 deletions BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -53,13 +53,18 @@ enum FieldType {
    Text = "text",
    Number = "number",
    Email = "email",
    Url = "url",
    Tel = "tel",
    TextArea = "textarea",
    Select = "select",
    Checkbox = "checkbox",
    Time = "time",
    Week = "week",
    Month = "month",
    Date = "date",
    DateTimeLocal = "datetime-local",
    Password = "password",
    Hidden = "hidden",
    }

    interface Props extends Omit<HTMLMotionProps<"div">, "layout"> {
    @@ -449,7 +454,7 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    return shouldBeInline ? 1 : styles.form.columns
    }

    const basicInput = (input) => {
    const baseInput = (input) => {
    return (
    <div
    style={{
    @@ -660,22 +665,14 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(

    const inputsHTML: (ReactElement | null)[] = inputs.map((input) => {
    let inputElement: ReactElement | null = null
    if (
    input.type === FieldType.Text ||
    input.type === FieldType.Number ||
    input.type === FieldType.Email ||
    input.type === FieldType.Time ||
    input.type === FieldType.Week ||
    input.type === FieldType.Date ||
    input.type === FieldType.DateTimeLocal
    ) {
    inputElement = basicInput(input)
    } else if (input.type === FieldType.Select) {
    if (input.type === FieldType.Select) {
    inputElement = selectInput(input)
    } else if (input.type === FieldType.TextArea) {
    inputElement = textareaInput(input)
    } else if (input.type === FieldType.Checkbox) {
    inputElement = checkboxInput(input)
    } else {
    inputElement = baseInput(input)
    }

    return inputElement
  6. ShinyObjectLabs revised this gist Jun 15, 2023. 1 changed file with 55 additions and 26 deletions.
    81 changes: 55 additions & 26 deletions BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -49,6 +49,19 @@ type SelectOption = {
    value: string
    }

    enum FieldType {
    Text = "text",
    Number = "number",
    Email = "email",
    TextArea = "textarea",
    Select = "select",
    Checkbox = "checkbox",
    Time = "time",
    Week = "week",
    Date = "date",
    DateTimeLocal = "datetime-local",
    }

    interface Props extends Omit<HTMLMotionProps<"div">, "layout"> {
    url: string
    method: "get" | "post" | "put" | "patch" | "delete"
    @@ -74,6 +87,16 @@ function isExternalURL(url: string) {
    return false
    }

    function hasMinMaxStep(type: FieldType) {
    return [
    FieldType.Time,
    FieldType.Week,
    FieldType.Number,
    FieldType.Date,
    FieldType.DateTimeLocal,
    ].includes(type)
    }

    /**
    * Increment the number whenever shipping a new version to customers.
    * This will ensure that multiple versions of this component can exist
    @@ -469,6 +492,9 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    getFocus === input.name ? "focused" : "default"
    }
    transition={{ duration: 0.3 }}
    min={input.min}
    max={input.max}
    step={input.step}
    />
    </div>
    )
    @@ -635,16 +661,20 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    const inputsHTML: (ReactElement | null)[] = inputs.map((input) => {
    let inputElement: ReactElement | null = null
    if (
    input.type === "text" ||
    input.type === "number" ||
    input.type === "email"
    input.type === FieldType.Text ||
    input.type === FieldType.Number ||
    input.type === FieldType.Email ||
    input.type === FieldType.Time ||
    input.type === FieldType.Week ||
    input.type === FieldType.Date ||
    input.type === FieldType.DateTimeLocal
    ) {
    inputElement = basicInput(input)
    } else if (input.type === "select") {
    } else if (input.type === FieldType.Select) {
    inputElement = selectInput(input)
    } else if (input.type === "textarea") {
    } else if (input.type === FieldType.TextArea) {
    inputElement = textareaInput(input)
    } else if (input.type === "checkbox") {
    } else if (input.type === FieldType.Checkbox) {
    inputElement = checkboxInput(input)
    }

    @@ -858,21 +888,8 @@ addPropertyControls(BaseForm, {
    },
    type: {
    type: ControlType.Enum,
    options: [
    "text",
    "number",
    "email",
    "textarea",
    "select",
    "checkbox",
    ],
    optionTitles: [
    "Text",
    "Number",
    "Textarea",
    "Select",
    "Checkbox",
    ],
    options: Object.values(FieldType),
    optionTitles: Object.keys(FieldType),
    },
    options: {
    type: ControlType.Array,
    @@ -893,6 +910,18 @@ addPropertyControls(BaseForm, {
    },
    hidden: (props) => props.type !== "select",
    },
    min: {
    type: ControlType.String,
    hidden: (props) => !hasMinMaxStep(props.type),
    },
    max: {
    type: ControlType.String,
    hidden: (props) => !hasMinMaxStep(props.type),
    },
    step: {
    type: ControlType.Number,
    hidden: (props) => !hasMinMaxStep(props.type),
    },
    required: { type: ControlType.Boolean },
    gridColumn: {
    title: "Grid Col",
    @@ -1362,35 +1391,35 @@ BaseForm.defaultProps = {
    name: "name",
    label: "Name",
    placeholder: "Jane",
    type: "text",
    type: FieldType.Text,
    required: false,
    },
    {
    name: "email",
    label: "Email",
    placeholder: "[email protected]",
    type: "text",
    type: FieldType.Email,
    required: false,
    },
    {
    name: "service",
    label: "Service",
    placeholder: "- select -",
    type: "select",
    type: FieldType.Select,
    required: false,
    options: [],
    },
    {
    name: "message",
    label: "Message",
    placeholder: "",
    type: "textarea",
    type: FieldType.TextArea,
    required: false,
    },
    {
    name: "terms",
    label: "I accept the terms & conditions",
    type: "checkbox",
    type: FieldType.Checkbox,
    required: false,
    },
    ],
  7. ShinyObjectLabs revised this gist Jun 15, 2023. 1 changed file with 5 additions and 1 deletion.
    6 changes: 5 additions & 1 deletion BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -212,6 +212,7 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    // Prevent submitting while submitting
    if (isLoading) return
    setLoading(true)
    setError(false)

    const headers = new Headers()

    @@ -289,8 +290,11 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    })
    .then(({ statusCode, data }) => {
    if (statusCode >= 200 && statusCode < 300) {
    // Handle success
    // Reset state
    setLoading(false)
    event.target.reset()

    // Handle success
    onSuccess()
    if (redirectAs === "overlay") onSubmit?.()
    } else {
  8. ShinyObjectLabs revised this gist Jun 12, 2023. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -491,6 +491,7 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    autoCapitalize={"off"}
    autoCorrect={"off"}
    spellCheck={"false"}
    required={input.required}
    style={{
    ...defaultStyle,
    padding: inputPaddingValue,
    @@ -614,8 +615,9 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    }}
    >
    <motion.input
    required={input.required}
    name={input.name}
    type="checkbox"
    required={input.required}
    style={{
    marginRight: "0.5rem",
    }}
  9. ShinyObjectLabs revised this gist Jun 9, 2023. 1 changed file with 19 additions and 3 deletions.
    22 changes: 19 additions & 3 deletions BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -267,9 +267,25 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    fetch(url, requestOptions)
    .then((response) => {
    const statusCode = response.status
    return response
    .json()
    .then((data) => ({ statusCode, data }))
    const contentType = response.headers.get("content-type")

    if (
    contentType &&
    contentType.includes("application/json")
    ) {
    return response
    .json()
    .then((data) => ({ statusCode, data }))
    } else if (
    contentType &&
    contentType.includes("text/plain")
    ) {
    return response
    .text()
    .then((data) => ({ statusCode, data }))
    } else {
    throw new Error("Unsupported response type")
    }
    })
    .then(({ statusCode, data }) => {
    if (statusCode >= 200 && statusCode < 300) {
  10. ShinyObjectLabs revised this gist Jun 9, 2023. 1 changed file with 5 additions and 1 deletion.
    6 changes: 5 additions & 1 deletion BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -238,6 +238,11 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    url += queryString ? `?${queryString}` : ""
    } else {
    headers.append("Content-Type", contentType)

    if (contentType === "application/json") {
    headers.append("accept", "application/json")
    }

    const formData = new FormData(event.target)

    if (contentType === "application/x-www-form-urlencoded") {
    @@ -511,7 +516,6 @@ const BaseForm: ComponentType<Props> = withCSS<Props>(
    })
    }

    console.log(input)
    options = options.concat(input.options)

    return (
  11. ShinyObjectLabs created this gist Jun 9, 2023.
    1,373 changes: 1,373 additions & 0 deletions BaseForm.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1373 @@
    /*
    MIT License
    Copyright © Joel Whitaker
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    The Software is provided "as is", without warranty of any kind, express or
    implied, including but not limited to the warranties of merchantability,
    fitness for a particular purpose and noninfringement. In no event shall the
    authors or copyright holders be liable for any claim, damages or other
    liability, whether in an action of contract, tort or otherwise, arising from,
    out of or in connection with the Software or the use or other dealings in the
    Software.
    */

    import {
    ComponentType,
    CSSProperties,
    ReactElement,
    useCallback,
    useState,
    } from "react"
    import {
    addPropertyControls,
    ControlType,
    withCSS,
    useRouter,
    inferInitialRouteFromPath,
    } from "framer"
    import { HTMLMotionProps, motion, useAnimationControls } from "framer-motion"

    interface Styles {
    form: any
    label: any
    input: any
    button: any
    }

    type SelectOption = {
    text: any
    value: string
    }

    interface Props extends Omit<HTMLMotionProps<"div">, "layout"> {
    url: string
    method: "get" | "post" | "put" | "patch" | "delete"
    contentType: "application/json" | "application/x-www-form-urlencoded"
    redirectAs: string
    link: string
    inputs: any
    button: any
    style: CSSProperties
    styles: Styles
    extraHeaders?: Record<string, string>
    onSubmit?: () => void
    }

    function isExternalURL(url: string) {
    try {
    return !!new URL(url)
    } catch {}
    try {
    return !!new URL(`https://${url}`)
    } catch {}

    return false
    }

    /**
    * Increment the number whenever shipping a new version to customers.
    * This will ensure that multiple versions of this component can exist
    * in the same project without css rules overlapping. Only use valid css class characters.
    */
    const VERSION = "v1"

    /**
    * BASEFORM
    * By Joel Whitaker (Alphi.dev)
    * Based on INPUT by Benjamin den Boer
    *
    * @framerDisableUnlink
    *
    * @framerIntrinsicWidth 300
    * @framerIntrinsicHeight 40
    *
    * @framerSupportedLayoutWidth fixed
    * @framerSupportedLayoutHeight any
    */
    const BaseForm: ComponentType<Props> = withCSS<Props>(
    function BaseForm({
    url,
    method,
    contentType,
    redirectAs,
    link,
    inputs,
    button,
    styles,
    extraHeaders,
    style,
    onSubmit,
    }: Props) {
    const [isError, setError] = useState(false)
    const [isLoading, setLoading] = useState(false)
    const [getFocus, setFocus] = useState(null)

    const {
    paddingPerSide: labelPaddingPerSide,
    paddingTop: labelPaddingTop,
    paddingRight: labelPaddingRight,
    paddingBottom: labelPaddingBottom,
    paddingLeft: labelPaddingLeft,
    padding: labelPadding,
    borderRadius: labelBorderRadius,
    borderObject: labelBorderObject,
    shadowObject: labelShadowObject,
    } = styles.label

    const {
    paddingPerSide: inputPaddingPerSide,
    paddingTop: inputPaddingTop,
    paddingRight: inputPaddingRight,
    paddingBottom: inputPaddingBottom,
    paddingLeft: inputPaddingLeft,
    padding: inputPadding,
    borderRadius: inputBorderRadius,
    borderObject: inputBorderObject,
    focusObject: inputFocusObject,
    shadowObject: inputShadowObject,
    } = styles.input

    const {
    paddingPerSide: buttonPaddingPerSide,
    paddingTop: buttonPaddingTop,
    paddingRight: buttonPaddingRight,
    paddingBottom: buttonPaddingBottom,
    paddingLeft: buttonPaddingLeft,
    padding: buttonPadding,
    borderRadius: buttonBorderRadius,
    borderObject: buttonBorderObject,
    shadowObject: buttonShadowObject,
    } = styles.button

    const labelPaddingValue = labelPaddingPerSide
    ? `${labelPaddingTop}px ${labelPaddingRight}px ${labelPaddingBottom}px ${labelPaddingLeft}px`
    : `${labelPadding}px ${labelPadding}px ${labelPadding}px ${labelPadding}px`

    const inputPaddingValue = inputPaddingPerSide
    ? `${inputPaddingTop}px ${inputPaddingRight}px ${inputPaddingBottom}px ${inputPaddingLeft}px`
    : `${inputPadding}px ${inputPadding}px ${inputPadding}px ${inputPadding}px`

    const buttonPaddingValue = buttonPaddingPerSide
    ? `${buttonPaddingTop}px ${buttonPaddingRight}px ${buttonPaddingBottom}px ${buttonPaddingLeft}px`
    : `${buttonPadding}px ${buttonPadding}px ${buttonPadding}px ${buttonPadding}px`

    const router = useRouter()

    const onSuccess = () => {
    /* Reset */
    setLoading(false)
    setFocus(null)

    if (redirectAs === "link" && link && !isError) {
    const [path, hash] = link.split("#")

    const { routeId, pathVariables } = inferInitialRouteFromPath(
    router.routes,
    path
    )
    if (routeId) {
    router.navigate(
    routeId, // Route id of the path
    hash,
    pathVariables
    )
    }

    if (isExternalURL(link)) {
    setError(true)
    formControls.start("error")
    return false
    }
    }
    return
    }

    const handleChange = useCallback((event: any) => {
    setError(false)
    }, [])

    const handleFocus = useCallback((event: any, input: any) => {
    setFocus(input.name)
    }, [])

    const handleBlur = useCallback((event: any) => {
    setFocus(null)
    setError(false)
    }, [])

    const handleSubmit = useCallback(
    (event) => {
    event.preventDefault()

    // Prevent submitting while submitting
    if (isLoading) return
    setLoading(true)

    const headers = new Headers()

    if (extraHeaders) {
    for (const [key, value] of Object.entries(extraHeaders)) {
    headers.append(key, value)
    }
    }

    let requestOptions = {
    method: method,
    headers: headers,
    }

    if (method === "get") {
    // Add form fields to URL for GET requests
    const urlSearchParams = new URLSearchParams()

    for (const [name, value] of new FormData(event.target)) {
    urlSearchParams.append(name, value.toString())
    }

    const queryString = urlSearchParams.toString()
    url += queryString ? `?${queryString}` : ""
    } else {
    headers.append("Content-Type", contentType)
    const formData = new FormData(event.target)

    if (contentType === "application/x-www-form-urlencoded") {
    const urlSearchParams = new URLSearchParams()

    for (const [name, value] of formData) {
    urlSearchParams.append(name, value.toString())
    }

    requestOptions["body"] = urlSearchParams.toString()
    } else if (contentType === "application/json") {
    const bodyObject = {}

    for (const [name, value] of formData) {
    bodyObject[name] = value
    }

    requestOptions["body"] = JSON.stringify(bodyObject)
    }
    }

    fetch(url, requestOptions)
    .then((response) => {
    const statusCode = response.status
    return response
    .json()
    .then((data) => ({ statusCode, data }))
    })
    .then(({ statusCode, data }) => {
    if (statusCode >= 200 && statusCode < 300) {
    // Handle success
    setLoading(false)
    onSuccess()
    if (redirectAs === "overlay") onSubmit?.()
    } else {
    // Handle errors
    let errorMessage =
    "An error occurred submitting the form"
    throw new Error(errorMessage)
    }
    })
    .catch((error) => {
    console.error(error)
    setError(true)
    setLoading(false)
    formControls.start("error")
    })
    },
    [onSubmit, isLoading]
    )

    // Animation
    const formControls = useAnimationControls()

    // Label Box Shadow Styles
    const labelShadowStyles = styles.label.shadowObject
    ? `${labelShadowObject.shadowX}px ${labelShadowObject.shadowY}px ${labelShadowObject.shadowBlur}px ${labelShadowObject.shadowColor}`
    : null

    const labelBorderStyles = styles.label.borderObject
    ? `inset 0 0 0 ${labelBorderObject.borderWidth}px ${labelBorderObject.borderColor}`
    : null

    // Input Box Shadow Styles
    const inputFocusStylesFrom = styles.input.focusObject
    ? `inset 0 0 0 ${inputFocusObject.focusWidthFrom}px ${inputFocusObject.focusColor}`
    : null

    const inputFocusStylesTo = styles.input.focusObject
    ? `inset 0 0 0 ${inputFocusObject.focusWidthTo}px ${inputFocusObject.focusColor}`
    : null

    const inputShadowStyles = styles.input.shadowObject
    ? `${inputShadowObject.shadowX}px ${inputShadowObject.shadowY}px ${inputShadowObject.shadowBlur}px ${inputShadowObject.shadowColor}`
    : null

    const inputBorderStyles = styles.input.borderObject
    ? `inset 0 0 0 ${inputBorderObject.borderWidth}px ${inputBorderObject.borderColor}`
    : null

    // Button Box Shadow Styles
    const buttonShadowStyles = styles.button.shadowObject
    ? `${buttonShadowObject.shadowX}px ${buttonShadowObject.shadowY}px ${buttonShadowObject.shadowBlur}px ${buttonShadowObject.shadowColor}`
    : null

    const buttonBorderStyles = styles.button.borderObject
    ? `inset 0 0 0 ${buttonBorderObject.borderWidth}px ${buttonBorderObject.borderColor}`
    : null

    // Shake or wiggle as error
    const formVariants = {
    default: {
    x: 0,
    },
    error: {
    x: [0, -4, 4, 0],
    transition: {
    duration: 0.2,
    },
    },
    }

    const inputVariants = {
    default: {
    boxShadow: dynamicBoxShadow(
    inputFocusStylesFrom,
    inputShadowStyles,
    inputBorderStyles
    ),
    },
    focused: {
    boxShadow: dynamicBoxShadow(
    inputFocusStylesTo,
    inputShadowStyles,
    inputBorderStyles
    ),
    },
    }

    const label = (input) => {
    if (!input.label) {
    return null
    }
    return (
    <label
    htmlFor={input.name}
    style={{
    marginBottom: "0.375rem",
    alignSelf: "flex-start",
    padding: labelPaddingValue,
    borderRadius: labelBorderRadius,
    fontSize: 16, // Default
    ...styles.label.font,
    background: styles.label.fill,
    color: styles.label.color,
    boxShadow: dynamicBoxShadow(
    labelShadowStyles,
    labelBorderStyles
    ),
    }}
    >
    {input.label}
    {requiredFlag(input.required)}
    </label>
    )
    }

    const getInputSpan = (input) => {
    return input.gridColumn > styles.form.columns
    ? styles.form.columns
    : input.gridColumn
    }

    const getButtonSpan = () => {
    const totalSpan = inputs.reduce(
    (sum, input) => sum + Number(input.gridColumn),
    0
    )

    const shouldBeInline = totalSpan === styles.form.columns - 1
    return shouldBeInline ? 1 : styles.form.columns
    }

    const basicInput = (input) => {
    return (
    <div
    style={{
    display: "flex",
    flexDirection: "column",
    gridColumn: `span ${getInputSpan(input)}`,
    }}
    >
    {label(input)}
    <motion.input
    type={input.type}
    name={input.name}
    placeholder={input.placeholder}
    className={`${VERSION} framer-custom-input`}
    onChange={handleChange}
    onFocus={(event) => handleFocus(event, input)}
    onBlur={handleBlur}
    autoComplete={"off"}
    autoCapitalize={"off"}
    autoCorrect={"off"}
    spellCheck={"false"}
    required={input.required}
    style={{
    ...defaultStyle,
    padding: inputPaddingValue,
    borderRadius: inputBorderRadius,
    fontSize: 16, // Default
    ...styles.input.font,
    background: styles.input.fill,
    color: styles.input.color,
    boxShadow: dynamicBoxShadow(
    inputFocusStylesFrom,
    inputShadowStyles,
    inputBorderStyles
    ),
    }}
    variants={inputVariants}
    initial={false}
    animate={
    getFocus === input.name ? "focused" : "default"
    }
    transition={{ duration: 0.3 }}
    />
    </div>
    )
    }

    const textareaInput = (input) => {
    return (
    <div
    style={{
    display: "flex",
    flexDirection: "column",
    gridColumn: `span ${getInputSpan(input)}`,
    }}
    >
    {label(input)}
    <motion.textarea
    name={input.name}
    placeholder={input.placeholder}
    className={`${VERSION} framer-custom-input`}
    onChange={handleChange}
    onFocus={(event) => handleFocus(event, input)}
    onBlur={handleBlur}
    autoComplete={"off"}
    autoCapitalize={"off"}
    autoCorrect={"off"}
    spellCheck={"false"}
    style={{
    ...defaultStyle,
    padding: inputPaddingValue,
    borderRadius: inputBorderRadius,
    fontSize: 16, // Default
    ...styles.input.font,
    background: styles.input.fill,
    color: styles.input.color,
    boxShadow: dynamicBoxShadow(
    inputFocusStylesFrom,
    inputShadowStyles,
    inputBorderStyles
    ),
    }}
    variants={inputVariants}
    initial={false}
    animate={
    getFocus === input.name ? "focused" : "default"
    }
    transition={{ duration: 0.3 }}
    />
    </div>
    )
    }

    const optionsHMTL = (options) => {
    return options.map((option) => {
    return <option value={option.value}>{option.text}</option>
    })
    }

    const selectInput = (input) => {
    let options: SelectOption[] = []

    if (input.placeholder) {
    options.push({
    text: input.placeholder,
    value: "",
    })
    }

    console.log(input)
    options = options.concat(input.options)

    return (
    <div
    style={{
    display: "flex",
    flexDirection: "column",
    gridColumn: `span ${getInputSpan(input)}`,
    }}
    >
    {label(input)}
    <div
    style={{
    position: "relative",
    display: "inline-block",
    }}
    >
    <div
    style={{
    ...selectChevron,
    borderColor: `${styles.input.color} transparent transparent transparent`,
    }}
    ></div>
    <motion.select
    name={input.name}
    placeholder={input.placeholder}
    className={`${VERSION} framer-custom-input`}
    onChange={handleChange}
    onFocus={(event) => handleFocus(event, input)}
    onBlur={handleBlur}
    autoComplete={"off"}
    autoCapitalize={"off"}
    autoCorrect={"off"}
    spellCheck={"false"}
    required={input.required}
    style={{
    ...defaultStyle,
    padding: inputPaddingValue,
    borderRadius: inputBorderRadius,
    fontSize: 16, // Default
    ...styles.input.font,
    background: styles.input.fill,
    color: styles.input.color,
    boxShadow: dynamicBoxShadow(
    inputFocusStylesFrom,
    inputShadowStyles,
    inputBorderStyles
    ),
    }}
    variants={inputVariants}
    initial={false}
    animate={
    getFocus === input.name ? "focused" : "default"
    }
    transition={{ duration: 0.3 }}
    disabled={isLoading}
    >
    {optionsHMTL(options)}
    </motion.select>
    </div>
    </div>
    )
    }

    function checkboxInput(input) {
    return (
    <div
    style={{
    gridColumn: `span ${getInputSpan(input)}`,
    }}
    >
    <label
    style={{
    display: "flex",
    alignItems: "center",
    fontSize: 16, // Default
    ...styles.label.font,
    background: styles.label.fill,
    color: styles.label.color,
    }}
    >
    <motion.input
    required={input.required}
    type="checkbox"
    style={{
    marginRight: "0.5rem",
    }}
    />
    {input.label}
    </label>
    </div>
    )
    }

    const inputsHTML: (ReactElement | null)[] = inputs.map((input) => {
    let inputElement: ReactElement | null = null
    if (
    input.type === "text" ||
    input.type === "number" ||
    input.type === "email"
    ) {
    inputElement = basicInput(input)
    } else if (input.type === "select") {
    inputElement = selectInput(input)
    } else if (input.type === "textarea") {
    inputElement = textareaInput(input)
    } else if (input.type === "checkbox") {
    inputElement = checkboxInput(input)
    }

    return inputElement
    })

    return (
    <motion.div
    style={{
    ...style,
    ...containerStyles,
    "--framer-custom-placeholder-color":
    styles.input.placeholderColor,
    // "--framer-custom-background-color": input.fill,
    // "--framer-custom-text-color": input.color,
    }}
    variants={formVariants}
    animate={formControls}
    >
    <form
    style={{
    width: "100%",
    display: "grid",
    gridTemplateColumns:
    styles.form.columns > 1 && getButtonSpan() === 1
    ? "1fr auto"
    : `repeat(${styles.form.columns}, 1fr)`,
    gap: `${styles.form.rowGap}px ${styles.form.columnGap}px`,
    background: styles.form.fill,
    }}
    onSubmit={handleSubmit}
    method="POST"
    >
    {inputsHTML}

    <div
    style={{
    display: "flex",
    gridColumn: `span ${getButtonSpan()}`,
    }}
    >
    {!button.shouldAppear && isLoading && (
    <Spinner
    shouldAppear={button.shouldAppear}
    paddingPerSide={buttonPaddingPerSide}
    paddingTop={buttonPaddingTop}
    paddingRight={buttonPaddingRight}
    padding={buttonPadding}
    color={styles.input.color}
    />
    )}

    {button.shouldAppear && (
    <div
    style={{
    width: "100%",
    display: "flex",
    flexDirection: "column",
    }}
    >
    <div
    style={{
    height: "100%",
    display: "flex",
    position: "relative",
    alignSelf: styles.button.align,
    }}
    >
    <motion.input
    type="submit"
    value={button.label}
    style={{
    ...defaultStyle,
    width: "100%",
    height: "100%",
    cursor: "pointer",
    padding: buttonPaddingValue,
    borderRadius: buttonBorderRadius,
    fontWeight:
    styles.button.fontWeight,
    fontSize: 16, // Default
    ...styles.button.font,
    background: styles.button.fill,
    color: styles.button.color,
    zIndex: 1,
    boxShadow: dynamicBoxShadow(
    buttonShadowStyles,
    buttonBorderStyles
    ),
    }}
    />
    {isLoading && (
    <div
    style={{
    borderRadius:
    buttonBorderRadius,
    position: "absolute",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    width: "100%",
    height: "100%",
    inset: 0,
    zIndex: 2,
    color: styles.button.color,
    background: styles.button.fill,
    boxShadow: dynamicBoxShadow(
    buttonShadowStyles,
    buttonBorderStyles
    ),
    }}
    >
    <Spinner
    color={styles.button.color}
    />
    </div>
    )}
    </div>
    </div>
    )}
    </div>
    </form>
    </motion.div>
    )
    },
    [
    `.${VERSION}.framer-custom-input::placeholder { color: var(--framer-custom-placeholder-color) !important; }`,
    // `.${VERSION}.framer-custom-input:autofill { box-shadow: 0 0 0 1000px var(--framer-custom-background-color) inset; -webkit-text-fill-color: var(--framer-custom-text-color); }`,
    ]
    )

    const Spinner = (props) => {
    const noButtonStyles = !props.shouldAppear
    ? {
    position: "absolute",
    top: `calc(50% - 8px)`,
    right: props.inputPaddingPerSide
    ? props.inputPaddingRight
    : props.inputPadding,
    }
    : {}

    return (
    <motion.div
    style={{ height: 16, width: 16, ...noButtonStyles }}
    initial={{ rotate: 0 }}
    animate={{ rotate: 360 }}
    transition={{
    duration: 1,
    repeat: Infinity,
    }}
    >
    <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }}>
    <svg
    xmlns="http://www.w3.org/2000/svg"
    width="16"
    height="16"
    style={{ fill: "currentColor", color: props.color }}
    >
    <path
    d="M 8 0 C 3.582 0 0 3.582 0 8 C 0 12.419 3.582 16 8 16 C 12.418 16 16 12.419 16 8 C 15.999 3.582 12.418 0 8 0 Z M 8 14 C 4.687 14 2 11.314 2 8 C 2 4.687 4.687 2 8 2 C 11.314 2 14 4.687 14 8 C 14 11.314 11.314 14 8 14 Z"
    fill="currentColor"
    opacity="0.2"
    />
    <path
    d="M 8 0 C 12.418 0 15.999 3.582 16 8 C 16 8 16 9 15 9 C 14 9 14 8 14 8 C 14 4.687 11.314 2 8 2 C 4.687 2 2 4.687 2 8 C 2 8 2 9 1 9 C 0 9 0 8 0 8 C 0 3.582 3.582 0 8 0 Z"
    fill="currentColor"
    />
    </svg>
    </motion.div>
    </motion.div>
    )
    }

    addPropertyControls(BaseForm, {
    url: {
    title: "Url",
    type: ControlType.String,
    },
    method: {
    type: ControlType.Enum,
    defaultValue: "post",
    options: ["get", "post", "put", "patch", "delete"],
    optionTitles: ["Get", "Post", "Put", "Patch", "Delete"],
    },
    contentType: {
    type: ControlType.Enum,
    defaultValue: "application/json",
    options: ["application/json", "application/x-www-form-urlencoded"],
    optionTitles: ["json", "x-www-form-urlencoded"],
    hidden: (props) => props.method === "get",
    },
    inputs: {
    title: "Inputs",
    type: ControlType.Array,
    control: {
    type: ControlType.Object,
    controls: {
    label: {
    title: "Label",
    type: ControlType.String,
    },
    name: {
    title: "Name",
    type: ControlType.String,
    },
    placeholder: {
    title: "Placeholder",
    type: ControlType.String,
    hidden: (props) => props.type === "checkbox",
    },
    type: {
    type: ControlType.Enum,
    options: [
    "text",
    "number",
    "email",
    "textarea",
    "select",
    "checkbox",
    ],
    optionTitles: [
    "Text",
    "Number",
    "Textarea",
    "Select",
    "Checkbox",
    ],
    },
    options: {
    type: ControlType.Array,
    title: "Options",
    control: {
    type: ControlType.Object,
    title: "Option",
    controls: {
    text: {
    type: ControlType.String,
    title: "Text",
    },
    value: {
    type: ControlType.String,
    title: "Value",
    },
    },
    },
    hidden: (props) => props.type !== "select",
    },
    required: { type: ControlType.Boolean },
    gridColumn: {
    title: "Grid Col",
    type: ControlType.Enum,
    defaultValue: 1,
    displaySegmentedControl: true,
    segmentedControlDirection: "horizontal",
    options: ["1", "2", "3"],
    optionTitles: ["1", "2", "3"],
    },
    },
    },
    },
    button: {
    title: "Button",
    type: ControlType.Object,
    controls: {
    shouldAppear: {
    title: "Show",
    type: ControlType.Boolean,
    defaultValue: true,
    },
    label: {
    title: "Label",
    type: ControlType.String,
    defaultValue: "Submit",
    },
    },
    },
    redirectAs: {
    title: "Success",
    type: ControlType.Enum,
    options: ["link", "overlay"],
    optionTitles: ["Open Link", "Show Overlay"],
    defaultValue: "link",
    },
    link: {
    title: "Redirect",
    type: ControlType.Link,
    hidden: (props) => props.redirectAs === "overlay",
    },
    onSubmit: {
    title: "Submit",
    type: ControlType.EventHandler,
    hidden: (props) => props.redirectAs === "link",
    },
    styles: {
    type: ControlType.Object,
    controls: {
    form: {
    type: ControlType.Object,
    controls: {
    fill: {
    title: "Fill",
    type: ControlType.Color,
    defaultValue: "#fff",
    },
    columns: {
    title: "Columns",
    type: ControlType.Enum,
    options: ["1", "2", "3"],
    displaySegmentedControl: true,
    },
    rowGap: {
    title: "Row gap",
    type: ControlType.Number,
    displayStepper: true,
    min: 0,
    defaultValue: 8,
    },
    columnGap: {
    title: "Col Gap",
    type: ControlType.Number,
    displayStepper: true,
    min: 0,
    defaultValue: 8,
    },
    },
    },
    label: {
    type: ControlType.Object,
    controls: {
    font: {
    type: ControlType.Font,
    title: "Font",
    controls: "extended",
    },
    fill: {
    title: "Fill",
    type: ControlType.Color,
    defaultValue: "transparent",
    },
    color: {
    title: "Text",
    type: ControlType.Color,
    defaultValue: "#000",
    },
    padding: {
    title: "Padding",
    type: ControlType.FusedNumber,
    toggleKey: "paddingPerSide",
    toggleTitles: ["Padding", "Padding per side"],
    defaultValue: 0,
    valueKeys: [
    "paddingTop",
    "paddingRight",
    "paddingBottom",
    "paddingLeft",
    ],
    valueLabels: ["T", "R", "B", "L"],
    min: 0,
    },
    borderRadius: {
    title: "Radius",
    type: ControlType.Number,
    displayStepper: true,
    min: 0,
    defaultValue: 8,
    },
    borderObject: {
    type: ControlType.Object,
    title: "Border",
    optional: true,
    controls: {
    borderWidth: {
    title: "Width",
    type: ControlType.Number,
    displayStepper: true,
    defaultValue: 1,
    },
    borderColor: {
    title: "Color",
    type: ControlType.Color,
    defaultValue: "rgba(200,200,200,0.5)",
    },
    },
    },
    shadowObject: {
    type: ControlType.Object,
    title: "Shadow",
    optional: true,
    controls: {
    shadowColor: {
    title: "Color",
    type: ControlType.Color,
    defaultValue: "rgba(0,0,0,0.25)",
    },
    shadowX: {
    title: "Shadow X",
    type: ControlType.Number,
    min: -100,
    max: 100,
    defaultValue: 0,
    },
    shadowY: {
    title: "Shadow Y",
    type: ControlType.Number,
    min: -100,
    max: 100,
    defaultValue: 2,
    },
    shadowBlur: {
    title: "Shadow B",
    type: ControlType.Number,
    min: 0,
    max: 100,
    defaultValue: 4,
    },
    },
    },
    },
    },
    input: {
    type: ControlType.Object,
    controls: {
    font: {
    type: ControlType.Font,
    title: "Font",
    controls: "extended",
    },
    placeholderColor: {
    title: "Placeholder",
    type: ControlType.Color,
    defaultValue: "rgba(0, 0, 0, 0.3)",
    },
    fill: {
    title: "Fill",
    type: ControlType.Color,
    defaultValue: "#EBEBEB",
    },
    color: {
    title: "Text",
    type: ControlType.Color,
    defaultValue: "#000",
    },
    padding: {
    title: "Padding",
    type: ControlType.FusedNumber,
    toggleKey: "paddingPerSide",
    toggleTitles: ["Padding", "Padding per side"],
    defaultValue: 12,
    valueKeys: [
    "paddingTop",
    "paddingRight",
    "paddingBottom",
    "paddingLeft",
    ],
    valueLabels: ["T", "R", "B", "L"],
    min: 0,
    },
    borderRadius: {
    title: "Radius",
    type: ControlType.Number,
    displayStepper: true,
    min: 0,
    defaultValue: 8,
    },
    focusObject: {
    type: ControlType.Object,
    title: "Focus",
    optional: true,
    controls: {
    focusWidthFrom: {
    title: "From",
    type: ControlType.Number,
    displayStepper: true,
    defaultValue: 0,
    },
    focusWidthTo: {
    title: "To",
    type: ControlType.Number,
    displayStepper: true,
    defaultValue: 2,
    },
    focusColor: {
    title: "Color",
    type: ControlType.Color,
    defaultValue: "#09F",
    },
    },
    },
    borderObject: {
    type: ControlType.Object,
    title: "Border",
    optional: true,
    controls: {
    borderWidth: {
    title: "Width",
    type: ControlType.Number,
    displayStepper: true,
    defaultValue: 1,
    },
    borderColor: {
    title: "Color",
    type: ControlType.Color,
    defaultValue: "rgba(200,200,200,0.5)",
    },
    },
    },
    shadowObject: {
    type: ControlType.Object,
    title: "Shadow",
    optional: true,
    controls: {
    shadowColor: {
    title: "Color",
    type: ControlType.Color,
    defaultValue: "rgba(0,0,0,0.25)",
    },
    shadowX: {
    title: "Shadow X",
    type: ControlType.Number,
    min: -100,
    max: 100,
    defaultValue: 0,
    },
    shadowY: {
    title: "Shadow Y",
    type: ControlType.Number,
    min: -100,
    max: 100,
    defaultValue: 2,
    },
    shadowBlur: {
    title: "Shadow B",
    type: ControlType.Number,
    min: 0,
    max: 100,
    defaultValue: 4,
    },
    },
    },
    },
    },
    button: {
    type: ControlType.Object,
    controls: {
    font: {
    type: ControlType.Font,
    title: "Font",
    controls: "extended",
    },

    fill: {
    title: "Fill",
    type: ControlType.Color,
    defaultValue: "#333",
    },
    color: {
    title: "Text",
    type: ControlType.Color,
    defaultValue: "#FFF",
    },
    align: {
    title: "Align",
    type: ControlType.Enum,
    segmentedControlDirection: "vertical",
    options: [
    "flex-start",
    "center",
    "flex-end",
    "stretch",
    ],
    optionTitles: ["Start", "Center", "End", "Stretch"],
    defaultValue: "stretch",
    },
    padding: {
    title: "Padding",
    type: ControlType.FusedNumber,
    toggleKey: "paddingPerSide",
    toggleTitles: ["Padding", "Padding per side"],
    defaultValue: 15,
    valueKeys: [
    "paddingTop",
    "paddingRight",
    "paddingBottom",
    "paddingLeft",
    ],
    valueLabels: ["T", "R", "B", "L"],
    min: 0,
    },
    borderRadius: {
    title: "Radius",
    type: ControlType.Number,
    displayStepper: true,
    min: 0,
    defaultValue: 8,
    },
    borderObject: {
    type: ControlType.Object,
    title: "Border",
    optional: true,
    controls: {
    borderWidth: {
    title: "Width",
    type: ControlType.Number,
    displayStepper: true,
    defaultValue: 1,
    },
    borderColor: {
    title: "Color",
    type: ControlType.Color,
    defaultValue: "rgba(200,200,200,0.5)",
    },
    },
    },
    shadowObject: {
    type: ControlType.Object,
    title: "Shadow",
    optional: true,
    controls: {
    shadowColor: {
    title: "Color",
    type: ControlType.Color,
    defaultValue: "rgba(0,0,0,0.25)",
    },
    shadowX: {
    title: "Shadow X",
    type: ControlType.Number,
    min: -100,
    max: 100,
    defaultValue: 0,
    },
    shadowY: {
    title: "Shadow Y",
    type: ControlType.Number,
    min: -100,
    max: 100,
    defaultValue: 2,
    },
    shadowBlur: {
    title: "Shadow B",
    type: ControlType.Number,
    min: 0,
    max: 100,
    defaultValue: 4,
    },
    },
    },
    },
    },
    },
    },
    })

    const defaultStyle: React.CSSProperties = {
    WebkitAppearance: "none",
    width: "100%",
    height: "auto",
    outline: "none",
    border: "none",
    }

    const containerStyles: React.CSSProperties = {
    position: "relative",
    width: "100%",
    height: "100%",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    }

    const selectChevron: React.CSSProperties = {
    position: "absolute",
    top: "50%",
    right: "12px",
    transform: "translateY(-50%)",
    width: "0",
    height: "0",
    borderStyle: "solid",
    borderWidth: "5px 5px 0 5px",
    pointerEvents: "none",
    }

    function dynamicBoxShadow(...shadows: Array<string | null>) {
    const output: string[] = []
    shadows.forEach((shadow) => shadow && output.push(shadow))
    return output.join(", ")
    }
    function requiredFlag(isRequired) {
    if (isRequired) {
    return <span>*</span>
    }
    return null
    }

    BaseForm.defaultProps = {
    url: "",
    styles: {
    form: {
    columns: 1,
    rowGap: 8,
    columnGap: 8,
    },
    label: {
    color: "#000",
    },
    input: {
    borderObject: {
    borderColor: "#ccc",
    },
    },
    button: {},
    },
    inputs: [
    {
    name: "name",
    label: "Name",
    placeholder: "Jane",
    type: "text",
    required: false,
    },
    {
    name: "email",
    label: "Email",
    placeholder: "[email protected]",
    type: "text",
    required: false,
    },
    {
    name: "service",
    label: "Service",
    placeholder: "- select -",
    type: "select",
    required: false,
    options: [],
    },
    {
    name: "message",
    label: "Message",
    placeholder: "",
    type: "textarea",
    required: false,
    },
    {
    name: "terms",
    label: "I accept the terms & conditions",
    type: "checkbox",
    required: false,
    },
    ],
    }

    export default BaseForm