Skip to content

Instantly share code, notes, and snippets.

@RedHatter
Created February 11, 2021 05:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RedHatter/19e8fb4b5c7606258d938353542adf18 to your computer and use it in GitHub Desktop.
Save RedHatter/19e8fb4b5c7606258d938353542adf18 to your computer and use it in GitHub Desktop.
Svelte form and input components with buildt-in validation
<script>
import { onMount, createEventDispatcher } from 'svelte'
import { getContext } from './Form.svelte'
const dispatch = createEventDispatcher()
export let value = ''
export let options = {}
let className
export { className as class }
export let equalWidth = false
export let required = false
let valid = true
function validate() {
return (valid = !(required && (value === '' || value === null)))
}
const context = getContext()
onMount(() => {
if (!context) return
context.register(validate)
return () => context.unregister(validate)
})
$: value, !valid && validate() && context.validate()
$: dispatch('change', value)
</script>
<style>
label {
margin: -1px;
transition: none;
&.active {
border-color: var(--dusty-orange);
background-color: var(--dusty-orange);
color: #ffffff;
}
:global(svg) {
width: 40px;
height: 40px;
}
}
</style>
<div class={className}>
<div class="flex overflow-hidden flex-wrap border rounded bg-white">
{#each Object.entries(options) as [optionValue, option]}
<label
class="flex-grow p-2 text-center cursor-pointer"
class:flex-1={equalWidth}
class:py-6={option.icon}
class:active={optionValue == value}>
<input
class="hidden"
type="radio"
bind:group={value}
value={optionValue} />
{#if typeof option == 'object'}
{#if option.icon}
<div class="icon-{option.icon} mx-auto" />
{/if}
{option.text}
{:else}{option}{/if}
</label>
{/each}
</div>
{#if !valid}
<span class="text-danger text-sm">This field is required</span>
{/if}
</div>
<script>
export let checked
export let name
let className
export { className as class }
</script>
<style>
input + div {
border: 1px solid rgb(206, 212, 218);
transition: all ease-out 0.2s;
&:after {
position: absolute 48% 38%;
width: 0;
height: 7px;
border-bottom: 2px solid var(--dusty-orange);
border-left: 2px solid var(--dusty-orange);
content: '';
opacity: 0;
transition: all ease-out 0.2s;
transform: rotate(-45deg);
transform-origin: bottom left;
}
}
input:checked + div:after {
width: 0.8rem;
opacity: 1;
}
</style>
<label class="{className} flex cursor-pointer">
<input class="hidden" type="checkbox" {name} on:change bind:checked />
<div
class="relative flex-grow-0 flex-shrink-0 mr-2 w-4 h-4 rounded bg-white" />
<div>
<slot />
</div>
</label>
<script context="module">
import { getContext as _getContext, setContext as _setContext } from 'svelte'
// Use object literal to avoid conflicts. A Symbol would be better but is
// unsupported by IE
const key = {}
export const getContext = _getContext.bind(undefined, key)
export const setContext = _setContext.bind(undefined, key)
</script>
<script>
import { createEventDispatcher } from 'svelte'
export let action
export let valid = true
let className
export { className as class }
const dispatch = createEventDispatcher()
const validatorList = []
const context = {
register: fn => validatorList.push(fn),
unregister: fn => {
let i = validatorList.indexOf(fn)
if (i != -1) validatorList.splice(i, 1)
},
validate: () => {
valid = validatorList.filter(fn => !fn()).length == 0
dispatch(valid ? 'valid' : 'invalid')
return valid
}
}
setContext(context)
function onSubmit(e) {
if (context.validate()) {
if (!action) e.preventDefault()
dispatch('submit')
} else {
e.preventDefault()
}
}
</script>
<form on:submit={onSubmit} class={className} {action}>
<slot />
</form>
<script>
import { onMount } from 'svelte'
import { getContext } from './Form.svelte'
export let disabled
export let id
let className
export { className as class }
export let value = ''
export let required = false
let valid = true
function validate() {
return (valid = !(required && (value === '' || value === null)))
}
const context = getContext()
onMount(() => {
if (!context) return
context.register(validate)
return () => context.unregister(validate)
})
$: value, !valid && validate() && context.validate()
</script>
<style>
select {
background: #ffffff
url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e")
no-repeat right 0.75rem center/8px 10px;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
&:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
}
</style>
<div class={className}>
<select
class="py-2 pr-6 pl-4 w-full border rounded cursor-pointer appearance-none"
{disabled}
{id}
bind:value
on:change>
<option value="">Select an option</option>
<slot />
</select>
{#if !valid}
<span class="text-red-600 text-sm">This field is required</span>
{/if}
</div>
<script>
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let value = false
export let onLabel = 'On'
export let offLabel = 'Off'
let className
export { className as class }
$: dispatch('change', value)
</script>
<style>
label {
cursor: pointer;
}
input {
display: none;
}
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
border: 1px solid var(--cool-gray);
border-radius: 20px;
background-color: white;
vertical-align: text-bottom;
marign: 0 5px;
}
.switch::after {
display: block;
margin: 1px;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: var(--cool-gray);
content: '';
}
input:checked ~ .switch {
border-color: var(--dodger-blue);
background-color: var(--dodger-blue);
}
input:checked ~ .switch::after {
background-color: #ffffff;
transform: translateX(20px);
}
.on-label,
input:checked ~ .off-label {
opacity: 0.3;
}
input:checked ~ .on-label,
.off-label {
opacity: 1;
}
.on-label,
.off-label,
.switch,
.switch::after {
transition: all 0.15s ease-in-out;
}
</style>
<label class={className}>
<input type="checkbox" bind:checked={value} />
<span class="off-label">{offLabel}</span>
<span class="switch" />
<span class="on-label">{onLabel}</span>
</label>
<script>
import { onMount } from 'svelte'
import { getContext } from './Form.svelte'
import { focusWithin } from '../actions.js'
export let value = ''
export let disabled
export let readonly
export let id
export let name
export let size
export let placeholder
let className
export { className as class }
export let unit
export let type = 'text'
export let required = false
export let errorMessage
export let min
export let max
export let pattern
export let oneOf
let _errorMessage = ''
function validate() {
_errorMessage =
required && (value === '' || value === null)
? 'This field is required'
: value === '' || value === null
? ''
: oneOf != undefined &&
((Array.isArray(oneOf) && !oneOf.includes(value)) || value != oneOf)
? 'Not a valid value'
: pattern instanceof RegExp && !pattern.test(value)
? 'Does not match the required format'
: min != undefined && value < min
? 'Must be larger than ' + min
: max != undefined && value > max
? 'Must be smaller than ' + max
: ''
return !_errorMessage
}
const context = getContext()
onMount(() => {
if (!context) return
context.register(validate)
return () => context.unregister(validate)
})
let attrs
$: {
// github:sveltejs/svelte#1434
attrs = { id, name, disabled, readonly }
if (size) attrs.size = size
if (placeholder) attrs.placeholder = placeholder
}
$: value, _errorMessage && validate() && context.validate()
</script>
<style>
input {
flex-grow: 1;
min-width: 0;
outline: none;
border: none;
background-color: transparent;
background-image: none;
}
input[type='number'] {
-moz-appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
}
}
.plain {
border-bottom: 2px dashed var(--border);
text-align: inherit;
font-family: monospace;
}
</style>
<div class="{className} relative">
{#if type == 'plain'}
<input class="plain pb-2" {...attrs} bind:value on:change />
{:else}
<div class="text-field" use:focusWithin>
{#if type == 'number'}
<input
class="p-0"
{...attrs}
{value}
type="number"
pattern="\d*"
on:change
on:input={e => {
value = e.target.value.replace(/\D/g, '')
e.target.value = value
}} />
{:else if type == 'password'}
<input class="p-0" type="password" {...attrs} bind:value on:change />
{:else}
<input class="p-0" {...attrs} bind:value on:change />
{/if}
{#if unit}
<span class="text-muted">{unit}</span>
{/if}
<slot />
</div>
{/if}
{#if _errorMessage}
<span class="text-red-600 text-sm">{errorMessage || _errorMessage}</span>
{/if}
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment