Start modal, page, form, form fields, logical components, session service, login page

This commit is contained in:
2021-12-12 23:21:47 -06:00
parent a238136d94
commit bf581dc584
23 changed files with 772 additions and 7 deletions

View 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)
}
}

View 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
}
}

View 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
}
}

View 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
}

View 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())
}
}

View 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
}
}

View 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>
`
}

View 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') || ''}`)
}
}

View 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
}
}

View 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
View 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
}