enhance-form 0.0.1
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/README.md +171 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +278 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# enhance-form
|
|
2
|
+
|
|
3
|
+
A custom element that progressively enhances HTML forms with AJAX submission, per-field validation, and targeted DOM updates.
|
|
4
|
+
|
|
5
|
+
Without JavaScript the form works as a normal HTML form. With JavaScript, `<enhance-form>` intercepts submissions and validation, swapping parts of the page with the server response.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm install enhance-form
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Import the package to register the `<enhance-form>` custom element:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import "enhance-form";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Wrap a `<form>` element:
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<enhance-form target="#result" fail-target="#errors">
|
|
25
|
+
<form action="/submit" method="POST">
|
|
26
|
+
<fieldset enhance-form-group>
|
|
27
|
+
<label for="email">Email</label>
|
|
28
|
+
<input id="email" name="email" type="email" enhance-validate />
|
|
29
|
+
</fieldset>
|
|
30
|
+
|
|
31
|
+
<fieldset enhance-form-group>
|
|
32
|
+
<label for="name">Name</label>
|
|
33
|
+
<input id="name" name="name" type="text" enhance-validate />
|
|
34
|
+
</fieldset>
|
|
35
|
+
|
|
36
|
+
<button type="submit">Submit</button>
|
|
37
|
+
</form>
|
|
38
|
+
</enhance-form>
|
|
39
|
+
|
|
40
|
+
<div id="result"></div>
|
|
41
|
+
<div id="errors"></div>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Attributes
|
|
45
|
+
|
|
46
|
+
### `<enhance-form>`
|
|
47
|
+
|
|
48
|
+
| Attribute | Description |
|
|
49
|
+
| ------------- | --------------------------------------------------------------------------- |
|
|
50
|
+
| `target` | CSS selector for the element to replace with the response on success (200). |
|
|
51
|
+
| `fail-target` | CSS selector for the element to replace on server errors (5XX). Defaults to `target`. |
|
|
52
|
+
|
|
53
|
+
### On child elements
|
|
54
|
+
|
|
55
|
+
| Attribute | Used on | Description |
|
|
56
|
+
| -------------------- | ------------------------ | ------------------------------------------------------------------ |
|
|
57
|
+
| `enhance-form-group` | `<fieldset>` or any element | Groups an input with its label and validation messages. Used to scope per-field validation replacement. |
|
|
58
|
+
| `enhance-validate` | `<input>`, `<textarea>` | Enables per-field validation on blur. |
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
### Per-field validation (blur)
|
|
63
|
+
|
|
64
|
+
When a user blurs an input with the `enhance-validate` attribute:
|
|
65
|
+
|
|
66
|
+
1. The component POSTs the full form data to the form's `action` URL.
|
|
67
|
+
2. A `X-Enhance-Validate` request header is sent with the field name as its value.
|
|
68
|
+
3. The server responds with HTML containing the form (with validation state).
|
|
69
|
+
4. The component extracts the matching `[enhance-form-group]` or `<fieldset>` that contains the validated field and replaces only that group in the DOM.
|
|
70
|
+
|
|
71
|
+
Empty fields are skipped. Concurrent validation requests for the same field are aborted automatically.
|
|
72
|
+
|
|
73
|
+
### Form submission
|
|
74
|
+
|
|
75
|
+
When the form is submitted:
|
|
76
|
+
|
|
77
|
+
1. Default submission is prevented.
|
|
78
|
+
2. Any in-flight per-field validation requests are aborted.
|
|
79
|
+
3. The component POSTs the form data with a `X-Enhance-Submit` request header.
|
|
80
|
+
4. The response is handled based on the status code:
|
|
81
|
+
|
|
82
|
+
| Status | Behavior |
|
|
83
|
+
| ------ | -------- |
|
|
84
|
+
| **200** | The element matching `target` is replaced with the corresponding element from the response. Uses the View Transitions API if available. |
|
|
85
|
+
| **4XX** | The `<form>` is replaced with the form from the response (expected to contain validation errors). The first input with `aria-invalid="true"` is focused. |
|
|
86
|
+
| **5XX** | The element matching `fail-target` is replaced with the corresponding element from the response. |
|
|
87
|
+
|
|
88
|
+
If the response includes an `X-Enhance-Redirect` header, the browser navigates to that URL via `window.location.assign`.
|
|
89
|
+
|
|
90
|
+
## Server contract
|
|
91
|
+
|
|
92
|
+
The server must inspect the custom request headers to determine what kind of request it's handling and respond accordingly.
|
|
93
|
+
|
|
94
|
+
### Request headers
|
|
95
|
+
|
|
96
|
+
| Header | Value | Meaning |
|
|
97
|
+
| -------------------- | ------------ | ------------------------------------------------ |
|
|
98
|
+
| `X-Enhance-Submit` | `"form"` | This is a full form submission via the component. |
|
|
99
|
+
| `X-Enhance-Validate` | Field `name` | This is a validation-only request for a single field. |
|
|
100
|
+
|
|
101
|
+
When neither header is present, treat it as a normal (non-JS) form submission.
|
|
102
|
+
|
|
103
|
+
### Response expectations
|
|
104
|
+
|
|
105
|
+
The server should always respond with **HTML**.
|
|
106
|
+
|
|
107
|
+
**On validation requests** (`X-Enhance-Validate`):
|
|
108
|
+
|
|
109
|
+
Return the full form markup. The component will extract the relevant form group itself. Include any validation errors or `aria-invalid` attributes on the validated field.
|
|
110
|
+
|
|
111
|
+
**On submit requests** (`X-Enhance-Submit`):
|
|
112
|
+
|
|
113
|
+
| Status | What to return |
|
|
114
|
+
| ------ | -------------- |
|
|
115
|
+
| **200** | HTML containing an element matching the `target` selector (the success state). |
|
|
116
|
+
| **4XX** | HTML containing the `<form>` with validation errors. Mark invalid inputs with `aria-invalid="true"`. |
|
|
117
|
+
| **5XX** | HTML containing an element matching the `fail-target` selector (the error state). |
|
|
118
|
+
|
|
119
|
+
### Response headers
|
|
120
|
+
|
|
121
|
+
| Header | Value | Effect |
|
|
122
|
+
| -------------------- | ----- | ------------------------------------------------ |
|
|
123
|
+
| `X-Enhance-Redirect` | URL | The browser will navigate to this URL instead of updating the DOM. |
|
|
124
|
+
|
|
125
|
+
### Accessing headers in code
|
|
126
|
+
|
|
127
|
+
The package exports the header names as constants:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
import { EnhancedHeader } from "enhance-form";
|
|
131
|
+
|
|
132
|
+
EnhancedHeader.Submit // "X-Enhance-Submit"
|
|
133
|
+
EnhancedHeader.Validate // "X-Enhance-Validate"
|
|
134
|
+
EnhancedHeader.Redirect // "X-Enhance-Redirect"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Server example
|
|
138
|
+
|
|
139
|
+
A minimal Express handler:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
import { EnhancedHeader } from "enhance-form";
|
|
143
|
+
|
|
144
|
+
app.post("/submit", (req, res) => {
|
|
145
|
+
const isEnhancedSubmit = req.headers[EnhancedHeader.Submit.toLowerCase()];
|
|
146
|
+
const validateField = req.headers[EnhancedHeader.Validate.toLowerCase()];
|
|
147
|
+
|
|
148
|
+
const errors = validate(req.body);
|
|
149
|
+
|
|
150
|
+
if (validateField) {
|
|
151
|
+
// Return the form with validation state for the field
|
|
152
|
+
return res.status(errors ? 422 : 200).send(renderForm(req.body, errors));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (errors) {
|
|
156
|
+
return res.status(422).send(renderForm(req.body, errors));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isEnhancedSubmit) {
|
|
160
|
+
// Return just the success content that matches the target selector
|
|
161
|
+
return res.send('<div id="result"><p>Saved.</p></div>');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Non-JS fallback: normal redirect
|
|
165
|
+
res.redirect("/success");
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom headers used by the EnhanceForm component for progressive enhancement.
|
|
3
|
+
* @constant
|
|
4
|
+
*/
|
|
5
|
+
export declare const EnhancedHeader: {
|
|
6
|
+
/** Response header. Will cause the browser to go to url via window.location.assign */
|
|
7
|
+
readonly Redirect: "X-Enhance-Redirect";
|
|
8
|
+
/** Request header. Indicates a full submit. */
|
|
9
|
+
readonly Submit: "X-Enhance-Submit";
|
|
10
|
+
/** Request header. Indicates that we're only interested in doing a validation */
|
|
11
|
+
readonly Validate: "X-Enhance-Validate";
|
|
12
|
+
};
|
|
13
|
+
export type EnhancedFormHeader = (typeof EnhancedHeader)[keyof typeof EnhancedHeader];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom headers used by the EnhanceForm component for progressive enhancement.
|
|
3
|
+
* @constant
|
|
4
|
+
*/
|
|
5
|
+
export const EnhancedHeader = {
|
|
6
|
+
/** Response header. Will cause the browser to go to url via window.location.assign */
|
|
7
|
+
Redirect: "X-Enhance-Redirect",
|
|
8
|
+
/** Request header. Indicates a full submit. */
|
|
9
|
+
Submit: "X-Enhance-Submit",
|
|
10
|
+
/** Request header. Indicates that we're only interested in doing a validation */
|
|
11
|
+
Validate: "X-Enhance-Validate",
|
|
12
|
+
};
|
|
13
|
+
const parser = new DOMParser();
|
|
14
|
+
/**
|
|
15
|
+
* A custom web component that progressively enhances HTML forms with AJAX submission,
|
|
16
|
+
* client-side validation, and targeted DOM updates.
|
|
17
|
+
*
|
|
18
|
+
* @class EnhanceForm
|
|
19
|
+
* @extends HTMLElement
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```html
|
|
23
|
+
* <enhance-form target="#results" fail-target="#error-container">
|
|
24
|
+
* <form action="/submit" method="POST">
|
|
25
|
+
* <fieldset enhance-form-group>
|
|
26
|
+
* <input name="email" enhance-validate />
|
|
27
|
+
* </fieldset>
|
|
28
|
+
* <button type="submit">Submit</button>
|
|
29
|
+
* </form>
|
|
30
|
+
* </enhance-form>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @attr {string} target - CSS selector for the element to replace on successful submission
|
|
34
|
+
* @attr {string} fail-target - CSS selector for the element to replace on server errors (defaults to target)
|
|
35
|
+
*/
|
|
36
|
+
class EnhanceForm extends HTMLElement {
|
|
37
|
+
/** The custom element tag name */
|
|
38
|
+
static tagName = "enhance-form";
|
|
39
|
+
/** The form element contained within this component */
|
|
40
|
+
_form;
|
|
41
|
+
/** CSS selector for the target element to update on success */
|
|
42
|
+
_targetSelector;
|
|
43
|
+
/** CSS selector for the target element to update on failure */
|
|
44
|
+
_failTargetSelector;
|
|
45
|
+
/** Map of abort controllers for managing concurrent requests */
|
|
46
|
+
controllers = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Creates an instance of EnhanceForm.
|
|
49
|
+
* @throws {Error} If no form element is found within the component
|
|
50
|
+
*/
|
|
51
|
+
constructor() {
|
|
52
|
+
super();
|
|
53
|
+
const form = this.querySelector("form");
|
|
54
|
+
this._targetSelector = this.getAttribute("target") || "";
|
|
55
|
+
this._failTargetSelector =
|
|
56
|
+
this.getAttribute("fail-target") || this.getAttribute("target") || "";
|
|
57
|
+
if (!form) {
|
|
58
|
+
throw new Error("No form found in <enhance-form>");
|
|
59
|
+
}
|
|
60
|
+
this._form = form;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Handles individual input field validation on blur events.
|
|
64
|
+
* Sends an AJAX request to validate the field and updates the UI with the response.
|
|
65
|
+
*
|
|
66
|
+
* @param {Event} event - The blur event from the input field
|
|
67
|
+
* @returns {Promise<void>}
|
|
68
|
+
*
|
|
69
|
+
* @remarks
|
|
70
|
+
* - Skips validation for empty inputs
|
|
71
|
+
* - Aborts any pending validation requests for the same field
|
|
72
|
+
* - Updates only the form group containing the validated input
|
|
73
|
+
*/
|
|
74
|
+
async handleInputValidation(event) {
|
|
75
|
+
const target = event.target;
|
|
76
|
+
// If the input is empty, we don't want to validate it.
|
|
77
|
+
if (target.value === "")
|
|
78
|
+
return;
|
|
79
|
+
this.controllers.get(target.name)?.abort("Aborted by user");
|
|
80
|
+
this.controllers.set(target.name, new AbortController());
|
|
81
|
+
const signal = this.controllers.get(target.name)?.signal;
|
|
82
|
+
const groupSelector = `:is([enhance-form-group], fieldset):has([name="${target.name}"])`;
|
|
83
|
+
try {
|
|
84
|
+
const headers = addHeader(EnhancedHeader.Validate, target.name);
|
|
85
|
+
const { html } = await this.requestForm(headers, signal);
|
|
86
|
+
if (!html)
|
|
87
|
+
return;
|
|
88
|
+
const newFormGroup = this.replaceElements(html, groupSelector);
|
|
89
|
+
const newValidateInput = newFormGroup?.querySelector(":is(input, textarea)[enhance-validate]");
|
|
90
|
+
if (newValidateInput) {
|
|
91
|
+
this.bindEvents(newValidateInput);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.warn("Form validation:", error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Handles form submission with AJAX.
|
|
100
|
+
* Prevents default form submission and handles the response based on status codes.
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* Response handling:
|
|
104
|
+
* - 200: Updates the target element with the response
|
|
105
|
+
* - 4XX: Replaces the form with validation errors
|
|
106
|
+
* - 5XX: Updates the fail-target element
|
|
107
|
+
* - Redirect header: Navigates to the specified URL
|
|
108
|
+
*/
|
|
109
|
+
async handleFormValidation(event) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
try {
|
|
112
|
+
// abort any individual input validation requests.
|
|
113
|
+
this.controllers.forEach((controller) => {
|
|
114
|
+
controller.abort();
|
|
115
|
+
});
|
|
116
|
+
this.controllers.set("form", new AbortController());
|
|
117
|
+
const { html, response } = await this.requestForm(addHeader(EnhancedHeader.Submit, "form"), this.controllers.get("form")?.signal);
|
|
118
|
+
if (getHeader(response, EnhancedHeader.Redirect) !== null) {
|
|
119
|
+
this.handleRedirect(response);
|
|
120
|
+
}
|
|
121
|
+
if (!html)
|
|
122
|
+
return;
|
|
123
|
+
// We expect the same form to be returned with all 4XX errors.
|
|
124
|
+
if (response.status >= 400 && response.status <= 499) {
|
|
125
|
+
const newForm = this.replaceElements(html, "form");
|
|
126
|
+
this._form = newForm;
|
|
127
|
+
this.bindEvents();
|
|
128
|
+
}
|
|
129
|
+
// We do NOT expect the same form to be returned with 5XX errors.
|
|
130
|
+
if (response.status >= 500 && response.status <= 599) {
|
|
131
|
+
this.replaceElements(html, this._failTargetSelector, this);
|
|
132
|
+
}
|
|
133
|
+
if (response.status === 200) {
|
|
134
|
+
const target = this.closest(this._targetSelector) !== null ? this.closest(this._targetSelector) : this;
|
|
135
|
+
viewTransition(() => this.replaceElements(html, this._targetSelector, target));
|
|
136
|
+
}
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.warn("Form validation:", error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Handles redirect responses by navigating to the specified URL.
|
|
145
|
+
*
|
|
146
|
+
* @param {Response} response - The fetch response containing redirect headers
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
handleRedirect(response) {
|
|
150
|
+
const redirectString = getHeader(response, EnhancedHeader.Redirect);
|
|
151
|
+
if (!redirectString)
|
|
152
|
+
return;
|
|
153
|
+
window.location.assign(redirectString);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Focuses the first input field with validation errors after form submission.
|
|
157
|
+
* Also positions the cursor at the end of the input value if possible.
|
|
158
|
+
*
|
|
159
|
+
* @private
|
|
160
|
+
*/
|
|
161
|
+
focusFirstInvalidInput() {
|
|
162
|
+
const firstInvalidInput = this._form.querySelector(':is(input, textarea)[aria-invalid="true"]');
|
|
163
|
+
if (!firstInvalidInput)
|
|
164
|
+
return;
|
|
165
|
+
firstInvalidInput.focus();
|
|
166
|
+
// If possible, put cursor at the end of the focused input
|
|
167
|
+
if ("setSelectionRange" in firstInvalidInput) {
|
|
168
|
+
firstInvalidInput.setSelectionRange(-1, -1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Sends a POST request to the form action URL with the form data.
|
|
173
|
+
*
|
|
174
|
+
* @param {Headers} [headers] - Optional headers to include in the request
|
|
175
|
+
* @param {AbortSignal} [signal] - Optional abort signal for cancelling the request
|
|
176
|
+
* @returns {Promise<{response: Response, html: Document | null}>} The response and parsed HTML
|
|
177
|
+
* @private
|
|
178
|
+
*
|
|
179
|
+
* @remarks
|
|
180
|
+
* - Automatically redirects for external URLs
|
|
181
|
+
* - Parses the response as HTML
|
|
182
|
+
*/
|
|
183
|
+
async requestForm(headers, signal) {
|
|
184
|
+
const response = await fetch(this._form.action, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers,
|
|
187
|
+
body: new FormData(this._form),
|
|
188
|
+
signal,
|
|
189
|
+
});
|
|
190
|
+
if (response.redirected && !isInternalUrl(response.url)) {
|
|
191
|
+
window.location.assign(response.url);
|
|
192
|
+
return { response, html: null };
|
|
193
|
+
}
|
|
194
|
+
const html = parser.parseFromString(await response.text(), "text/html");
|
|
195
|
+
return { response, html };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Replaces an element in the DOM with a new element from the response HTML.
|
|
199
|
+
*
|
|
200
|
+
* @param {Document} html - The parsed HTML document from the server response
|
|
201
|
+
* @param {string} selector - CSS selector for the element to replace
|
|
202
|
+
* @param {Element | null} [replaceRootElement] - Optional root element to search within
|
|
203
|
+
* @returns {Element | null} The new element that was inserted, or null if replacement failed
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
replaceElements(html, selector, replaceRootElement) {
|
|
207
|
+
const newElement = html.querySelector(selector);
|
|
208
|
+
const currentElement = replaceRootElement || this.querySelector(selector);
|
|
209
|
+
if (newElement && currentElement) {
|
|
210
|
+
currentElement.replaceWith(newElement);
|
|
211
|
+
return newElement;
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Binds event listeners to form inputs and the form itself.
|
|
217
|
+
*
|
|
218
|
+
* @param {HTMLInputElement} [target] - Optional specific input to bind events to
|
|
219
|
+
* @private
|
|
220
|
+
*
|
|
221
|
+
* @remarks
|
|
222
|
+
* - If target is provided, only binds blur event to that input
|
|
223
|
+
* - Otherwise, binds submit event to form and blur events to all inputs with enhance-validate attribute
|
|
224
|
+
*/
|
|
225
|
+
bindEvents(target) {
|
|
226
|
+
const blurHandler = (event) => {
|
|
227
|
+
this.handleInputValidation(event);
|
|
228
|
+
};
|
|
229
|
+
if (target) {
|
|
230
|
+
target.addEventListener("blur", blurHandler);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this._form.addEventListener("submit", async (event) => {
|
|
234
|
+
await this.handleFormValidation(event);
|
|
235
|
+
this.focusFirstInvalidInput();
|
|
236
|
+
});
|
|
237
|
+
const inputs = this._form.querySelectorAll(":is(input, textarea)[enhance-validate]");
|
|
238
|
+
for (const input of inputs) {
|
|
239
|
+
input.addEventListener("blur", blurHandler);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
connectedCallback() {
|
|
243
|
+
this.bindEvents();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Wraps a DOM update function in a view transition if supported by the browser.
|
|
248
|
+
* Falls back to immediate execution if View Transitions API is not available.
|
|
249
|
+
*
|
|
250
|
+
* @param {() => void} fn - The function to execute during the view transition
|
|
251
|
+
*/
|
|
252
|
+
function viewTransition(fn) {
|
|
253
|
+
if (!document.startViewTransition) {
|
|
254
|
+
fn();
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
document.startViewTransition(() => fn());
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const isInternalUrl = (url) => {
|
|
261
|
+
try {
|
|
262
|
+
const urlObj = new URL(url);
|
|
263
|
+
return urlObj.origin === window.location.origin;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return true; // Relative URLs are considered internal
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const addHeader = (name, value) => {
|
|
270
|
+
const headers = new Headers();
|
|
271
|
+
headers.append(name, value);
|
|
272
|
+
return headers;
|
|
273
|
+
};
|
|
274
|
+
const getHeader = (response, name) => response.headers.get(name);
|
|
275
|
+
// Register the custom element if not already defined
|
|
276
|
+
if (!customElements.get(EnhanceForm.tagName)) {
|
|
277
|
+
customElements.define(EnhanceForm.tagName, EnhanceForm);
|
|
278
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "enhance-form",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A custom element that progressively enhances HTML forms with AJAX submission, per-field validation, and targeted DOM updates.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"lint": "biome check .",
|
|
19
|
+
"lint:fix": "biome check --write .",
|
|
20
|
+
"publint": "pnpm dlx publint",
|
|
21
|
+
"prepublishOnly": "npm run lint && npm run build"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@biomejs/biome": "^2.3.14",
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
},
|
|
27
|
+
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
|
28
|
+
}
|