Skip to content

Commit 327cb54

Browse files
authored
Merge pull request #122 from umbraco/feature/uui-form
2 parents d8ad4ca + 412b2fa commit 327cb54

22 files changed

+527
-103
lines changed

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uui-base/lib/mixins/FormControlMixin.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export declare abstract class FormControlMixinInterface {
1414
protected _value: FormDataEntryValue;
1515
protected _internals: any;
1616
protected abstract getFormElement(): HTMLElement | undefined;
17+
pristine: boolean;
18+
required: boolean;
19+
requiredMessage: string;
20+
error: boolean;
21+
errorMessage: string;
1722
}
1823

1924
type FlagTypes =
@@ -85,6 +90,15 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
8590
// Validation
8691
private _validityState: any = {};
8792

93+
/**
94+
* Determines wether the form control has been touched or interacted with, this determines wether the validation-status of this form control should be made visible.
95+
* @type {boolean}
96+
* @attr
97+
* @default false
98+
*/
99+
@property({ type: Boolean, reflect: true })
100+
pristine: boolean = true;
101+
88102
/**
89103
* Apply validation rule for requiring a value of this form control.
90104
* @type {boolean}
@@ -140,6 +154,10 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
140154
() => this.errorMessage,
141155
() => this.error
142156
);
157+
158+
this.addEventListener('blur', () => {
159+
this.pristine = false;
160+
});
143161
}
144162

145163
public hasValue(): boolean {
@@ -148,27 +166,13 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
148166

149167
protected abstract getFormElement(): HTMLElement | undefined;
150168

151-
connectedCallback(): void {
152-
super.connectedCallback();
153-
this._removeFormListeners();
154-
// TODO: try using formAssociatedCallback
155-
this._form = this._internals.form;
156-
if (this._form) {
157-
if (this._form.hasAttribute('hide-validation')) {
158-
this.setAttribute('hide-validation', '');
159-
}
160-
this._form.addEventListener('submit', this._onFormSubmit);
161-
this._form.addEventListener('reset', this._onFormReset);
162-
}
163-
}
164169
disconnectedCallback(): void {
165170
super.disconnectedCallback();
166171
this._removeFormListeners();
167172
}
168173
private _removeFormListeners() {
169174
if (this._form) {
170175
this._form.removeEventListener('submit', this._onFormSubmit);
171-
this._form.removeEventListener('reset', this._onFormReset);
172176
}
173177
}
174178

@@ -211,17 +215,22 @@ export const FormControlMixin = <T extends Constructor<LitElement>>(
211215
}
212216

213217
private _onFormSubmit = () => {
214-
if (this._form && this._form.checkValidity() === false) {
215-
this.removeAttribute('hide-validation');
216-
} else {
217-
this.setAttribute('hide-validation', '');
218-
}
219-
};
220-
private _onFormReset = () => {
221-
this.setAttribute('hide-validation', '');
218+
this.pristine = false;
222219
};
223220

221+
public formAssociatedCallback() {
222+
this._removeFormListeners();
223+
this._form = this._internals.form;
224+
if (this._form) {
225+
// This relies on the form begin a 'uui-form':
226+
if (this._form.hasAttribute('invalid-submit')) {
227+
this.pristine = false;
228+
}
229+
this._form.addEventListener('submit', this._onFormSubmit);
230+
}
231+
}
224232
public formResetCallback() {
233+
this.pristine = true;
225234
this.value = this.getAttribute('value') || '';
226235
}
227236

packages/uui-boolean-input/lib/uui-boolean-input.element.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export abstract class UUIBooleanInputElement extends FormControlMixin(
178178
}
179179

180180
private _onInputChange() {
181+
this.pristine = false;
181182
this.checked = this._input.checked;
182183
this.dispatchEvent(new UUIBooleanInputEvent(UUIBooleanInputEvent.CHANGE));
183184
}

packages/uui-checkbox/lib/uui-checkbox.element.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,15 @@ export class UUICheckboxElement extends UUIBooleanInputElement {
134134
transform: scale(0.9);
135135
}
136136
137-
:host(:not([hide-validation]):invalid) #ticker,
138-
:host(:not([hide-validation]):invalid) label:hover #ticker,
139-
:host(:not([hide-validation]):invalid) label:hover input:checked:not([disabled]) + #ticker,
140-
:host(:not([hide-validation]):invalid) label:focus input:checked + #ticker,
137+
:host(:not([pristine]):invalid) #ticker,
138+
:host(:not([pristine]):invalid) label:hover #ticker,
139+
:host(:not([pristine]):invalid) label:hover input:checked:not([disabled]) + #ticker,
140+
:host(:not([pristine]):invalid) label:focus input:checked + #ticker,
141141
/* polyfill support */
142-
:host(:not([hide-validation])[internals-invalid]) #ticker,
143-
:host(:not([hide-validation])[internals-invalid]) label:hover #ticker,
144-
:host(:not([hide-validation])[internals-invalid]) label:hover input:checked:not([disabled]) + #ticker,
145-
:host(:not([hide-validation])[internals-invalid]) label:focus input:checked + #ticker {
142+
:host(:not([pristine])[internals-invalid]) #ticker,
143+
:host(:not([pristine])[internals-invalid]) label:hover #ticker,
144+
:host(:not([pristine])[internals-invalid]) label:hover input:checked:not([disabled]) + #ticker,
145+
:host(:not([pristine])[internals-invalid]) label:focus input:checked + #ticker {
146146
border: 1px solid var(--uui-look-danger-border);
147147
}
148148

packages/uui-form/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# uui-form
2+
3+
![npm](https://img.shields.io/npm/v/@umbraco-ui/uui-form?logoColor=%231B264F)
4+
5+
Umbraco style form component.
6+
7+
## Installation
8+
9+
### ES imports
10+
11+
```zsh
12+
npm i @umbraco-ui/uui-form
13+
```
14+
15+
Import the registration of `<uui-form>` via:
16+
17+
```javascript
18+
import '@umbraco-ui/uui-form/lib';
19+
```
20+
21+
When looking to leverage the `UUIFormElement` base class as a type and/or for extension purposes, do so via:
22+
23+
```javascript
24+
import { UUIFormElement } from '@umbraco-ui/uui-form/lib';
25+
```
26+
27+
### CDN
28+
29+
The component is available via CDN. This means it can be added to your application without the need of any bundler configuration. Here is how to use it with jsDelivr.
30+
31+
```html
32+
<!-- Latest Version -->
33+
<script src="https://cdn.jsdelivr.net/npm/@umbraco-ui/uui-form@latest/dist/uui-form.min.js"></script>
34+
35+
<!-- Specific version -->
36+
<script src="https://cdn.jsdelivr.net/npm/@umbraco-ui/[email protected]/dist/uui-form.min.js"></script>
37+
```
38+
39+
## Usage
40+
41+
```html
42+
<uui-form></uui-form>
43+
```

packages/uui-form/lib/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { UUIFormElement } from './uui-form.element';
2+
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
3+
4+
defineElement('uui-form', UUIFormElement, { extends: 'form' });
5+
6+
export * from './uui-form.element';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @element uui-form
3+
*/
4+
export class UUIFormElement extends HTMLFormElement {
5+
constructor() {
6+
super();
7+
this.setAttribute('novalidate', '');
8+
this.addEventListener('submit', this._onSubmit);
9+
this.addEventListener('reset', this._onReset);
10+
}
11+
12+
private _onSubmit(event: Event) {
13+
event.preventDefault();
14+
15+
const isValid = this.checkValidity();
16+
17+
if (!isValid) {
18+
this.setAttribute('submit-invalid', '');
19+
return;
20+
}
21+
this.removeAttribute('submit-invalid');
22+
23+
const formData = new FormData(this);
24+
25+
for (const value of formData.values()) {
26+
console.log(value);
27+
}
28+
}
29+
30+
private _onReset() {
31+
this.removeAttribute('submit-invalid');
32+
}
33+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Story } from '@storybook/web-components';
2+
import { html } from 'lit-html';
3+
import '@umbraco-ui/uui-form/lib/index';
4+
import '@umbraco-ui/uui-checkbox/lib';
5+
import '@umbraco-ui/uui-slider/lib';
6+
import '@umbraco-ui/uui-radio/lib';
7+
import '@umbraco-ui/uui-toggle/lib';
8+
import { UUIRadioGroupEvent } from '@umbraco-ui/uui-radio/lib/UUIRadioGroupEvent';
9+
10+
export default {
11+
id: 'uui-form',
12+
title: 'Inputs/Form',
13+
component: 'uui-form',
14+
};
15+
16+
const _onRadioGroupChanged = (e: UUIRadioGroupEvent) => {
17+
e.target.error = e.target.value !== 'radio2';
18+
};
19+
20+
export const Overview: Story = () => html` <form
21+
is="uui-form"
22+
style="max-width: 800px;">
23+
<div style="margin-bottom: 15px;">
24+
<uui-checkbox
25+
name="checkbox"
26+
value="Bike"
27+
label="This is my checked checkbox"
28+
checked
29+
required>
30+
This is my checked checkbox
31+
</uui-checkbox>
32+
</div>
33+
34+
<div style="margin-bottom: 15px;">
35+
<uui-toggle name="toggle" label="This is my toggle" required>
36+
This is my toggle
37+
</uui-toggle>
38+
</div>
39+
40+
<div style="margin-bottom: 15px;">
41+
<uui-radio-group
42+
name="radio"
43+
label="This is my radio"
44+
required
45+
@change=${_onRadioGroupChanged}>
46+
<uui-radio value="radio1" label="radio1" name="radio1">Label</uui-radio>
47+
<uui-radio value="radio2" label="radio2" name="radio2">Label</uui-radio>
48+
<uui-radio value="radio3" label="radio3" name="radio3">Label</uui-radio>
49+
</uui-radio-group>
50+
</div>
51+
52+
<div style="margin-bottom: 15px;">
53+
<uui-input name="email" type="text" label="Email" required> </uui-input>
54+
</div>
55+
56+
<div style="margin-bottom: 15px;">
57+
<uui-input
58+
type="password"
59+
name="password"
60+
value="MyPassword"
61+
label="Password"
62+
required>
63+
</uui-input>
64+
</div>
65+
66+
<div style="margin-bottom: 15px;">
67+
<uui-slider
68+
label="Slider"
69+
name="slider"
70+
value="5.5"
71+
min="0"
72+
max="10"
73+
step="1"
74+
required>
75+
</uui-slider>
76+
</div>
77+
78+
<div style="margin-bottom: 15px;">
79+
<input
80+
name="nativeCheckbox"
81+
label="Native input text"
82+
type="checkbox"
83+
value="NativeCheckboxValue"
84+
placeholder="native text input"
85+
checked
86+
required />
87+
</div>
88+
89+
<div style="margin-bottom: 15px;">
90+
<input
91+
name="nativeInput"
92+
label="Native input text"
93+
type="text"
94+
default-value="default test value"
95+
value="test value"
96+
placeholder="native text input"
97+
required />
98+
</div>
99+
100+
<div style="margin-bottom: 15px;">
101+
<input
102+
name="nativeInputNumber"
103+
label="Native input number"
104+
type="number"
105+
value=""
106+
placeholder="native number input"
107+
min="0"
108+
max="10"
109+
required />
110+
</div>
111+
<div>
112+
<uui-button type="submit" label="Submit" look="positive">
113+
Submit
114+
</uui-button>
115+
116+
<uui-button type="reset" label="Reset" look="secondary"> Reset </uui-button>
117+
</div>
118+
</form>`;

0 commit comments

Comments
 (0)