Skip to content

Commit e15bd06

Browse files
committed
Merge remote-tracking branch 'origin/hk/finish-pr-807'
# Conflicts: # docs/contributing/documentation.md
2 parents dc9ee29 + 92f91cd commit e15bd06

52 files changed

Lines changed: 2923 additions & 172 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ npm-debug.log
1717
/tmp
1818
/node_modules
1919
.$*.xml.*
20+
21+
# Implementation plans are local working notes (see docs/contributing/code-organization.md)
22+
/docs/plans/*
23+
!/docs/plans/.gitkeep

‎CHANGELOG.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@ If you're upgrading from an older Unpoly version, you should load [`unpoly-migra
88
You may browse a formatted and hyperlinked version of this file at <https://unpoly.com/changes>.
99

1010

11+
Unreleased
12+
----------
13+
14+
### Custom form fields
15+
16+
Unpoly now builds a form's request params with the browser's own [form-data algorithm](https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData), instead of walking the form's fields itself.
17+
18+
- [Form-associated custom elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#form-associated_custom_elements) are now submitted, without any configuration. (by @kmmbvnr)
19+
- Values appended by a [`formdata`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/formdata_event) event listener are now submitted. (by @kmmbvnr)
20+
- A form-associated custom element is now also [watched](/up.watch), [validated](/validation), [switched](/switching-form-state) and [disabled](/disabling-forms), if it [exposes its state to a script](/custom-form-fields#contract).
21+
- A new guide explains the [patterns for building a custom form field](/custom-form-fields).
22+
- A `formdata` listener affects what Unpoly *sends*, not what it *watches*. See [`formdata` listeners](/custom-form-fields#formdata).
23+
- A form-associated custom element that never calls `setFormValue()` submits no value, even when it is listed in `up.form.config.fieldSelectors`.
24+
- ⚠️ A form containing an `<input type="file">` is now submitted as `multipart/form-data` even when no file is selected, because the browser's form-data algorithm produces an entry for every file input.
25+
- ⚠️ An `<input type="image">` used as the submit button now contributes its `name.x` and `name.y` coordinates rather than its `[value]`, matching a native form submission.
26+
- ⚠️ A `<button type="reset">` or `<input type="reset">` with a `[name]` is no longer treated as a form field, so it contributes no param to submissions, validations or watched values — matching a native form submission. It is still disabled by [`[up-disable]`](/up-submit#up-disable).
27+
- When an overlay is closed by submitting a form with `[up-accept]` or `[up-dismiss]`, the [result value](/closing-overlays#result-values) now also contains the `[name]` and `[value]` of the submit button the user pressed. Formerly it contained only the form's fields.
28+
- ⚠️ `up.Params.fromForm()` now requires a `<form>` element. It used to accept any container, which `up.Params.fromContainer()` does.
29+
- ⚠️ Params now arrive in the order the browser produces them: the form's own fields in tree order, then values appended by a `formdata` listener, then controls that only Unpoly knows about (custom elements listed in `up.form.config.fieldSelectors`). In particular a submit button's `[name]` and `[value]` now appear at the button's position in the form, rather than after the form's `[up-params]`.
30+
- `up.Params.fromForm()` now takes a `{ submitButton }` option, and assumes the form's first submit button when it is omitted. Pass `false` to submit no button at all. A button that the browser associates with a different form is ignored rather than crashing.
31+
- `up.Params.fromForm()` ignores an `{ includeDisabled }` option. The browser's algorithm never includes a disabled control. The option still works with `up.Params.fromContainer()`, which is what form watching uses.
32+
- ⚠️ Unpoly now throws an error when a field uses a `[form]` attribute to associate with a form in another [layer](/up.layer). The browser resolves such an attribute by document order and ignores layers, so it would submit one layer's field with another layer's form. If you render the same page into an overlay and it uses `[form]`, either give the form a unique `[id]` per layer, or move the field inside the form. Two forms sharing an `[id]` within a single layer are unaffected.
33+
- ⚠️ A field inside a `<fieldset disabled>` is no longer submitted or validated, matching a native form submission. It is still reported by [watchers](/up.watch) and [`[up-switch]`](/switching-form-state).
34+
- A custom control that exposes a `{ name }` but no readable `{ value }` no longer contributes an `undefined` param when watched.
35+
- [Disabling a form](/disabling-forms) now also disables a form-associated custom element.
36+
- The requirements for a [custom form field](/custom-form-fields#contract) are relaxed. A `value` getter is the only thing a custom control must provide.
37+
- ⚠️ When Unpoly disables a custom control that exposes no `{ disabled }` property, it now sets the control's `[disabled]` attribute instead of assigning the property.
38+
- ⚠️ A validation request no longer sends the params of the form's first submit button, and no longer honors that button's `[formaction]`, `[formmethod]`, `[up-params]` or `[up-headers]`. Nothing pressed the button, so only the `<form>` element decides where a validation goes.
39+
- ⚠️ A `click` event dispatched by a script is no longer passed to [`up.on()`](/up.on) callbacks when the element carries a `[disabled]` attribute, even if the platform cannot disable that kind of element. Formerly only a `{ disabled }` property had this effect.
40+
- ⚠️ When Unpoly [focuses](/focus) a form-associated custom element, it now assigns `.up-focus-visible` instead of `.up-focus-hidden`, even when the user interacted with a mouse or touch. `up.viewport.config.autoFocusVisible` shows a [focus ring](/focus-visibility) for every [field](/up.form.config#config.fieldSelectors), and such an element is now a field.
41+
- `up.form.config.genericButtonSelectors` was renamed to `up.form.config.anyButtonSelectors` and now matches every kind of button, including submit and reset buttons. Unpoly uses it to disable a form's buttons while the form is submitting. The old name still works with [`unpoly-migrate.js`](/changes/upgrading).
42+
43+
1144
3.14.3
1245
------
1346

‎CONTRIBUTING.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ feature, or before reviewing someone else's.
5353

5454
> Where files live, why Unpoly is a `window.up` global, which module owns which topic,
5555
> how we structure modules internally, and how to find tests for a given file.
56+
> Implementation plans for larger changes go in `docs/plans/`, named after their subject.
5657
5758
[Read the code organization guide](docs/contributing/code-organization.md) before
5859
adding or moving code.
@@ -63,7 +64,8 @@ adding or moving code.
6364
> Any change must be tested. Tests ("specs") are written in Jasmine.
6465
> They always run in a real browser, but can be driven from the terminal
6566
> using `bin/test`. The network is always mocked. We have patterns to wait for
66-
> async code. Filtering to the module you're editing turns a multi-minute suite into a few seconds.
67+
> async code. The full suite takes ~9 minutes, so find the relevant groups with `bin/find-spec`,
68+
> run only those, and leave the minified, ES6, CSP and migrate variants to CI.
6769
6870
[Read the full testing guide](docs/contributing/testing.md) before writing or changing a spec.
6971
Many old specs use deprecated patterns, so don't copy a neighbour.

‎bin/build‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// bin/build production bundles, gzipped
44
// bin/build --config=development dev bundles (what bin/dev's watcher builds)
55
// bin/build --config=ci minified bundles for CI
6+
// bin/build --config=minified only the minified bundles
67
// bin/build --watch rebuild on change (never gzips)
78
// bin/build --gzip / --no-gzip force gzip on/off (default: on for production)
89
import { runBuild } from '../tooling/build/build.mjs'

‎bin/test‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
#!/usr/bin/env node
22
// Dev spec runner. Starts the dev environment in the background if none is running.
3+
// Everything it prints is also written to tmp/test.log, so a truncated terminal never
4+
// costs you the failure block.
35
import { runDev } from '../tooling/runner/terminal/run.mjs'
4-
process.exit(await runDev(process.argv.slice(2), process.env))
6+
import { teeToLogFile, LOG_FILE } from '../tooling/runner/terminal/tee.mjs'
7+
8+
const log = teeToLogFile()
9+
const code = await runDev(process.argv.slice(2), process.env)
10+
// Point at the log only when specs actually ran and something failed (1 = failures,
11+
// 2 = the watchdog). A preflight refusal has already printed its whole message.
12+
if (code === 1 || code === 2) console.error(`Full output: ${LOG_FILE}`)
13+
await log.close()
14+
process.exit(code)

‎bin/update-contributing-tocs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env node
2+
// Regenerates the table of contents in every contributing guide that carries the
3+
// <!-- toc --> markers.
4+
// bin/update-contributing-tocs rewrite the TOCs
5+
// bin/update-contributing-tocs --check report stale TOCs, change nothing, exit non-zero
6+
import path from 'node:path'
7+
import { updateGuides, guidesMissingToc, GUIDE_DIR } from '../tooling/toc.mjs'
8+
9+
const check = process.argv.includes('--check')
10+
const relative = (file) => path.relative(process.cwd(), file)
11+
12+
const stale = updateGuides(undefined, { write: !check })
13+
const missing = guidesMissingToc()
14+
15+
for (const file of stale) console.log(`${check ? 'stale' : 'updated'}: ${relative(file)}`)
16+
for (const file of missing) console.log(`no TOC, but long enough to want one: ${relative(file)}`)
17+
18+
if (!stale.length && !missing.length) console.log(`All TOCs in ${relative(GUIDE_DIR)} are current.`)
19+
if (check && (stale.length || missing.length)) {
20+
console.error('Run `bin/update-contributing-tocs` to regenerate.')
21+
process.exit(1)
22+
}

‎docs/contributing/code-organization.md‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
# Code organization
22

3-
## Directory structure / Where to find files
3+
Where code lives in this repository and why: the directory layout, the `window.up` global,
4+
which module owns which topic, how a module is structured internally, and where to find the
5+
specs for a given file.
6+
<!-- toc -->
7+
- [Directory structure](#directory-structure)
8+
- [Where to find a module's specs](#where-to-find-a-modules-specs)
9+
- [Implementation plans](#implementation-plans)
10+
- [Architecture](#architecture)
11+
- [Not an ESM module](#not-an-esm-module)
12+
- [Responsibilities](#responsibilities)
13+
- [Main module pattern](#main-module-pattern)
14+
- [Entry points and optional bundles](#entry-points-and-optional-bundles)
15+
<!-- /toc -->
16+
17+
18+
## Directory structure
419

520
You will mostly make changes in `src/unpoly` and test them in `spec/unpoly`. How the specs
621
themselves are structured is covered in [Testing](testing.md#how-specs-are-organized).
@@ -60,6 +75,10 @@ tooling/ # Our own dev machinery, never shipped (rarely need to
6075
find_spec.mjs # ... Spec-title search behind bin/find-spec
6176
test/ # ... Unit tests for all of the above (bin/self-test)
6277
78+
docs/ # Documentation for contributors, never shipped
79+
contributing/ # ... The guides linked from CONTRIBUTING.md
80+
plans/ # ... Implementation plans (gitignored)
81+
6382
dist/ # Output directory for the Webpack bundler (gitignored, never edit directly)
6483
unpoly.js # ... Main Unpoly bundle (JS)
6584
unpoly.css # ... Main Unpoly bundle (CSS)
@@ -68,13 +87,36 @@ dist/ # Output directory for the Webpack bundler (gitignored
6887
...
6988
```
7089

90+
### Where to find a module's specs
91+
7192
A module's specs are not always in a single file: a group that grew too large lives in a
7293
sibling file sharing the module's prefix, like `form_switch_spec.js` above. See
7394
[How specs are organized](testing.md#how-specs-are-organized) for the naming, how to
7495
[find the spec for a feature](testing.md#finding-the-spec-for-a-feature), and
7596
[where a new spec goes](testing.md#where-a-new-spec-goes).
7697

7798

99+
### Implementation plans
100+
101+
A change worth more than one commit is worth a written plan first: what was decided,
102+
what was ruled out and why, and the sequence of commits it will take. Plans go in
103+
`docs/plans/`, which is gitignored except for its `.gitkeep`.
104+
105+
A plan is a working note, not a deliverable. Keeping it out of the history means it
106+
never reaches a reviewer, while still surviving the branch switches and rebases that a
107+
long-lived branch goes through.
108+
109+
Name the file after the change it plans — the request, the feature or the pull request —
110+
so that a directory listing reads as an index:
111+
112+
```
113+
docs/plans/
114+
pr-807-form-associated-custom-elements.md
115+
up-switch-array-fields.md
116+
optimistic-rendering.md
117+
```
118+
119+
78120
## Architecture
79121

80122
### Not an ESM module

‎docs/contributing/code-style.md‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Code style
22

3-
Some of these rules are enforced by ESLint, most are not.
3+
How Unpoly's own source is written: no types, no dependencies, no jQuery, and a real concern
4+
for byte count. Some of these rules are enforced by ESLint (`bin/lint`), most are not.
45

56

67
## No types
@@ -25,7 +26,26 @@ while writing code:
2526
functions, and threading the whole object costs fewer bytes than rebuilding it at
2627
each level.
2728

28-
We sometimes accept a suboptimal pattern because it saves bytes.
29+
We sometimes accept a suboptimal pattern because it saves bytes. Only bytes a minifier
30+
can't recover, though — see [Leave it to the minifier](#leave-it-to-the-minifier).
31+
32+
33+
## Leave it to the minifier
34+
35+
We ship a minified build, and apps bundle Unpoly through their own minifier. Terser
36+
already collapses branches into conditional expressions and rewrites control flow to
37+
return early. Hand-writing those transformations saves nothing, and the version we have
38+
to read and maintain for years is the unminified one.
39+
40+
- **Don't chain ternaries.** A single ternary for a genuinely binary choice is fine. A
41+
chain of them standing in for `if` / `else if` / `else` is not: it reads as a puzzle,
42+
and the minifier produces the same output from the readable form.
43+
- **Early returns are for guards.** Return early to reject a missing argument, or an
44+
element we don't handle, before the real work starts. Don't restructure a function
45+
body into early returns just to avoid an `else`.
46+
47+
The same reasoning applies to any other trick whose only benefit is fewer characters.
48+
Shorten code by removing work or reusing a helper, not by compressing its syntax.
2949

3050

3151
## No dependencies

‎docs/contributing/commit-conventions.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Commit conventions
22

3+
How we write commits and what happens to them: the module prefix every subject carries, what
4+
CI runs when you open a pull request, and what an agent must ask before committing.
5+
36
## Message prefix
47

58
Every commit subject begins with the [main module](code-organization.md#responsibilities)

‎docs/contributing/compatibility.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Compatibility
22

3+
Which browsers and language features Unpoly can rely on, and what we do instead of breaking a
4+
public API: a polyfill in `unpoly-migrate.js` rather than a major version bump.
5+
36
## Browser support
47

58
We support browser versions from the last two years, in Chrome, Firefox and Safari.

0 commit comments

Comments
 (0)