Last active
June 5, 2025 08:12
Revisions
-
ShinyObjectLabs revised this gist
Jul 2, 2023 . 1 changed file with 3 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -902,7 +902,7 @@ const Spinner = (props) => { ) } const basePropertyControls = { url: { title: "Url", type: ControlType.String, @@ -1483,4 +1483,5 @@ BaseForm.defaultProps = { ], } export default BaseForm export { basePropertyControls } -
ShinyObjectLabs revised this gist
Jul 2, 2023 . 1 changed file with 26 additions and 6 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 formData) { urlSearchParams.append(name, value.toString()) } @@ -273,15 +277,21 @@ const BaseForm: ComponentType<Props> = withCSS<Props>( headers.append("accept", "application/json") } 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) => { ) } 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 -
ShinyObjectLabs revised this gist
Jun 16, 2023 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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} defaultValue={input.value} placeholder={input.placeholder} className={`${VERSION} framer-custom-input`} onChange={handleChange} -
ShinyObjectLabs revised this gist
Jun 15, 2023 . 1 changed file with 43 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 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={{ 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", -
ShinyObjectLabs revised this gist
Jun 15, 2023 . 1 changed file with 9 additions and 12 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 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.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 -
ShinyObjectLabs revised this gist
Jun 15, 2023 . 1 changed file with 55 additions and 26 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 === 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) { inputElement = selectInput(input) } else if (input.type === FieldType.TextArea) { inputElement = textareaInput(input) } else if (input.type === FieldType.Checkbox) { inputElement = checkboxInput(input) } @@ -858,21 +888,8 @@ addPropertyControls(BaseForm, { }, type: { type: ControlType.Enum, 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: FieldType.Text, required: false, }, { name: "email", label: "Email", placeholder: "[email protected]", type: FieldType.Email, required: false, }, { name: "service", label: "Service", placeholder: "- select -", type: FieldType.Select, required: false, options: [], }, { name: "message", label: "Message", placeholder: "", type: FieldType.TextArea, required: false, }, { name: "terms", label: "I accept the terms & conditions", type: FieldType.Checkbox, required: false, }, ], -
ShinyObjectLabs revised this gist
Jun 15, 2023 . 1 changed file with 5 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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) { // Reset state setLoading(false) event.target.reset() // Handle success onSuccess() if (redirectAs === "overlay") onSubmit?.() } else { -
ShinyObjectLabs revised this gist
Jun 12, 2023 . 1 changed file with 3 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 name={input.name} type="checkbox" required={input.required} style={{ marginRight: "0.5rem", }} -
ShinyObjectLabs revised this gist
Jun 9, 2023 . 1 changed file with 19 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 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) { -
ShinyObjectLabs revised this gist
Jun 9, 2023 . 1 changed file with 5 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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>( }) } options = options.concat(input.options) return ( -
ShinyObjectLabs created this gist
Jun 9, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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