@brightspace-ui/core 3.117.0 → 3.118.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/form/README.md +3 -4
- package/components/form/docs/form-element-mixin.md +5 -7
- package/components/form/docs/form-element-nesting.md +1 -1
- package/components/form/docs/form.md +1 -74
- package/components/form/form.js +163 -2
- package/components/validation/README.md +2 -2
- package/custom-elements.json +4 -102
- package/package.json +1 -1
- package/components/form/demo/form-native-demo.js +0 -80
- package/components/form/demo/form-native.html +0 -29
- package/components/form/form-mixin.js +0 -202
- package/components/form/form-native.js +0 -148
@@ -2,16 +2,15 @@
|
|
2
2
|
|
3
3
|
There are several components and mixins available to help make working with forms easier.
|
4
4
|
|
5
|
-
## Validating and Submitting Forms Using `<d2l-form>`
|
5
|
+
## Validating and Submitting Forms Using `<d2l-form>`
|
6
6
|
|
7
|
-
|
7
|
+
Much like a native `<form>` element, `<d2l-form>` groups nested form elements together and controls how their data is validated and submitted. Unlike native `<form>`s, it supports our custom form elements.
|
8
8
|
|
9
9
|
- [d2l-form](docs/form.md#form-d2l-form): supports aggregation of nested forms for validation and submission
|
10
|
-
- [d2l-form-native](docs/form.md#native-form-d2l-form-native): emulates how a native `<form>` submits, but supports our custom form elements
|
11
10
|
|
12
11
|
## Custom Elements in Forms
|
13
12
|
|
14
|
-
To allow custom elements to participate in `<d2l-form>`
|
13
|
+
To allow custom elements to participate in `<d2l-form>` submission and validation, use the [FormElementMixin](docs/form-element-mixin.md).
|
15
14
|
|
16
15
|
If your custom element uses other nested *custom* form elements internally, read about [form element nesting](docs/form-element-nesting.md).
|
17
16
|
|
@@ -1,14 +1,14 @@
|
|
1
1
|
# FormElementMixin
|
2
2
|
|
3
|
-
The `FormElementMixin` allows
|
3
|
+
The `FormElementMixin` allows custom web components to participate as a form element.
|
4
4
|
|
5
5
|
This means that the component will be able to:
|
6
6
|
1. Perform live self-validation when being edited.
|
7
|
-
1. Participate in validation and submission when added to `d2l-form
|
7
|
+
1. Participate in validation and submission when added to `d2l-form`.
|
8
8
|
|
9
|
-
All custom form elements must provide a form value that will be submitted during `d2l-form`
|
10
|
-
|
11
|
-
invalid state.
|
9
|
+
All custom form elements must provide a form value that will be submitted during `d2l-form` submission.
|
10
|
+
|
11
|
+
Participating in validation is, however, optional. Some custom form elements may not need validation because they have no invalid state.
|
12
12
|
|
13
13
|
## Form Value
|
14
14
|
|
@@ -24,7 +24,6 @@ invalid state.
|
|
24
24
|
- When an `Object` is provided, all keys-value pairs will be submitted. To avoid collision it is recommended that you prefix each key with the component's `name` property value.
|
25
25
|
1. A single value: `'my-value'`
|
26
26
|
- When a single value is provided a key-value pair will be submitted with the component's `name` property value as the key. If `name` does not have a value, the value will not be submitted.
|
27
|
-
- **Note:** When using `d2l-form-native` all values will be converted to `String`s. Complex value may be used with `d2l-form` but should only be used if absolutely required to maintain compatibility with `d2l-form-native`.
|
28
27
|
|
29
28
|
## Validation
|
30
29
|
|
@@ -62,7 +61,6 @@ invalid state.
|
|
62
61
|
|
63
62
|
## Usage
|
64
63
|
|
65
|
-
|
66
64
|
```javascript
|
67
65
|
import { FormElementMixin } from '@brightspace-ui/core/form/form-element-mixin.js';
|
68
66
|
|
@@ -199,7 +199,7 @@ render() {
|
|
199
199
|
|
200
200
|
**8. Support Validate:**
|
201
201
|
|
202
|
-
The last step is to ensure that calling validate on the parent will result in the nested components being validated. This is not required for self-validation but is important to ensure your custom form element works properly when used inside a [`d2l-form`]
|
202
|
+
The last step is to ensure that calling validate on the parent will result in the nested components being validated. This is not required for self-validation but is important to ensure your custom form element works properly when used inside a [`d2l-form`].
|
203
203
|
|
204
204
|
```javascript
|
205
205
|
async validate() {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Form
|
1
|
+
# Form [d2l-form]
|
2
2
|
|
3
3
|
<!-- docs: demo -->
|
4
4
|
```html
|
@@ -34,12 +34,6 @@
|
|
34
34
|
</d2l-form>
|
35
35
|
```
|
36
36
|
|
37
|
-
There are two form components that can be used with our custom elements: `d2l-form` and `d2l-form-native`. These are useful in the following scenarios:
|
38
|
-
- `d2l-form`: when submitting form data via your own API calls OR when nesting multiple forms within each other
|
39
|
-
- `d2l-form-native`: when emulating native form element submission
|
40
|
-
|
41
|
-
## Form [d2l-form]
|
42
|
-
|
43
37
|
The `d2l-form` component can be used to build sections containing interactive controls that are validated and submitted as a group.
|
44
38
|
|
45
39
|
It differs from the native HTML `form` element in 4 ways:
|
@@ -48,8 +42,6 @@ It differs from the native HTML `form` element in 4 ways:
|
|
48
42
|
1. `d2l-form` elements can be nested. If a parent form is validated or submitted it will also trigger the corresponding action for descendent `d2l-form`s unless they explicitly opt-out using `no-nesting`. This means that a `d2l-form` will only pass validation if it and all of its nested descendants pass validation.
|
49
43
|
1. Submission is not handled directly by `d2l-form`. Instead, all form data will be aggregated and passed back to the caller via an event. The caller is then responsible for submitting the data.
|
50
44
|
|
51
|
-
If you're looking to emulate native form element submission, `d2l-form-native` may be more appropriate.
|
52
|
-
|
53
45
|
<!-- docs: demo code properties name:d2l-form sandboxTitle:'Form' autoSize:false display:block size:large -->
|
54
46
|
```html
|
55
47
|
<script type="module">
|
@@ -174,68 +166,3 @@ In the above example, calling `submit` on form `#a` will cause forms `#a` and `#
|
|
174
166
|
- `d2l-form#a` will be submitted because submit was called on it directly.
|
175
167
|
- `d2l-form#b` will be submitted because it is nested directly within `#a`'s slot.
|
176
168
|
- `d2l-form#root`, `d2l-form#c` and the `d2l-form` within the shadow root of `#d` will _**not**_ be submitted because they are ancestors of `#a` rather than descendants.
|
177
|
-
|
178
|
-
## Native Form [d2l-form-native]
|
179
|
-
|
180
|
-
The `d2l-form-native` component can be used to build sections containing interactive controls that are validated and submitted as a group.
|
181
|
-
|
182
|
-
It differs from the native HTML `form` element in 2 ways:
|
183
|
-
1. It supports custom form elements made using the [`FormElementMixin`](./form-element-mixin.md) in addition to native form elements like `input`, `select` and `textarea`.
|
184
|
-
1. Upon validation, it will display an error summary that contains error messages for any elements that failed validation.
|
185
|
-
|
186
|
-
If you're looking to submit form data via your own API calls or nest multiple forms within each other, `d2l-form` may be more appropriate.
|
187
|
-
|
188
|
-
<!-- docs: demo code properties name:d2l-form-native sandboxTitle:'Native Form' autoSize:false display:block size:medium -->
|
189
|
-
```html
|
190
|
-
<script type="module">
|
191
|
-
import '@brightspace-ui/core/components/form/form-native.js';
|
192
|
-
import '@brightspace-ui/core/components/inputs/input-text.js';
|
193
|
-
|
194
|
-
const button = document.querySelector('button');
|
195
|
-
const form = document.querySelector('d2l-form-native');
|
196
|
-
button.addEventListener('click', () => {
|
197
|
-
form.submit();
|
198
|
-
});
|
199
|
-
|
200
|
-
function handleSubmission(e) {
|
201
|
-
const { formData } = e.detail;
|
202
|
-
const email = formData.get('email');
|
203
|
-
const pets = formData.get('pets');
|
204
|
-
console.log('Form submission data: email = ' + email + ', pets = ' + pets);
|
205
|
-
}
|
206
|
-
form.addEventListener('formdata', (e) => handleSubmission(e));
|
207
|
-
</script>
|
208
|
-
<d2l-form-native>
|
209
|
-
<d2l-input-text required label="Email" name="email" type="email"></d2l-input-text>
|
210
|
-
<select class="d2l-input-select" name="pets" required>
|
211
|
-
<option value="">--Please choose an option--</option>
|
212
|
-
<option value="porpoise">Porpoise</option>
|
213
|
-
<option value="house hippo">House Hippo</option>
|
214
|
-
<option value="spiker monkey">Spider Monkey</option>
|
215
|
-
<option value="capybara">Capybara</option>
|
216
|
-
</select>
|
217
|
-
<button name="action" value="save" type="submit">Save</button>
|
218
|
-
</d2l-form-native>
|
219
|
-
```
|
220
|
-
|
221
|
-
<!-- docs: start hidden content -->
|
222
|
-
### Properties
|
223
|
-
|
224
|
-
| Property | Type | Description |
|
225
|
-
|---|---|---|
|
226
|
-
| `action` | String | The URL that processes the form submission. |
|
227
|
-
| `enctype` | default: `"application/x-www-form-urlencoded"`<br>`"multipart/form-data"`<br>`"text/plain"` | If the value of the method attribute is post, enctype is the MIME type of the form submission. |
|
228
|
-
| `method` | default: `"get"`<br>`"post"` | The URL that processes the form submission. |
|
229
|
-
| `target` | default: `"_self"`<br>`"_blank"`<br>`"_parent"`<br>`"_top"` | Indicates where to display the response after submitting the form. |
|
230
|
-
| `track-changes` | Boolean, default: `false` | Indicates that the form should interrupt and warn on navigation if the user has unsaved changes. |
|
231
|
-
|
232
|
-
### Events
|
233
|
-
- `submit`: Dispatched when the form is submitted. Cancelling this event will prevent form submission.
|
234
|
-
- `formdata`: Dispatched after the entry list representing the form's data is constructed. This happens when the form is submitted just prior to submission. The form data can be obtained from the `detail`'s `formData` property.
|
235
|
-
- `d2l-form-dirty`: Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not.
|
236
|
-
<!-- docs: end hidden content -->
|
237
|
-
|
238
|
-
### Methods
|
239
|
-
- `submit()`: Submits the form to the server. This will first perform validation on all elements within the form. Submission will only happen if validation succeeds.
|
240
|
-
- `requestSubmit(submitter)`: Requests that the form be submitted using the specified submit button and its corresponding configuration. A `button`'s value is only submitted if that button is both part of the form and the `submitter`.
|
241
|
-
- `async validate()`: Validates the form without submitting even if validation succeeds. This returns a `Map` mapping from an element to the list of error messages associated with it.
|
package/components/form/form.js
CHANGED
@@ -1,7 +1,13 @@
|
|
1
|
+
import './form-errory-summary.js';
|
2
|
+
import '../tooltip/tooltip.js';
|
3
|
+
import '../link/link.js';
|
1
4
|
import { css, html, LitElement } from 'lit';
|
2
5
|
import { findFormElements, flattenMap, getFormElementData, isCustomFormElement, isNativeFormElement } from './form-helper.js';
|
3
6
|
import { findComposedAncestor } from '../../helpers/dom.js';
|
4
|
-
import {
|
7
|
+
import { getComposedActiveElement } from '../../helpers/focus.js';
|
8
|
+
import { getUniqueId } from '../../helpers/uniqueId.js';
|
9
|
+
import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
|
10
|
+
import { localizeFormElement } from './form-element-localize-helper.js';
|
5
11
|
|
6
12
|
/**
|
7
13
|
* A component that can be used to build sections containing interactive controls that are validated and submitted as a group.
|
@@ -9,7 +15,7 @@ import { FormMixin } from './form-mixin.js';
|
|
9
15
|
* @slot - The native and custom form elements that participate in validation and submission
|
10
16
|
* @fires d2l-form-connect - Internal event
|
11
17
|
*/
|
12
|
-
class Form extends
|
18
|
+
class Form extends LocalizeCoreElement(LitElement) {
|
13
19
|
|
14
20
|
static get properties() {
|
15
21
|
return {
|
@@ -20,6 +26,13 @@ class Form extends FormMixin(LitElement) {
|
|
20
26
|
* @type {boolean}
|
21
27
|
*/
|
22
28
|
noNesting: { type: Boolean, attribute: 'no-nesting', reflect: true },
|
29
|
+
/**
|
30
|
+
* Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.
|
31
|
+
* @type {boolean}
|
32
|
+
*/
|
33
|
+
trackChanges: { type: Boolean, attribute: 'track-changes', reflect: true },
|
34
|
+
_errors: { type: Object },
|
35
|
+
_hasErrors: { type: Boolean, attribute: '_has-errors', reflect: true },
|
23
36
|
};
|
24
37
|
}
|
25
38
|
|
@@ -39,21 +52,37 @@ class Form extends FormMixin(LitElement) {
|
|
39
52
|
|
40
53
|
constructor() {
|
41
54
|
super();
|
55
|
+
this.trackChanges = false;
|
56
|
+
this._errors = new Map();
|
42
57
|
this._isSubForm = false;
|
43
58
|
this._nestedForms = new Map();
|
59
|
+
this._firstUpdateResolve = null;
|
60
|
+
this._firstUpdatePromise = new Promise((resolve) => {
|
61
|
+
this._firstUpdateResolve = resolve;
|
62
|
+
});
|
63
|
+
this._tooltips = new Map();
|
64
|
+
this._validationCustoms = new Set();
|
65
|
+
|
66
|
+
this._onUnload = this._onUnload.bind(this);
|
67
|
+
this._onNativeSubmit = this._onNativeSubmit.bind(this);
|
44
68
|
|
45
69
|
/** @ignore */
|
46
70
|
this.addEventListener('d2l-form-connect', this._onFormConnect);
|
71
|
+
this.addEventListener('d2l-form-errors-change', this._onErrorsChange);
|
72
|
+
this.addEventListener('d2l-form-element-errors-change', this._onErrorsChange);
|
73
|
+
this.addEventListener('d2l-validation-custom-connected', this._validationCustomConnected);
|
47
74
|
}
|
48
75
|
|
49
76
|
connectedCallback() {
|
50
77
|
super.connectedCallback();
|
78
|
+
window.addEventListener('beforeunload', this._onUnload);
|
51
79
|
/** @ignore */
|
52
80
|
this._isSubForm = !this.dispatchEvent(new CustomEvent('d2l-form-connect', { bubbles: true, composed: true, cancelable: true }));
|
53
81
|
}
|
54
82
|
|
55
83
|
disconnectedCallback() {
|
56
84
|
super.disconnectedCallback();
|
85
|
+
window.removeEventListener('beforeunload', this._onUnload);
|
57
86
|
/** @ignore */
|
58
87
|
this.dispatchEvent(new CustomEvent('d2l-form-disconnect'));
|
59
88
|
this._isSubForm = false;
|
@@ -62,6 +91,10 @@ class Form extends FormMixin(LitElement) {
|
|
62
91
|
firstUpdated(changedProperties) {
|
63
92
|
super.firstUpdated(changedProperties);
|
64
93
|
|
94
|
+
this.addEventListener('change', this._onFormElementChange);
|
95
|
+
this.addEventListener('input', this._onFormElementChange);
|
96
|
+
this.addEventListener('focusout', this._onFormElementChange);
|
97
|
+
this._firstUpdateResolve();
|
65
98
|
this._setupDialogValidationReset();
|
66
99
|
}
|
67
100
|
|
@@ -79,6 +112,13 @@ class Form extends FormMixin(LitElement) {
|
|
79
112
|
`;
|
80
113
|
}
|
81
114
|
|
115
|
+
willUpdate(changedProperties) {
|
116
|
+
super.willUpdate(changedProperties);
|
117
|
+
if (changedProperties.has('_errors')) {
|
118
|
+
this._hasErrors = this._errors.size > 0;
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
82
122
|
async requestSubmit(submitter) {
|
83
123
|
const errors = await this.validate();
|
84
124
|
if (errors.size > 0) {
|
@@ -145,6 +185,34 @@ class Form extends FormMixin(LitElement) {
|
|
145
185
|
return flattenedErrorMap;
|
146
186
|
}
|
147
187
|
|
188
|
+
_displayInvalid(ele, message) {
|
189
|
+
let tooltip = this._tooltips.get(ele);
|
190
|
+
if (!tooltip) {
|
191
|
+
tooltip = document.createElement('d2l-tooltip');
|
192
|
+
tooltip.for = ele.id;
|
193
|
+
tooltip.align = 'start';
|
194
|
+
tooltip.state = 'error';
|
195
|
+
ele.parentNode.append(tooltip);
|
196
|
+
this._tooltips.set(ele, tooltip);
|
197
|
+
|
198
|
+
tooltip.appendChild(document.createTextNode(message));
|
199
|
+
} else if (tooltip.innerText.trim() !== message.trim()) {
|
200
|
+
tooltip.textContent = '';
|
201
|
+
tooltip.appendChild(document.createTextNode(message));
|
202
|
+
tooltip.updatePosition();
|
203
|
+
}
|
204
|
+
ele.setAttribute('aria-invalid', 'true');
|
205
|
+
}
|
206
|
+
|
207
|
+
_displayValid(ele) {
|
208
|
+
const tooltip = this._tooltips.get(ele);
|
209
|
+
if (tooltip) {
|
210
|
+
this._tooltips.delete(ele);
|
211
|
+
tooltip.remove();
|
212
|
+
}
|
213
|
+
ele.setAttribute('aria-invalid', 'false');
|
214
|
+
}
|
215
|
+
|
148
216
|
_findFormElements() {
|
149
217
|
const isFormElementPredicate = ele => this._hasSubForms(ele);
|
150
218
|
const visitChildrenPredicate = ele => !this._hasSubForms(ele);
|
@@ -163,6 +231,14 @@ class Form extends FormMixin(LitElement) {
|
|
163
231
|
return !this._isSubForm || this.noNesting;
|
164
232
|
}
|
165
233
|
|
234
|
+
_onErrorsChange(e) {
|
235
|
+
if (e.target === this) {
|
236
|
+
return;
|
237
|
+
}
|
238
|
+
e.stopPropagation();
|
239
|
+
this._updateErrors(e.target, e.detail.errors);
|
240
|
+
}
|
241
|
+
|
166
242
|
_onFormConnect(e) {
|
167
243
|
if (e.target === this) {
|
168
244
|
return;
|
@@ -195,6 +271,37 @@ class Form extends FormMixin(LitElement) {
|
|
195
271
|
|
196
272
|
}
|
197
273
|
|
274
|
+
async _onFormElementChange(e) {
|
275
|
+
const ele = e.target;
|
276
|
+
|
277
|
+
if ((isNativeFormElement(ele) || isCustomFormElement(ele)) && e.type !== 'focusout') {
|
278
|
+
this._dirty = true;
|
279
|
+
/** Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not. */
|
280
|
+
this.dispatchEvent(new CustomEvent('d2l-form-dirty'));
|
281
|
+
}
|
282
|
+
|
283
|
+
if (!isNativeFormElement(ele)) {
|
284
|
+
return;
|
285
|
+
}
|
286
|
+
e.stopPropagation();
|
287
|
+
const errors = await this._validateFormElement(ele, e.type === 'focusout');
|
288
|
+
this._updateErrors(ele, errors);
|
289
|
+
}
|
290
|
+
|
291
|
+
_onNativeSubmit(e) {
|
292
|
+
e.preventDefault();
|
293
|
+
e.stopPropagation();
|
294
|
+
const submitter = e.submitter || getComposedActiveElement();
|
295
|
+
this.requestSubmit(submitter);
|
296
|
+
}
|
297
|
+
|
298
|
+
_onUnload(e) {
|
299
|
+
if (this.trackChanges && this._dirty) {
|
300
|
+
e.preventDefault();
|
301
|
+
e.returnValue = false;
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
198
305
|
_setupDialogValidationReset() {
|
199
306
|
const dialogAncestor = findComposedAncestor(
|
200
307
|
this,
|
@@ -229,5 +336,59 @@ class Form extends FormMixin(LitElement) {
|
|
229
336
|
this.dispatchEvent(new CustomEvent('d2l-form-submit', { detail: { formData } }));
|
230
337
|
}
|
231
338
|
|
339
|
+
_updateErrors(ele, errors) {
|
340
|
+
|
341
|
+
if (!this._errors.has(ele)) {
|
342
|
+
return false;
|
343
|
+
}
|
344
|
+
if (Array.from(errors).length === 0) {
|
345
|
+
this._errors.delete(ele);
|
346
|
+
} else {
|
347
|
+
this._errors.set(ele, errors);
|
348
|
+
}
|
349
|
+
const detail = { bubbles: true, composed: true, detail: { errors: this._errors } };
|
350
|
+
/** @ignore */
|
351
|
+
this.dispatchEvent(new CustomEvent('d2l-form-errors-change', detail));
|
352
|
+
this.requestUpdate('_errors');
|
353
|
+
return true;
|
354
|
+
}
|
355
|
+
|
356
|
+
async _validateFormElement(ele, showNewErrors) {
|
357
|
+
// if validation occurs before we've rendered,
|
358
|
+
// localization may not have loaded yet
|
359
|
+
await this._firstUpdatePromise;
|
360
|
+
ele.id = ele.id || getUniqueId();
|
361
|
+
if (isCustomFormElement(ele)) {
|
362
|
+
return ele.validate(showNewErrors);
|
363
|
+
} else if (isNativeFormElement(ele)) {
|
364
|
+
const customs = [...this._validationCustoms].filter(custom => custom.forElement === ele);
|
365
|
+
const results = await Promise.all(customs.map(custom => custom.validate()));
|
366
|
+
const errors = customs.map(custom => custom.failureText).filter((_, i) => !results[i]);
|
367
|
+
if (!ele.checkValidity()) {
|
368
|
+
const validationMessage = localizeFormElement(this.localize.bind(this), ele);
|
369
|
+
errors.unshift(validationMessage);
|
370
|
+
}
|
371
|
+
if (errors.length > 0 && (showNewErrors || ele.getAttribute('aria-invalid') === 'true')) {
|
372
|
+
this._displayInvalid(ele, errors[0]);
|
373
|
+
} else {
|
374
|
+
this._displayValid(ele);
|
375
|
+
}
|
376
|
+
return errors;
|
377
|
+
}
|
378
|
+
return [];
|
379
|
+
}
|
380
|
+
|
381
|
+
_validationCustomConnected(e) {
|
382
|
+
e.stopPropagation();
|
383
|
+
const custom = e.composedPath()[0];
|
384
|
+
this._validationCustoms.add(custom);
|
385
|
+
|
386
|
+
const onDisconnect = () => {
|
387
|
+
custom.removeEventListener('d2l-validation-custom-disconnected', onDisconnect);
|
388
|
+
this._validationCustoms.delete(custom);
|
389
|
+
};
|
390
|
+
custom.addEventListener('d2l-validation-custom-disconnected', onDisconnect);
|
391
|
+
}
|
392
|
+
|
232
393
|
}
|
233
394
|
customElements.define('d2l-form', Form);
|
@@ -4,10 +4,10 @@
|
|
4
4
|
The `d2l-validation-custom` component is used to add custom validation logic to native form elements like `input`, `select` and `textarea` or custom form elements created with the [`FormElementMixin`](../form/docs/form-element-mixin.md).
|
5
5
|
|
6
6
|
**Native Form Elements:**
|
7
|
-
- When attached to native form elements like `input`, `select` and `textarea`, both the `d2l-validation-custom` and native form element **must** be within a [`d2l-form`](../form/docs/form.md)
|
7
|
+
- When attached to native form elements like `input`, `select` and `textarea`, both the `d2l-validation-custom` and native form element **must** be within a [`d2l-form`](../form/docs/form.md) for the validation custom to function.
|
8
8
|
|
9
9
|
**Custom Form Elements:**
|
10
|
-
- When attached to custom form elements created with the [`FormElementMixin`](../form/docs/form-element-mixin.md), the `d2l-validation-custom` will function even if no [`d2l-form`](../form/docs/form.md)
|
10
|
+
- When attached to custom form elements created with the [`FormElementMixin`](../form/docs/form-element-mixin.md), the `d2l-validation-custom` will function even if no [`d2l-form`](../form/docs/form.md) is present.
|
11
11
|
|
12
12
|
**Usage:**
|
13
13
|
```html
|
package/custom-elements.json
CHANGED
@@ -4759,10 +4759,6 @@
|
|
4759
4759
|
"name": "d2l-form-dialog-demo",
|
4760
4760
|
"path": "./components/form/demo/form-dialog-demo.js"
|
4761
4761
|
},
|
4762
|
-
{
|
4763
|
-
"name": "d2l-form-native-demo",
|
4764
|
-
"path": "./components/form/demo/form-native-demo.js"
|
4765
|
-
},
|
4766
4762
|
{
|
4767
4763
|
"name": "d2l-form-panel-demo",
|
4768
4764
|
"path": "./components/form/demo/form-panel-demo.js"
|
@@ -4778,100 +4774,6 @@
|
|
4778
4774
|
}
|
4779
4775
|
]
|
4780
4776
|
},
|
4781
|
-
{
|
4782
|
-
"name": "d2l-form-native",
|
4783
|
-
"path": "./components/form/form-native.js",
|
4784
|
-
"description": "A component that can be used to build sections containing interactive controls that are validated and submitted as a group.\nThese interactive controls are submitted using a native HTML form submission.",
|
4785
|
-
"attributes": [
|
4786
|
-
{
|
4787
|
-
"name": "action",
|
4788
|
-
"description": "The URL that processes the form submission.",
|
4789
|
-
"type": "string",
|
4790
|
-
"default": "\"\""
|
4791
|
-
},
|
4792
|
-
{
|
4793
|
-
"name": "enctype",
|
4794
|
-
"description": "If the value of the method attribute is post, enctype is the MIME type of the form submission.",
|
4795
|
-
"type": "'application/x-www-form-urlencoded'|'multipart/form-data'|'text/plain'",
|
4796
|
-
"default": "\"application/x-www-form-urlencoded\""
|
4797
|
-
},
|
4798
|
-
{
|
4799
|
-
"name": "method",
|
4800
|
-
"description": "The HTTP method to submit the form with.",
|
4801
|
-
"type": "'get'|'post'",
|
4802
|
-
"default": "\"get\""
|
4803
|
-
},
|
4804
|
-
{
|
4805
|
-
"name": "target",
|
4806
|
-
"description": "Indicates where to display the response after submitting the form.",
|
4807
|
-
"type": "'_self '|'_blank'|'_parent'|'_top'",
|
4808
|
-
"default": "\"_self\""
|
4809
|
-
},
|
4810
|
-
{
|
4811
|
-
"name": "track-changes",
|
4812
|
-
"description": "Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.",
|
4813
|
-
"type": "boolean",
|
4814
|
-
"default": "false"
|
4815
|
-
}
|
4816
|
-
],
|
4817
|
-
"properties": [
|
4818
|
-
{
|
4819
|
-
"name": "action",
|
4820
|
-
"attribute": "action",
|
4821
|
-
"description": "The URL that processes the form submission.",
|
4822
|
-
"type": "string",
|
4823
|
-
"default": "\"\""
|
4824
|
-
},
|
4825
|
-
{
|
4826
|
-
"name": "enctype",
|
4827
|
-
"attribute": "enctype",
|
4828
|
-
"description": "If the value of the method attribute is post, enctype is the MIME type of the form submission.",
|
4829
|
-
"type": "'application/x-www-form-urlencoded'|'multipart/form-data'|'text/plain'",
|
4830
|
-
"default": "\"application/x-www-form-urlencoded\""
|
4831
|
-
},
|
4832
|
-
{
|
4833
|
-
"name": "method",
|
4834
|
-
"attribute": "method",
|
4835
|
-
"description": "The HTTP method to submit the form with.",
|
4836
|
-
"type": "'get'|'post'",
|
4837
|
-
"default": "\"get\""
|
4838
|
-
},
|
4839
|
-
{
|
4840
|
-
"name": "target",
|
4841
|
-
"attribute": "target",
|
4842
|
-
"description": "Indicates where to display the response after submitting the form.",
|
4843
|
-
"type": "'_self '|'_blank'|'_parent'|'_top'",
|
4844
|
-
"default": "\"_self\""
|
4845
|
-
},
|
4846
|
-
{
|
4847
|
-
"name": "trackChanges",
|
4848
|
-
"attribute": "track-changes",
|
4849
|
-
"description": "Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.",
|
4850
|
-
"type": "boolean",
|
4851
|
-
"default": "false"
|
4852
|
-
}
|
4853
|
-
],
|
4854
|
-
"events": [
|
4855
|
-
{
|
4856
|
-
"name": "submit",
|
4857
|
-
"description": "Dispatched when the form is submitted. Cancelling this event will prevent form submission."
|
4858
|
-
},
|
4859
|
-
{
|
4860
|
-
"name": "formdata",
|
4861
|
-
"description": "Dispatched after the entry list representing the form's data is constructed. This happens when the form is submitted just prior to submission. The form data can be obtained from the `detail`'s `formData` property."
|
4862
|
-
},
|
4863
|
-
{
|
4864
|
-
"name": "d2l-form-dirty",
|
4865
|
-
"description": "Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not."
|
4866
|
-
}
|
4867
|
-
],
|
4868
|
-
"slots": [
|
4869
|
-
{
|
4870
|
-
"name": "",
|
4871
|
-
"description": "The native and custom form elements that participate in validation and submission"
|
4872
|
-
}
|
4873
|
-
]
|
4874
|
-
},
|
4875
4777
|
{
|
4876
4778
|
"name": "d2l-form",
|
4877
4779
|
"path": "./components/form/form.js",
|
@@ -4913,13 +4815,13 @@
|
|
4913
4815
|
"name": "d2l-form-invalid",
|
4914
4816
|
"description": "Dispatched when the form fails validation. The error map can be obtained from the `detail`'s `errors` property."
|
4915
4817
|
},
|
4916
|
-
{
|
4917
|
-
"name": "d2l-form-submit",
|
4918
|
-
"description": "Dispatched when the form is submitted. The form data can be obtained from the `detail`'s `formData` property."
|
4919
|
-
},
|
4920
4818
|
{
|
4921
4819
|
"name": "d2l-form-dirty",
|
4922
4820
|
"description": "Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not."
|
4821
|
+
},
|
4822
|
+
{
|
4823
|
+
"name": "d2l-form-submit",
|
4824
|
+
"description": "Dispatched when the form is submitted. The form data can be obtained from the `detail`'s `formData` property."
|
4923
4825
|
}
|
4924
4826
|
],
|
4925
4827
|
"slots": [
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@brightspace-ui/core",
|
3
|
-
"version": "3.
|
3
|
+
"version": "3.118.0",
|
4
4
|
"description": "A collection of accessible, free, open-source web components for building Brightspace applications",
|
5
5
|
"type": "module",
|
6
6
|
"repository": "https://github.com/BrightspaceUI/core.git",
|
@@ -1,80 +0,0 @@
|
|
1
|
-
|
2
|
-
import '../../button/button.js';
|
3
|
-
import '../../inputs/input-date.js';
|
4
|
-
import '../../inputs/input-date-time-range.js';
|
5
|
-
import '../../inputs/input-text.js';
|
6
|
-
import '../../validation/validation-custom.js';
|
7
|
-
import '../form-native.js';
|
8
|
-
import { css, html, LitElement } from 'lit';
|
9
|
-
import { inputStyles } from '../../inputs/input-styles.js';
|
10
|
-
import { selectStyles } from '../../inputs/input-select-styles.js';
|
11
|
-
|
12
|
-
class FormNativeDemo extends LitElement {
|
13
|
-
|
14
|
-
static get styles() {
|
15
|
-
return [inputStyles, selectStyles, css`
|
16
|
-
:first-child.d2l-form-demo-container {
|
17
|
-
margin-top: 18px;
|
18
|
-
}
|
19
|
-
.d2l-form-demo-container {
|
20
|
-
margin-bottom: 10px;
|
21
|
-
}
|
22
|
-
`];
|
23
|
-
}
|
24
|
-
|
25
|
-
render() {
|
26
|
-
return html`
|
27
|
-
<d2l-form-native>
|
28
|
-
<div class="d2l-form-demo-container">
|
29
|
-
<d2l-input-text label="Name" type="text" name="name" required minlength="4" maxlength="8"></d2l-input-text>
|
30
|
-
</div>
|
31
|
-
<div class="d2l-form-demo-container">
|
32
|
-
<d2l-input-text label="Email" name="email" type="email"></d2l-input-text>
|
33
|
-
</div>
|
34
|
-
<div class="d2l-form-demo-container">
|
35
|
-
<d2l-validation-custom for="password" @d2l-validation-custom-validate=${this._validatePassword} failure-text="Expected hunter2 or 12345" ></d2l-validation-custom>
|
36
|
-
<d2l-input-text label="Password" id="password" name="password" required type="password"></d2l-input-text>
|
37
|
-
</div>
|
38
|
-
<fieldset class="d2l-form-demo-container">
|
39
|
-
<legend>Choose your favorite monster</legend>
|
40
|
-
<input type="radio" id="kraken" name="monster" value="kraken">
|
41
|
-
<label for="kraken">Kraken</label><br />
|
42
|
-
<input type="radio" id="sasquatch" name="monster" value="sasquatch">
|
43
|
-
<label for="sasquatch">Sasquatch</label><br />
|
44
|
-
</fieldset>
|
45
|
-
<div class="d2l-form-demo-container">
|
46
|
-
<label for="pet-select">Favorite Pet</label><br />
|
47
|
-
<select class="d2l-input-select" name="pets" id="pet-select" required>
|
48
|
-
<option value="">--Please choose an option--</option>
|
49
|
-
<option value="porpoise">Porpoise</option>
|
50
|
-
<option value="house hippo">House Hippo</option>
|
51
|
-
<option value="spiker monkey">Spider Monkey</option>
|
52
|
-
<option value="capybara">Capybara</option>
|
53
|
-
</select>
|
54
|
-
</div>
|
55
|
-
<d2l-input-date label="Date" name="my-date" required></d2l-input-date>
|
56
|
-
<div class="d2l-form-demo-container">
|
57
|
-
<label for="story">Tell us your story</label>
|
58
|
-
<textarea class="d2l-input" minlength="20" id="story" name="story" rows="5" cols="33">It was...</textarea>
|
59
|
-
</label>
|
60
|
-
</div>
|
61
|
-
<d2l-input-date-time-range label="Assignment Dates" required min-value="2018-08-27T12:30:00Z" max-value="2018-09-30T12:30:00Z"></d2l-input-date-time-range>
|
62
|
-
<div class="d2l-form-demo-container">
|
63
|
-
<label for="file">Super Secret File</label><br />
|
64
|
-
<input type="file" id="file" name="super-secret-file">
|
65
|
-
</div>
|
66
|
-
<button name="action" value="save" type="submit" @click=${this._onClick}>Save</button>
|
67
|
-
</d2l-form-native>
|
68
|
-
`;
|
69
|
-
}
|
70
|
-
|
71
|
-
_onClick(e) {
|
72
|
-
if (this.shadowRoot) this.shadowRoot.querySelector('d2l-form-native').requestSubmit(e.target);
|
73
|
-
}
|
74
|
-
|
75
|
-
_validatePassword(e) {
|
76
|
-
e.detail.resolve(e.detail.forElement.value === 'hunter2' || e.detail.forElement.value === '12345');
|
77
|
-
}
|
78
|
-
|
79
|
-
}
|
80
|
-
customElements.define('d2l-form-native-demo', FormNativeDemo);
|
@@ -1,29 +0,0 @@
|
|
1
|
-
<!DOCTYPE html>
|
2
|
-
<html lang="en">
|
3
|
-
|
4
|
-
<head>
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
-
<meta charset="UTF-8">
|
7
|
-
<link rel="stylesheet" href="../../demo/styles.css" type="text/css">
|
8
|
-
<script type="module">
|
9
|
-
import '../../demo/demo-page.js';
|
10
|
-
import './form-native-demo.js';
|
11
|
-
</script>
|
12
|
-
</head>
|
13
|
-
|
14
|
-
<body unresolved>
|
15
|
-
|
16
|
-
<d2l-demo-page page-title="d2l-form-native">
|
17
|
-
|
18
|
-
<h2>Basic</h2>
|
19
|
-
<d2l-demo-snippet>
|
20
|
-
<template>
|
21
|
-
<d2l-form-native-demo></d2l-form-native-demo>
|
22
|
-
</template>
|
23
|
-
</d2l-demo-snippet>
|
24
|
-
|
25
|
-
</d2l-demo-page>
|
26
|
-
|
27
|
-
</body>
|
28
|
-
|
29
|
-
</html>
|
@@ -1,202 +0,0 @@
|
|
1
|
-
import './form-errory-summary.js';
|
2
|
-
import '../tooltip/tooltip.js';
|
3
|
-
import '../link/link.js';
|
4
|
-
import { isCustomFormElement, isNativeFormElement } from './form-helper.js';
|
5
|
-
import { getComposedActiveElement } from '../../helpers/focus.js';
|
6
|
-
import { getUniqueId } from '../../helpers/uniqueId.js';
|
7
|
-
import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
|
8
|
-
import { localizeFormElement } from './form-element-localize-helper.js';
|
9
|
-
|
10
|
-
export const FormMixin = superclass => class extends LocalizeCoreElement(superclass) {
|
11
|
-
|
12
|
-
static get properties() {
|
13
|
-
return {
|
14
|
-
/**
|
15
|
-
* Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.
|
16
|
-
* @type {boolean}
|
17
|
-
*/
|
18
|
-
trackChanges: { type: Boolean, attribute: 'track-changes', reflect: true },
|
19
|
-
_errors: { type: Object },
|
20
|
-
_hasErrors: { type: Boolean, attribute: '_has-errors', reflect: true },
|
21
|
-
};
|
22
|
-
}
|
23
|
-
|
24
|
-
constructor() {
|
25
|
-
super();
|
26
|
-
this._onUnload = this._onUnload.bind(this);
|
27
|
-
this._onNativeSubmit = this._onNativeSubmit.bind(this);
|
28
|
-
|
29
|
-
this.trackChanges = false;
|
30
|
-
this._errors = new Map();
|
31
|
-
this._firstUpdateResolve = null;
|
32
|
-
this._firstUpdatePromise = new Promise((resolve) => {
|
33
|
-
this._firstUpdateResolve = resolve;
|
34
|
-
});
|
35
|
-
this._tooltips = new Map();
|
36
|
-
this._validationCustoms = new Set();
|
37
|
-
|
38
|
-
this.addEventListener('d2l-form-errors-change', this._onErrorsChange);
|
39
|
-
this.addEventListener('d2l-form-element-errors-change', this._onErrorsChange);
|
40
|
-
this.addEventListener('d2l-validation-custom-connected', this._validationCustomConnected);
|
41
|
-
}
|
42
|
-
|
43
|
-
connectedCallback() {
|
44
|
-
super.connectedCallback();
|
45
|
-
window.addEventListener('beforeunload', this._onUnload);
|
46
|
-
}
|
47
|
-
|
48
|
-
disconnectedCallback() {
|
49
|
-
super.disconnectedCallback();
|
50
|
-
window.removeEventListener('beforeunload', this._onUnload);
|
51
|
-
}
|
52
|
-
|
53
|
-
firstUpdated(changedProperties) {
|
54
|
-
super.firstUpdated(changedProperties);
|
55
|
-
this.addEventListener('change', this._onFormElementChange);
|
56
|
-
this.addEventListener('input', this._onFormElementChange);
|
57
|
-
this.addEventListener('focusout', this._onFormElementChange);
|
58
|
-
this._firstUpdateResolve();
|
59
|
-
}
|
60
|
-
|
61
|
-
willUpdate(changedProperties) {
|
62
|
-
super.willUpdate(changedProperties);
|
63
|
-
if (changedProperties.has('_errors')) {
|
64
|
-
this._hasErrors = this._errors.size > 0;
|
65
|
-
}
|
66
|
-
}
|
67
|
-
|
68
|
-
// eslint-disable-next-line no-unused-vars
|
69
|
-
async requestSubmit(submitter) {
|
70
|
-
throw new Error('FormMixin.requestSubmit must be overridden');
|
71
|
-
}
|
72
|
-
|
73
|
-
async submit() {
|
74
|
-
throw new Error('FormMixin.submit must be overridden');
|
75
|
-
}
|
76
|
-
|
77
|
-
async validate() {
|
78
|
-
throw new Error('FormMixin.validate must be overridden');
|
79
|
-
}
|
80
|
-
|
81
|
-
_displayInvalid(ele, message) {
|
82
|
-
let tooltip = this._tooltips.get(ele);
|
83
|
-
if (!tooltip) {
|
84
|
-
tooltip = document.createElement('d2l-tooltip');
|
85
|
-
tooltip.for = ele.id;
|
86
|
-
tooltip.align = 'start';
|
87
|
-
tooltip.state = 'error';
|
88
|
-
ele.parentNode.append(tooltip);
|
89
|
-
this._tooltips.set(ele, tooltip);
|
90
|
-
|
91
|
-
tooltip.appendChild(document.createTextNode(message));
|
92
|
-
} else if (tooltip.innerText.trim() !== message.trim()) {
|
93
|
-
tooltip.textContent = '';
|
94
|
-
tooltip.appendChild(document.createTextNode(message));
|
95
|
-
tooltip.updatePosition();
|
96
|
-
}
|
97
|
-
ele.setAttribute('aria-invalid', 'true');
|
98
|
-
}
|
99
|
-
|
100
|
-
_displayValid(ele) {
|
101
|
-
const tooltip = this._tooltips.get(ele);
|
102
|
-
if (tooltip) {
|
103
|
-
this._tooltips.delete(ele);
|
104
|
-
tooltip.remove();
|
105
|
-
}
|
106
|
-
ele.setAttribute('aria-invalid', 'false');
|
107
|
-
}
|
108
|
-
|
109
|
-
_onErrorsChange(e) {
|
110
|
-
if (e.target === this) {
|
111
|
-
return;
|
112
|
-
}
|
113
|
-
e.stopPropagation();
|
114
|
-
this._updateErrors(e.target, e.detail.errors);
|
115
|
-
}
|
116
|
-
|
117
|
-
async _onFormElementChange(e) {
|
118
|
-
const ele = e.target;
|
119
|
-
|
120
|
-
if ((isNativeFormElement(ele) || isCustomFormElement(ele)) && e.type !== 'focusout') {
|
121
|
-
this._dirty = true;
|
122
|
-
/** Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not. */
|
123
|
-
this.dispatchEvent(new CustomEvent('d2l-form-dirty'));
|
124
|
-
}
|
125
|
-
|
126
|
-
if (!isNativeFormElement(ele)) {
|
127
|
-
return;
|
128
|
-
}
|
129
|
-
e.stopPropagation();
|
130
|
-
const errors = await this._validateFormElement(ele, e.type === 'focusout');
|
131
|
-
this._updateErrors(ele, errors);
|
132
|
-
}
|
133
|
-
|
134
|
-
_onNativeSubmit(e) {
|
135
|
-
e.preventDefault();
|
136
|
-
e.stopPropagation();
|
137
|
-
const submitter = e.submitter || getComposedActiveElement();
|
138
|
-
this.requestSubmit(submitter);
|
139
|
-
}
|
140
|
-
|
141
|
-
_onUnload(e) {
|
142
|
-
if (this.trackChanges && this._dirty) {
|
143
|
-
e.preventDefault();
|
144
|
-
e.returnValue = false;
|
145
|
-
}
|
146
|
-
}
|
147
|
-
|
148
|
-
_updateErrors(ele, errors) {
|
149
|
-
|
150
|
-
if (!this._errors.has(ele)) {
|
151
|
-
return false;
|
152
|
-
}
|
153
|
-
if (Array.from(errors).length === 0) {
|
154
|
-
this._errors.delete(ele);
|
155
|
-
} else {
|
156
|
-
this._errors.set(ele, errors);
|
157
|
-
}
|
158
|
-
const detail = { bubbles: true, composed: true, detail: { errors: this._errors } };
|
159
|
-
/** @ignore */
|
160
|
-
this.dispatchEvent(new CustomEvent('d2l-form-errors-change', detail));
|
161
|
-
this.requestUpdate('_errors');
|
162
|
-
return true;
|
163
|
-
}
|
164
|
-
|
165
|
-
async _validateFormElement(ele, showNewErrors) {
|
166
|
-
// if validation occurs before we've rendered,
|
167
|
-
// localization may not have loaded yet
|
168
|
-
await this._firstUpdatePromise;
|
169
|
-
ele.id = ele.id || getUniqueId();
|
170
|
-
if (isCustomFormElement(ele)) {
|
171
|
-
return ele.validate(showNewErrors);
|
172
|
-
} else if (isNativeFormElement(ele)) {
|
173
|
-
const customs = [...this._validationCustoms].filter(custom => custom.forElement === ele);
|
174
|
-
const results = await Promise.all(customs.map(custom => custom.validate()));
|
175
|
-
const errors = customs.map(custom => custom.failureText).filter((_, i) => !results[i]);
|
176
|
-
if (!ele.checkValidity()) {
|
177
|
-
const validationMessage = localizeFormElement(this.localize.bind(this), ele);
|
178
|
-
errors.unshift(validationMessage);
|
179
|
-
}
|
180
|
-
if (errors.length > 0 && (showNewErrors || ele.getAttribute('aria-invalid') === 'true')) {
|
181
|
-
this._displayInvalid(ele, errors[0]);
|
182
|
-
} else {
|
183
|
-
this._displayValid(ele);
|
184
|
-
}
|
185
|
-
return errors;
|
186
|
-
}
|
187
|
-
return [];
|
188
|
-
}
|
189
|
-
|
190
|
-
_validationCustomConnected(e) {
|
191
|
-
e.stopPropagation();
|
192
|
-
const custom = e.composedPath()[0];
|
193
|
-
this._validationCustoms.add(custom);
|
194
|
-
|
195
|
-
const onDisconnect = () => {
|
196
|
-
custom.removeEventListener('d2l-validation-custom-disconnected', onDisconnect);
|
197
|
-
this._validationCustoms.delete(custom);
|
198
|
-
};
|
199
|
-
custom.addEventListener('d2l-validation-custom-disconnected', onDisconnect);
|
200
|
-
}
|
201
|
-
|
202
|
-
};
|
@@ -1,148 +0,0 @@
|
|
1
|
-
import { css, html, LitElement } from 'lit';
|
2
|
-
import { findFormElements, getFormElementData, isCustomFormElement, isNativeFormElement } from './form-helper.js';
|
3
|
-
import { FormMixin } from './form-mixin.js';
|
4
|
-
import { getUniqueId } from '../../helpers/uniqueId.js';
|
5
|
-
|
6
|
-
/**
|
7
|
-
* A component that can be used to build sections containing interactive controls that are validated and submitted as a group.
|
8
|
-
* These interactive controls are submitted using a native HTML form submission.
|
9
|
-
* @slot - The native and custom form elements that participate in validation and submission
|
10
|
-
* @fires submit - Dispatched when the form is submitted. Cancelling this event will prevent form submission.
|
11
|
-
*/
|
12
|
-
class FormNative extends FormMixin(LitElement) {
|
13
|
-
|
14
|
-
static get properties() {
|
15
|
-
return {
|
16
|
-
/**
|
17
|
-
* The URL that processes the form submission.
|
18
|
-
* @type {string}
|
19
|
-
*/
|
20
|
-
action: { type: String },
|
21
|
-
/**
|
22
|
-
* If the value of the method attribute is post, enctype is the MIME type of the form submission.
|
23
|
-
* @type {'application/x-www-form-urlencoded'|'multipart/form-data'|'text/plain'}
|
24
|
-
*/
|
25
|
-
enctype: { type: String },
|
26
|
-
/**
|
27
|
-
* The HTTP method to submit the form with.
|
28
|
-
* @type {'get'|'post'}
|
29
|
-
*/
|
30
|
-
method: { type: String },
|
31
|
-
/**
|
32
|
-
* Indicates where to display the response after submitting the form.
|
33
|
-
* @type {'_self '|'_blank'|'_parent'|'_top'}
|
34
|
-
*/
|
35
|
-
target: { type: String },
|
36
|
-
};
|
37
|
-
}
|
38
|
-
|
39
|
-
static get styles() {
|
40
|
-
return css`
|
41
|
-
:host {
|
42
|
-
display: block;
|
43
|
-
}
|
44
|
-
:host([hidden]) {
|
45
|
-
display: none;
|
46
|
-
}
|
47
|
-
`;
|
48
|
-
}
|
49
|
-
|
50
|
-
constructor() {
|
51
|
-
super();
|
52
|
-
this.action = '';
|
53
|
-
this.enctype = 'application/x-www-form-urlencoded';
|
54
|
-
this.method = 'get';
|
55
|
-
this.target = '_self';
|
56
|
-
}
|
57
|
-
|
58
|
-
render() {
|
59
|
-
const errors = [...this._errors]
|
60
|
-
.filter(([, eleErrors]) => eleErrors.length > 0)
|
61
|
-
.map(([ele, eleErrors]) => ({ href: `#${ele.id}`, message: eleErrors[0], onClick: () => ele.focus() }));
|
62
|
-
return html`
|
63
|
-
<d2l-form-error-summary .errors=${errors}></d2l-form-error-summary>
|
64
|
-
<slot></slot>
|
65
|
-
`;
|
66
|
-
}
|
67
|
-
|
68
|
-
shouldUpdate(changedProperties) {
|
69
|
-
if (!super.shouldUpdate(changedProperties)) {
|
70
|
-
return false;
|
71
|
-
}
|
72
|
-
const ignoredProps = new Set(['action', 'enctype', 'method', 'target']);
|
73
|
-
return [...changedProperties].filter(([prop]) => !ignoredProps.has(prop)).length > 0;
|
74
|
-
}
|
75
|
-
|
76
|
-
async requestSubmit(submitter) {
|
77
|
-
const errors = await this.validate();
|
78
|
-
if (errors.size > 0) {
|
79
|
-
return;
|
80
|
-
}
|
81
|
-
this._dirty = false;
|
82
|
-
|
83
|
-
const form = document.createElement('form');
|
84
|
-
form.addEventListener('formdata', this._onFormData);
|
85
|
-
form.id = getUniqueId();
|
86
|
-
form.action = this.action;
|
87
|
-
form.enctype = this.enctype;
|
88
|
-
form.method = this.method;
|
89
|
-
form.target = this.target;
|
90
|
-
this.appendChild(form);
|
91
|
-
|
92
|
-
let customFormData = {};
|
93
|
-
const formElements = findFormElements(this);
|
94
|
-
for (const ele of formElements) {
|
95
|
-
const eleData = getFormElementData(ele, submitter);
|
96
|
-
const isCustom = isCustomFormElement(ele);
|
97
|
-
if (isCustom || ele === submitter) {
|
98
|
-
customFormData = { ...customFormData, ...eleData };
|
99
|
-
}
|
100
|
-
if (!isCustom && isNativeFormElement(ele)) {
|
101
|
-
ele.setAttribute('form', form.id);
|
102
|
-
}
|
103
|
-
}
|
104
|
-
for (const entry of Object.entries(customFormData)) {
|
105
|
-
const input = document.createElement('input');
|
106
|
-
input.type = 'hidden';
|
107
|
-
input.name = entry[0];
|
108
|
-
input.value = entry[1];
|
109
|
-
form.appendChild(input);
|
110
|
-
}
|
111
|
-
const submit = this.dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true }));
|
112
|
-
/** Dispatched after the entry list representing the form's data is constructed. This happens when the form is submitted just prior to submission. The form data can be obtained from the `detail`'s `formData` property. */
|
113
|
-
this.dispatchEvent(new CustomEvent('formdata', { detail: { formData: new FormData(form) } }));
|
114
|
-
if (submit) {
|
115
|
-
form.submit();
|
116
|
-
}
|
117
|
-
form.remove();
|
118
|
-
}
|
119
|
-
|
120
|
-
async submit() {
|
121
|
-
return this.requestSubmit(null);
|
122
|
-
}
|
123
|
-
|
124
|
-
async validate() {
|
125
|
-
let errors = [];
|
126
|
-
const errorMap = new Map();
|
127
|
-
const formElements = findFormElements(this);
|
128
|
-
for (const ele of formElements) {
|
129
|
-
const eleErrors = await this._validateFormElement(ele, true);
|
130
|
-
if (eleErrors.length > 0) {
|
131
|
-
errors = [...errors, ...eleErrors];
|
132
|
-
errorMap.set(ele, eleErrors);
|
133
|
-
}
|
134
|
-
}
|
135
|
-
this._errors = errorMap;
|
136
|
-
if (this.shadowRoot && errorMap.size > 0) {
|
137
|
-
const errorSummary = this.shadowRoot.querySelector('d2l-form-error-summary');
|
138
|
-
this.updateComplete.then(() => errorSummary.focus());
|
139
|
-
}
|
140
|
-
return errorMap;
|
141
|
-
}
|
142
|
-
|
143
|
-
_onFormData(e) {
|
144
|
-
e.stopPropagation();
|
145
|
-
}
|
146
|
-
|
147
|
-
}
|
148
|
-
customElements.define('d2l-form-native', FormNative);
|