generated from garrettmills/template-npm-typescript
Start modal, page, form, form fields, logical components, session service, login page
This commit is contained in:
62
src/form/Form.component.ts
Normal file
62
src/form/Form.component.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {ExComponent} from '../ExComponent.js'
|
||||
import {Attribute, Component} from '../decorators.js'
|
||||
import {FormControl, FormField, FormData} from './types.js'
|
||||
|
||||
@Component('ex-form')
|
||||
export class FormComponent extends ExComponent implements FormControl {
|
||||
protected static html = `
|
||||
<style>
|
||||
slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`
|
||||
|
||||
protected fields: FormField[] = []
|
||||
|
||||
@Attribute()
|
||||
public name?: string
|
||||
|
||||
hasField(field: FormField): boolean {
|
||||
return this.fields.some(maybe => maybe.getFieldName() === field.getFieldName())
|
||||
}
|
||||
|
||||
registerField(field: FormField): void {
|
||||
this.fields.push(field)
|
||||
}
|
||||
|
||||
gather(): FormData {
|
||||
const data: FormData = {}
|
||||
|
||||
for ( const field of this.fields ) {
|
||||
data[field.getFieldName()] = field.getValue()
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
processChanges(): void {
|
||||
for ( const field of this.fields ) {
|
||||
field.validate()
|
||||
}
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
let valid = true
|
||||
|
||||
for ( const field of this.fields ) {
|
||||
valid = field.validate() && valid
|
||||
}
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
async submit(): Promise<void> {
|
||||
(3 + 4)
|
||||
}
|
||||
}
|
||||
63
src/form/SubmitButton.component.ts
Normal file
63
src/form/SubmitButton.component.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {ExComponent} from '../ExComponent.js'
|
||||
import {Attribute, Component, Element} from '../decorators.js'
|
||||
|
||||
@Component('ex-submit')
|
||||
export class SubmitButtonComponent extends ExComponent {
|
||||
protected static styles = `
|
||||
<style>
|
||||
button {
|
||||
display: inline-block;
|
||||
padding: 7px 12px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-text-on-accent);
|
||||
border: 1px solid var(--color-accent-darkened);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
border-radius: 7px;
|
||||
|
||||
transition: background-color 0.1s linear;
|
||||
width: unset;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: var(--color-accent-darkened);
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
`
|
||||
|
||||
protected static html = `
|
||||
<div class="container">
|
||||
<button type="submit"></button>
|
||||
</div>
|
||||
`
|
||||
|
||||
@Attribute()
|
||||
public label = 'Submit'
|
||||
|
||||
@Element('button[type=submit]')
|
||||
protected buttonEl!: HTMLButtonElement
|
||||
|
||||
mount() {
|
||||
super.mount()
|
||||
|
||||
this.buttonEl.addEventListener('click', () => {
|
||||
this.dispatchCustom('onSubmit', {})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render()
|
||||
|
||||
this.buttonEl.innerText = this.label
|
||||
}
|
||||
}
|
||||
28
src/form/fields/EmailInputField.component.ts
Normal file
28
src/form/fields/EmailInputField.component.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {TextInputFieldComponent} from './TextInputField.component.js'
|
||||
import {Component} from '../../decorators.js'
|
||||
|
||||
@Component('ex-input-email')
|
||||
export class EmailInputFieldComponent extends TextInputFieldComponent {
|
||||
public static validationRex = /^\s*[\w\-+_]+(\.[\w\-+_]+)*@[\w\-+_]+\.[\w\-+_]+(\.[\w\-+_]+)*\s*$/
|
||||
|
||||
protected static html = `
|
||||
<label>
|
||||
<span class="label"></span>
|
||||
<input class="input" type="email">
|
||||
<small class="error"></small>
|
||||
</label>
|
||||
`
|
||||
|
||||
validate(): boolean {
|
||||
if ( !super.validate() ) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ( !String(this.getValue()).match(EmailInputFieldComponent.validationRex) ) {
|
||||
this.error = 'Value must be a valid e-mail address'
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
28
src/form/fields/Input.component.ts
Normal file
28
src/form/fields/Input.component.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {ExComponent} from '../../ExComponent.js'
|
||||
import {FormControl, FormField} from '../types.js'
|
||||
import {FormComponent} from '../Form.component.js'
|
||||
|
||||
export abstract class InputComponent extends ExComponent implements FormField {
|
||||
mount() {
|
||||
super.mount()
|
||||
this.control()?.registerField(this)
|
||||
}
|
||||
|
||||
protected control(): undefined | FormControl {
|
||||
let parent = this.parentElement
|
||||
|
||||
while ( parent ) {
|
||||
if ( parent instanceof FormComponent ) {
|
||||
return parent
|
||||
}
|
||||
|
||||
parent = parent.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
abstract getFieldName(): string
|
||||
|
||||
abstract getValue(): any
|
||||
|
||||
abstract validate(): boolean
|
||||
}
|
||||
68
src/form/fields/NumberInputField.component.ts
Normal file
68
src/form/fields/NumberInputField.component.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {TextInputFieldComponent} from './TextInputField.component.js'
|
||||
import {Attribute, Component} from '../../decorators.js'
|
||||
|
||||
@Component('ex-input-number')
|
||||
export class NumberInputFieldComponent extends TextInputFieldComponent {
|
||||
protected static html = `
|
||||
<label>
|
||||
<span class="label"></span>
|
||||
<input class="input" type="number">
|
||||
<small class="error"></small>
|
||||
</label>
|
||||
`
|
||||
|
||||
@Attribute()
|
||||
public max?: number
|
||||
|
||||
@Attribute()
|
||||
public min?: number
|
||||
|
||||
@Attribute()
|
||||
public step?: number
|
||||
|
||||
render() {
|
||||
super.render()
|
||||
|
||||
const max = (!this.max && this.max !== 0) ? '' : String(this.max)
|
||||
this.inputEl.setAttribute('max', max)
|
||||
|
||||
const min = (!this.min && this.min !== 0) ? '' : String(this.min)
|
||||
this.inputEl.setAttribute('min', min)
|
||||
|
||||
const step = (!this.step && this.step !== 0) ? '' : String(this.step)
|
||||
this.inputEl.setAttribute('step', step)
|
||||
}
|
||||
|
||||
validate(): boolean {
|
||||
if ( !super.validate() ) {
|
||||
return false
|
||||
}
|
||||
|
||||
const num = this.getValue()
|
||||
if ( isNaN(num) ) {
|
||||
this.error = 'Value must be a number'
|
||||
return false
|
||||
}
|
||||
|
||||
if ( (this.max || this.max === 0) && num > this.max ) {
|
||||
this.error = `Value must be at most ${this.max}`
|
||||
return false
|
||||
}
|
||||
|
||||
if ( (this.min || this.min === 0) && num < this.min ) {
|
||||
this.error = `Value must be at least ${this.min}`
|
||||
return false
|
||||
}
|
||||
|
||||
if ( this.step && num % this.step !== 0 ) {
|
||||
this.error = `Value must be a multiple of ${this.step}`
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
getValue(): number {
|
||||
return parseFloat(super.getValue())
|
||||
}
|
||||
}
|
||||
62
src/form/fields/PasswordInputField.component.ts
Normal file
62
src/form/fields/PasswordInputField.component.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {TextInputFieldComponent} from './TextInputField.component.js'
|
||||
import {Component, Element} from '../../decorators.js'
|
||||
|
||||
@Component('ex-input-password')
|
||||
export class PasswordInputFieldComponent extends TextInputFieldComponent {
|
||||
protected static styles = `
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
`
|
||||
|
||||
protected static html = `
|
||||
<div class="container">
|
||||
<label>
|
||||
<span class="label"></span>
|
||||
<input class="input" type="password">
|
||||
<small class="error"></small>
|
||||
</label>
|
||||
<label>
|
||||
<input class="input confirm" type="password">
|
||||
</label>
|
||||
</div>
|
||||
`
|
||||
|
||||
@Element('input.confirm')
|
||||
protected confirmEl!: HTMLInputElement
|
||||
|
||||
render() {
|
||||
super.render()
|
||||
|
||||
const hasConfirm = this.hasAttribute('confirmed')
|
||||
this.confirmEl.hidden = !hasConfirm
|
||||
|
||||
if ( hasConfirm ) {
|
||||
const label = this.label || this.placeholder
|
||||
this.confirmEl.setAttribute('placeholder', (label ? label + ' ' : '') + 'Confirmation')
|
||||
this.confirmEl.setAttribute('name', (this.name || this.uuid) + '_confirm')
|
||||
}
|
||||
|
||||
if ( this.hasAttribute('required') && hasConfirm ) {
|
||||
this.confirmEl.setAttribute('required', '1')
|
||||
} else {
|
||||
this.confirmEl.removeAttribute('required')
|
||||
}
|
||||
}
|
||||
|
||||
validate(): boolean {
|
||||
if ( !super.validate() ) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ( this.hasAttribute('confirmed') && this.confirmEl.value !== this.getValue() ) {
|
||||
this.error = 'Confirmation does not match'
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
13
src/form/fields/PhoneInputField.component.ts
Normal file
13
src/form/fields/PhoneInputField.component.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {TextInputFieldComponent} from './TextInputField.component.js'
|
||||
import {Component} from '../../decorators.js'
|
||||
|
||||
@Component('ex-input-phone')
|
||||
export class PhoneInputFieldComponent extends TextInputFieldComponent {
|
||||
protected static html = `
|
||||
<label>
|
||||
<span class="label"></span>
|
||||
<input class="input" type="tel">
|
||||
<small class="error"></small>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
19
src/form/fields/SearchInputField.component.ts
Normal file
19
src/form/fields/SearchInputField.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {TextInputFieldComponent} from './TextInputField.component.js'
|
||||
import {Component} from '../../decorators.js'
|
||||
|
||||
@Component('ex-input-search')
|
||||
export class SearchInputFieldComponent extends TextInputFieldComponent {
|
||||
protected static html = `
|
||||
<label>
|
||||
<span class="label"></span>
|
||||
<input class="input" type="search">
|
||||
<small class="error"></small>
|
||||
</label>
|
||||
`
|
||||
|
||||
render() {
|
||||
super.render()
|
||||
|
||||
this.inputEl.setAttribute('placeholder', `🔎︎ ${this.inputEl.getAttribute('placeholder') || ''}`)
|
||||
}
|
||||
}
|
||||
105
src/form/fields/TextInputField.component.ts
Normal file
105
src/form/fields/TextInputField.component.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {Attribute, Component, Element} from '../../decorators.js'
|
||||
import {InputComponent} from './Input.component.js'
|
||||
|
||||
@Component('ex-input')
|
||||
export class TextInputFieldComponent extends InputComponent {
|
||||
protected static styles = `
|
||||
<style>
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
small.error {
|
||||
margin-top: 3px;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
</style>
|
||||
`
|
||||
|
||||
protected static html = `
|
||||
<label>
|
||||
<span class="label"></span>
|
||||
<input class="input" type="text">
|
||||
<small class="error"></small>
|
||||
</label>
|
||||
`
|
||||
|
||||
@Attribute()
|
||||
public name?: string
|
||||
|
||||
@Attribute()
|
||||
public placeholder?: string
|
||||
|
||||
@Attribute()
|
||||
public label?: string
|
||||
|
||||
@Attribute()
|
||||
public error?: string
|
||||
|
||||
@Attribute()
|
||||
public minlength?: string
|
||||
|
||||
@Attribute()
|
||||
public maxlength?: string
|
||||
|
||||
@Element('span.label')
|
||||
protected labelEl!: HTMLSpanElement
|
||||
|
||||
@Element('input.input')
|
||||
protected inputEl!: HTMLInputElement
|
||||
|
||||
@Element('small.error')
|
||||
protected errorEl!: HTMLElement
|
||||
|
||||
render() {
|
||||
super.render()
|
||||
|
||||
this.labelEl.innerText = this.label || ''
|
||||
this.inputEl.setAttribute('placeholder', this.placeholder || '')
|
||||
this.inputEl.setAttribute('name', this.name || this.uuid)
|
||||
|
||||
if ( this.hasAttribute('required') ) {
|
||||
this.inputEl.setAttribute('required', '1')
|
||||
} else {
|
||||
this.inputEl.removeAttribute('required')
|
||||
}
|
||||
|
||||
this.errorEl.hidden = !this.error
|
||||
this.errorEl.innerText = this.error || ''
|
||||
}
|
||||
|
||||
getFieldName(): string {
|
||||
return this.name || this.uuid
|
||||
}
|
||||
|
||||
getValue(): any {
|
||||
return this.inputEl.value
|
||||
}
|
||||
|
||||
validate(): boolean {
|
||||
const isValid = (
|
||||
!this.hasAttribute('required')
|
||||
|| this.getValue().trim()
|
||||
)
|
||||
|
||||
if ( !isValid ) {
|
||||
this.error = 'This field is required'
|
||||
} else {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
const min = parseInt(this.minlength || '', 10)
|
||||
if ( this.minlength && !isNaN(min) && String(this.getValue()).length < min ) {
|
||||
this.error = `Value must be at least ${min} characters`
|
||||
}
|
||||
|
||||
const max = parseInt(this.maxlength || '', 10)
|
||||
if ( this.maxlength && !isNaN(max) && String(this.getValue()).length > max ) {
|
||||
this.error = `Value must be at most ${max} characters`
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
}
|
||||
13
src/form/fields/URLInputField.component.ts
Normal file
13
src/form/fields/URLInputField.component.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {TextInputFieldComponent} from './TextInputField.component.js'
|
||||
import {Component} from '../../decorators.js'
|
||||
|
||||
@Component('ex-input-url')
|
||||
export class URLInputFieldComponent extends TextInputFieldComponent {
|
||||
protected static html = `
|
||||
<label>
|
||||
<span class="label"></span>
|
||||
<input class="input" type="url">
|
||||
<small class="error"></small>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
52
src/form/types.ts
Normal file
52
src/form/types.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export enum FormFieldType {
|
||||
text,
|
||||
password,
|
||||
url,
|
||||
email,
|
||||
phone,
|
||||
search,
|
||||
number,
|
||||
error,
|
||||
select,
|
||||
checkboxes,
|
||||
textarea,
|
||||
date,
|
||||
color,
|
||||
}
|
||||
|
||||
export interface FormFieldConfig {
|
||||
type: FormFieldType,
|
||||
name: string,
|
||||
title: string,
|
||||
required?: boolean,
|
||||
}
|
||||
|
||||
export interface FormConfig {
|
||||
endpoint: string,
|
||||
method?: 'get'|'post'|'put'|'delete'|'patch',
|
||||
fields: (FormFieldConfig)[],
|
||||
}
|
||||
|
||||
export type FormData = {[key: string]: any}
|
||||
|
||||
export interface FormField {
|
||||
getFieldName(): string
|
||||
|
||||
getValue(): any
|
||||
|
||||
validate(): boolean
|
||||
}
|
||||
|
||||
export interface FormControl {
|
||||
hasField(field: FormField): boolean
|
||||
|
||||
registerField(field: FormField): void
|
||||
|
||||
processChanges(): void
|
||||
|
||||
gather(): FormData | Promise<FormData>
|
||||
|
||||
submit(): void | Promise<void>
|
||||
|
||||
isValid(): boolean
|
||||
}
|
||||
Reference in New Issue
Block a user