mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 12:09:26 +00:00
Experimental form
This commit is contained in:
@@ -2,51 +2,59 @@
|
||||
|
||||
[component-header:sl-form]
|
||||
|
||||
Forms...
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
|
||||
|
||||
TODO
|
||||
|
||||
- Since no native [type="submit"] exists, we need to listen for enter being pressed in native elements. Might as well just listen for it on sl elements too.
|
||||
|
||||
|
||||
Forms collect data that can easily be processed or sent to a server.
|
||||
|
||||
All of Shoelace's components make use of the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) to encapsulate markup, style, and behavior. One caveat of this approach is that native `<form>` elements don't recognize Shoelace form controls. This component solves that problem by serializing _both_ Shoelace form controls and native form controls.
|
||||
|
||||
```html preview
|
||||
<sl-form class="form-overview">
|
||||
<sl-input name="name" type="text" placeholder="Name"></sl-input>
|
||||
|
||||
<br>
|
||||
<sl-select name="options" placeholder="Select your favorite">
|
||||
<sl-menu-item value="birds">Birds</sl-menu-item>
|
||||
<sl-menu-item value="cats">Cats</sl-menu-item>
|
||||
<sl-menu-item value="dogs">Dogs</sl-menu-item>
|
||||
</sl-select>
|
||||
<br>
|
||||
<sl-checkbox name="awesome" value="yes!">
|
||||
I agree that Shoelace is awesome
|
||||
</sl-checkbox>
|
||||
<br><br>
|
||||
|
||||
<sl-textarea name="bio" placeholder="Bio"></sl-textarea>
|
||||
|
||||
<br><br>
|
||||
|
||||
<input type="text" name="native-text" placeholder="Native Textfield">
|
||||
|
||||
<br><br>
|
||||
|
||||
<input type="file" name="upload">
|
||||
|
||||
<br><br>
|
||||
|
||||
<sl-button type="primary" submit>Submit</sl-button>
|
||||
<sl-button submit>Submit</sl-button>
|
||||
</sl-form>
|
||||
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.form-overview');
|
||||
|
||||
|
||||
form.addEventListener('slSubmit', event => {
|
||||
form.getFormData().then(formData => {
|
||||
for (const entry of formData.entries()) {
|
||||
console.log(entry);
|
||||
}
|
||||
});
|
||||
});
|
||||
const formData = event.detail.formData;
|
||||
const formControls = event.detail.formControls;
|
||||
|
||||
// do something with the form data...
|
||||
for( const entry of formData.entries()) {
|
||||
console.log(entry);
|
||||
}
|
||||
|
||||
// ...or do something with the raw form controls
|
||||
console.log(formControls);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
?> Shoelace forms don't make use of `action` and `method` attributes, and they don't submit automatically like native forms. To handle submission, you need to listen for the `slSubmit` event as shown in the example's source above.
|
||||
|
||||
[component-metadata:sl-form]
|
||||
|
||||
## Examples
|
||||
|
||||
### GET
|
||||
|
||||
TODO
|
||||
|
||||
### POST
|
||||
|
||||
TODO
|
||||
|
||||
### Native Form Controls
|
||||
|
||||
TODO
|
||||
|
||||
14
src/components.d.ts
vendored
14
src/components.d.ts
vendored
@@ -303,7 +303,11 @@ export namespace Components {
|
||||
}
|
||||
interface SlForm {
|
||||
/**
|
||||
* Serializes form controls and returns all data as a FormData object.
|
||||
* Gets all form control elements (native and custom).
|
||||
*/
|
||||
"getFormControls": () => Promise<HTMLElement[]>;
|
||||
/**
|
||||
* Serializes all form controls elements and returns a `FormData` object.
|
||||
*/
|
||||
"getFormData": () => Promise<FormData>;
|
||||
/**
|
||||
@@ -568,6 +572,10 @@ export namespace Components {
|
||||
* Set to true to enable multiselect.
|
||||
*/
|
||||
"multiple": boolean;
|
||||
/**
|
||||
* The select's name.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* The select's placeholder text.
|
||||
*/
|
||||
@@ -1658,6 +1666,10 @@ declare namespace LocalJSX {
|
||||
* Set to true to enable multiselect.
|
||||
*/
|
||||
"multiple"?: boolean;
|
||||
/**
|
||||
* The select's name.
|
||||
*/
|
||||
"name"?: string;
|
||||
/**
|
||||
* Emitted when the control loses focus
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: var(--sl-input-font-family);
|
||||
font-size: var(--sl-input-font-size-medium);
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
.checkbox__control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--sl-toggle-size);
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
|
||||
.checkbox__icon {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
width: var(--sl-toggle-size);
|
||||
height: var(--sl-toggle-size);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
@import 'component';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form {
|
||||
outline: dashed 1px lightgray;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import { Component, Event, EventEmitter, Method, h } from '@stencil/core';
|
||||
|
||||
interface FormControlSerializer {
|
||||
elements: string[];
|
||||
serialize: (el: any) => any;
|
||||
interface FormControl {
|
||||
tag: string;
|
||||
serialize: (el: HTMLElement, formData: FormData) => void;
|
||||
click?: (event: MouseEvent) => any;
|
||||
keyDown?: (event: KeyboardEvent) => any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
* @status ready
|
||||
* @status experimental
|
||||
*/
|
||||
|
||||
//
|
||||
// TODO:
|
||||
//
|
||||
// - restructure serializer logic to make more sense
|
||||
// - serialize file inputs
|
||||
//
|
||||
|
||||
@Component({
|
||||
tag: 'sl-form',
|
||||
styleUrl: 'form.scss',
|
||||
@@ -26,79 +19,139 @@ interface FormControlSerializer {
|
||||
})
|
||||
export class Form {
|
||||
form: HTMLElement;
|
||||
serializers: FormControlSerializer[];
|
||||
formControls: FormControl[];
|
||||
|
||||
constructor() {
|
||||
this.serializers = [
|
||||
this.formControls = [
|
||||
{
|
||||
elements: ['sl-button', 'sl-input', 'sl-range', 'sl-select', 'sl-textarea'],
|
||||
serialize: el => (!el.name || el.disabled ? null : { name: el.name, value: el.value }),
|
||||
click: (event: MouseEvent) => {
|
||||
const target = event.target as any;
|
||||
const tag = target.tagName.toLowerCase();
|
||||
|
||||
if (tag === 'sl-button' && target.submit) {
|
||||
this.submit();
|
||||
}
|
||||
},
|
||||
keyDown: (event: KeyboardEvent) => {
|
||||
const target = event.target as any;
|
||||
const tag = target.tagName.toLowerCase();
|
||||
|
||||
if (event.key === 'Enter' && tag === 'sl-input') {
|
||||
this.submit();
|
||||
}
|
||||
|
||||
// Submit buttons should trigger a submit
|
||||
if (tag === 'sl-button' && target.submit && event.key === 'Enter') {
|
||||
tag: 'button',
|
||||
serialize: (el: HTMLButtonElement, formData) =>
|
||||
el.name && !el.disabled ? formData.append(el.name, el.value) : null,
|
||||
click: event => {
|
||||
const target = event.target as HTMLButtonElement;
|
||||
if (target.type === 'submit') {
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
elements: ['sl-checkbox', 'sl-radio', 'sl-switch'],
|
||||
serialize: el => (!el.name || !el.checked || el.disabled ? null : { name: el.name, value: el.value })
|
||||
},
|
||||
{
|
||||
elements: ['button', 'input', 'select', 'textarea'],
|
||||
serialize: el => {
|
||||
tag: 'input',
|
||||
serialize: (el: HTMLInputElement, formData) => {
|
||||
if (!el.name || el.disabled) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.type === 'file') {
|
||||
console.log(el.files);
|
||||
return el.multiple ? el.files : el.files[0];
|
||||
[...el.files].map(file => formData.append(el.name, file));
|
||||
return;
|
||||
}
|
||||
|
||||
return { name: el.name, value: el.value };
|
||||
formData.append(el.name, el.value);
|
||||
},
|
||||
click: (event: MouseEvent) => {
|
||||
const target = event.target as any;
|
||||
const tag = target.tagName.toLowerCase();
|
||||
|
||||
if (tag === 'button' && target.type === 'submit' && target.submit) {
|
||||
this.submit();
|
||||
}
|
||||
},
|
||||
keyDown: (event: KeyboardEvent) => {
|
||||
click: event => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const tag = target.tagName.toLowerCase();
|
||||
|
||||
// Pressing enter in an input should submit
|
||||
if (event.key === 'Enter' && tag === 'input') {
|
||||
if (target.type === 'submit') {
|
||||
this.submit();
|
||||
}
|
||||
|
||||
// Submit buttons should trigger a submit
|
||||
if (tag === 'button' && target.type === 'submit' && event.key === 'Enter') {
|
||||
},
|
||||
keyDown: event => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (event.key === 'Enter' && !['checkbox', 'file', 'radio'].includes(target.type)) {
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'select',
|
||||
serialize: (el: HTMLSelectElement, formData) => {
|
||||
if (el.name && !el.disabled) {
|
||||
if (el.multiple) {
|
||||
const selectedOptions = [...el.querySelectorAll('option:checked')];
|
||||
if (selectedOptions.length) {
|
||||
selectedOptions.map((option: HTMLOptionElement) => formData.append(el.name, option.value));
|
||||
} else {
|
||||
formData.append(el.name, '');
|
||||
}
|
||||
} else {
|
||||
formData.append(el.name, el.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-button',
|
||||
serialize: (el: HTMLSlButtonElement, formData) =>
|
||||
el.name && !el.disabled ? formData.append(el.name, el.value) : null,
|
||||
click: event => {
|
||||
const target = event.target as HTMLSlButtonElement;
|
||||
if (target.submit) {
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-checkbox',
|
||||
serialize: (el: HTMLSlCheckboxElement, formData) =>
|
||||
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
},
|
||||
{
|
||||
tag: 'sl-input',
|
||||
serialize: (el: HTMLSlInputElement, formData) =>
|
||||
el.name && !el.disabled ? formData.append(el.name, el.value) : null,
|
||||
keyDown: event => {
|
||||
if (event.key === 'Enter') {
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-radio',
|
||||
serialize: (el: HTMLSlRadioElement, formData) =>
|
||||
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
},
|
||||
{
|
||||
tag: 'sl-range',
|
||||
serialize: (el: HTMLSlRangeElement, formData) => {
|
||||
if (el.name && !el.disabled) {
|
||||
formData.append(el.name, el.value + '');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-select',
|
||||
serialize: (el: HTMLSlSelectElement, formData) => {
|
||||
if (el.name && !el.disabled) {
|
||||
if (el.multiple) {
|
||||
const selectedOptions = [...el.value];
|
||||
if (selectedOptions.length) {
|
||||
selectedOptions.map(value => formData.append(el.name, value));
|
||||
} else {
|
||||
formData.append(el.name, '');
|
||||
}
|
||||
} else {
|
||||
formData.append(el.name, el.value + '');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'sl-switch',
|
||||
serialize: (el: HTMLSlSwitchElement, formData) =>
|
||||
el.name && el.checked && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
},
|
||||
{
|
||||
tag: 'sl-textarea',
|
||||
serialize: (el: HTMLSlTextareaElement, formData) =>
|
||||
el.name && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
},
|
||||
{
|
||||
tag: 'textarea',
|
||||
serialize: (el: HTMLTextAreaElement, formData) =>
|
||||
el.name && !el.disabled ? formData.append(el.name, el.value) : null
|
||||
}
|
||||
];
|
||||
|
||||
@@ -109,41 +162,43 @@ export class Form {
|
||||
/** Emitted when the form is submitted. */
|
||||
@Event() slSubmit: EventEmitter;
|
||||
|
||||
/** Serializes form controls and returns all data as a FormData object. */
|
||||
/** Serializes all form controls elements and returns a `FormData` object. */
|
||||
@Method()
|
||||
async getFormData() {
|
||||
const formData = new FormData();
|
||||
const assignedElements = this.getAssignedElements();
|
||||
const formControls = await this.getFormControls();
|
||||
|
||||
assignedElements.map(el => {
|
||||
const data = this.serializeElement(el);
|
||||
if (data) {
|
||||
formData.append(data.name, data.value);
|
||||
}
|
||||
});
|
||||
formControls.map(el => this.serializeElement(el, formData));
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/** Gets all form control elements (native and custom). */
|
||||
@Method()
|
||||
async getFormControls() {
|
||||
const slot = this.form.querySelector('slot');
|
||||
const tags = this.formControls.map(control => control.tag);
|
||||
return slot
|
||||
.assignedElements({ flatten: true })
|
||||
.filter(el => tags.includes(el.tagName.toLowerCase())) as HTMLElement[];
|
||||
}
|
||||
|
||||
/** Submits the form. */
|
||||
@Method()
|
||||
async submit() {
|
||||
const formData = await this.getFormData();
|
||||
this.slSubmit.emit({ formData });
|
||||
}
|
||||
const formControls = await this.getFormControls();
|
||||
|
||||
getAssignedElements() {
|
||||
const slot = this.form.querySelector('slot');
|
||||
return slot.assignedElements({ flatten: true }) as HTMLElement[];
|
||||
this.slSubmit.emit({ formData, formControls });
|
||||
}
|
||||
|
||||
handleClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const tag = target.tagName.toLowerCase();
|
||||
|
||||
for (const serializer of this.serializers) {
|
||||
if (serializer.elements.includes(tag) && serializer.keyDown) {
|
||||
serializer.click(event);
|
||||
for (const formControl of this.formControls) {
|
||||
if (formControl.tag === tag && formControl.click) {
|
||||
formControl.click(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,19 +207,19 @@ export class Form {
|
||||
const target = event.target as HTMLElement;
|
||||
const tag = target.tagName.toLowerCase();
|
||||
|
||||
for (const serializer of this.serializers) {
|
||||
if (serializer.elements.includes(tag) && serializer.keyDown) {
|
||||
serializer.keyDown(event);
|
||||
for (const formControl of this.formControls) {
|
||||
if (formControl.tag === tag && formControl.keyDown) {
|
||||
formControl.keyDown(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serializeElement(el: HTMLElement) {
|
||||
serializeElement(el: HTMLElement, formData: FormData) {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
|
||||
for (const serializer of this.serializers) {
|
||||
if (serializer.elements.includes(tag)) {
|
||||
return serializer.serialize(el);
|
||||
for (const formControl of this.formControls) {
|
||||
if (formControl.tag === tag) {
|
||||
return formControl.serialize(el, formData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: var(--sl-input-font-family);
|
||||
font-size: var(--sl-input-font-size-medium);
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.radio__icon {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
width: var(--sl-toggle-size);
|
||||
height: var(--sl-toggle-size);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
.radio__control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--sl-toggle-size);
|
||||
|
||||
@@ -49,6 +49,9 @@ export class Select {
|
||||
/** Set to true to disable the select control. */
|
||||
@Prop() disabled = false;
|
||||
|
||||
/** The select's name. */
|
||||
@Prop() name = '';
|
||||
|
||||
/** The select's placeholder text. */
|
||||
@Prop() placeholder = '';
|
||||
|
||||
@@ -263,6 +266,7 @@ export class Select {
|
||||
slot="trigger"
|
||||
ref={el => (this.input = el)}
|
||||
class="select__input"
|
||||
name={this.name}
|
||||
value={this.displayLabel}
|
||||
disabled={this.disabled}
|
||||
placeholder={this.displayLabel === '' && this.displayTags.length === 0 ? this.placeholder : null}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
}
|
||||
|
||||
.switch {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: var(--sl-input-font-family);
|
||||
font-size: var(--sl-input-font-size-medium);
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
.switch__control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--width);
|
||||
|
||||
Reference in New Issue
Block a user