Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ad0613d
feat(pat-inject): Add new trigger autoload-navigate which becomes act…
thet Nov 6, 2026
93d3719
feat(core polyfills): Fire navigatesuccess in the Navigation API poly…
thet Nov 8, 2026
b7e94c7
fix(pat-inject): Do not store the UUID on the shared pattern object.
thet Nov 8, 2026
e5ab3f7
fix(pat-inject): Register the autoload-navigate handler instead of ca…
thet Nov 8, 2026
5cf29cf
feat(pat-inject): Trigger autoload-navigate on navigatesuccess for UR…
thet Nov 8, 2026
4c9af90
feat(pat-inject): Do not allow autoload-navigate together with histor…
thet Nov 8, 2026
666e161
fix(pat-inject): Clean up the autoload-navigate listener for removed …
thet Nov 8, 2026
b65d89d
feat(pat-inject): Debounce autoload-navigate and re-trigger after run…
thet Nov 8, 2026
7d61328
maint(pat-inject): Remove the autoload-navigate trigger again.
thet Nov 9, 2026
4db0d07
feat(pat-reload): Add pat-reload to reload content on events.
thet Nov 9, 2026
25a4734
feat(pat-reload): Add documentation and demo.
thet Nov 9, 2026
b8f0395
feat(pat-reload): Add pat-reload to the Patternslib bundle.
thet Nov 9, 2026
b681b82
feat(pat-inject): Native API for injection state: execute return valu…
thet Nov 9, 2026
d353334
fix(pat-inject): Do not inject if a non-id target cannot be created.
thet Nov 9, 2026
490f1e1
maint(pat-reload): Use native APIs instead of jQuery for the reload s…
thet Nov 9, 2026
c55e935
breaking(pat-inject): Return a Promise from execute, which settles wh…
thet Nov 9, 2026
385afad
maint(pat-reload): Use async/await with pat-inject's execute.
thet Nov 9, 2026
2852d1c
fix(pat-ajax): Track the latest request per element instead of per URL.
thet Nov 9, 2026
cda00c7
maint: Add an AGENTS.md file for AI agent instructions.
thet Nov 9, 2026
a18c49a
maint(adr): Add ADRs for pat-reload, navigation handling, pat-inject'…
thet Nov 9, 2026
9e8c04e
fixup: maint: Fix Agents.md problems.
thet Nov 9, 2026
4f231db
maint: Add docs/adr for documenting architectural decision records (A…
thet Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Patternslib AGENTS.md instructions

## Relevant Paths

- First search all the relevant paths, then - if needed - the individual `node_modules` directories.

- "@patternslib/patternslib" at `./` is the main Patternslib directory
- "@patternslib/dev" at `../REPOS-pat/dev/` is for base configuration files, e.g. Webpack, prettier, eslint and release-it.
- External Patterns which use this Patternslib infrastructure can be found at: `../REPOS-pat/`
- Plone's "@plone/mockup" repository - the base Plone ClassicUI JavaScript library which uses "@patternslib/patternslib" can be found at: `../../../../plone/patternslib/mockup/`

## Dev environment tips

- "@plone/mockup" uses "pnpm"
- "@patternslib/patternslib" and most others use "yarn" (We think of switching to "pnpm")
- All we still Webpack based projects (We think of switching to "rspack" or "vite")
- All use "Jest" as testing framework (We think of switching to "vitest" or "rstest")
- We make heavy use of Module Federation to share libraries between individual Patternslib addons and not to load dependencies twice.
- We use "release-it" for releasing packages on npm.

### Commit message style

- We use "conventionalchangelog" changelog generation when releasing. The format is `category(subtitle): commit message`, where category is one of `breaking`, `feat`, `fix`, `maint` or `tech`. Subtitle should define the Pattern or module we were working on, e.g. "pat-inject". The commit message title is a concise summary of what was done. A more elaborate body can be given.
Example:

```
feat(pat-inject):‌ Sanitize HTML using DOMPurify

We're now using DOMPurify to sanitize the HTML response instead of the previous custom regex based sanitization method. This improves the libraries security.
```

- If a source commit implements a decision documented in an ADR, reference that ADR in the commit message as (same rule as in AGENTS.md):

```
ORIGINAL COMMIT MESSAGE

Ref: ADR NNNN
```

- If a commit was done with significant help from someone else, append this to the commit message:

```
ORIGINAL COMMIT MESSAGE

Co-Authored-By: NAME <EMAIL>
```

- If a commit was done with significant help from an AI, append this to the commit message:

```
ORIGINAL COMMIT MESSAGE

Assisted-by: NAME <EMAIL>
```

Don't use `Co-Authored-By` for AI contributions, as for Plone and other projects we need a contributor agreement for commits and an AI cannot sign it.

- Use these rules in order as they are defined here - first ADR, then scrum, then co-authored, then assisted references.

- These rules do not apply to build-output commits as defined above. Their commit message must be `Compiled.`

## Architecture Decision Records

Record significant decisions made during agentic sessions as Architecture
Decision Records (ADRs) in `./docs/adr/`.

- Write an ADR when a session makes a non-trivial design, architecture or
styling decision, or a change whose reasoning is not obvious from the
code. Q&A, reviews and trivial fixes need none. If unsure, ask.
- Start with a draft ADR, so that it can be referenced from source commits.
- One ADR per decision, not per session. Write it when the decision is
implemented, before committing. This is not a strict rule.
- File name: `NNNN-short-kebab-title.md`, numbered consecutively.
- Sections: title, `Status: <Proposed|Accepted|Superseded by NNNN> (YYYY-MM-DD)`,
Context, Decision, Consequences. See `./docs/adr/0004-pat-inject-execute-returns-a-promise.md` as an example.
- Unpushed ADRs may be rewritten freely. Once pushed, an accepted ADR is
only changed for typos and clarifications; to revise the decision, write
a new ADR and mark the old one `Superseded by NNNN`.
- If a source commit implements a decision documented in an ADR, reference that ADR in the commit message as:

```
ORIGINAL COMMIT MESSAGE

Ref: ADR NNNN
```
9 changes: 9 additions & 0 deletions docs/adr/0000-README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Architecture Decision Records

This directory contains Architecture Decision Records from agentic sessions.

## References

- https://www.heise.de/hintergrund/Gut-dokumentiert-Architecture-Decision-Records-4664988.html
- https://martinfowler.com/bliki/ArchitectureDecisionRecord.html
- https://adr.github.io/
70 changes: 70 additions & 0 deletions docs/adr/0001-pat-reload-instead-of-pat-inject-trigger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# 0001 - Reload content on events with pat-reload instead of a pat-inject trigger

Status: Accepted (2026-11-09)

## Context

A toolbar - a plain `<div>`, not an `<a>` or `<form>` - needs to be reloaded
whenever the browser navigates to another URL without a full page load (via
`history.pushState`, e.g. in Plone's folder contents).

The first approach was a new pat-inject trigger `autoload-navigate`, as
pat-inject is already good at fetching and injecting content. While
implementing and reviewing it, it turned out that it required:

- changing pat-inject's general contract for this one trigger:
- resolving `url` relative to the current location (appending to it)
instead of like a normal link,
- allowing elements other than `<a>` and `<form>`, if they have a `url`,
- about 70 lines of navigation handling: excluding `history: record`
(which would cause an infinite loop, as recording history fires a
navigation event again), filtering URL changes, cleaning up listeners of
removed elements, debouncing and re-triggering after a running
injection.

None of that is about injection. It is about _when_ and _from which URL_ to
reload. pat-inject is already the most complex pattern in Patternslib.

Mockup's `pat-toolbar` shows the same split: it listens to an
application-specific event and builds the URL from application knowledge,
but implements fetching and replacing itself with `$.ajax` - duplicating
what pat-inject does better (source/target selection, error handling,
loading classes, injection events, scanning for new patterns).

## Decision

- Remove the `autoload-navigate` trigger from pat-inject again.
- Add a generic pattern `pat-reload`, configured via `data-pat-reload`:
- `url` (required): the URL to reload from. See ADR 0002.
- `trigger-event` (default `navigatesuccess`): the event which triggers
a reload. See ADR 0003.
- `delay` (default `0`): debounce time for successive events.
- pat-reload decides _when_ and _from which URL_ to reload. _What_ is
fetched and _where_ it goes is delegated to pat-inject via its JavaScript
API (`inject.extractConfig` and `inject.execute`) and configured with
`data-pat-inject` on the same element. The element must not have the
`pat-inject` class.
- Unlike pat-inject, which injects into `body` by default, pat-reload
injects into the element itself if no target is configured.
- Events which arrive while a reload is running lead to exactly one more
reload after it has finished, so that the latest state is shown.
- When the element was removed from the DOM, the event listener is removed.
- The names `url` and `trigger-event` were chosen over `reload-view` (Zope
jargon, and the `reload-` prefix is redundant within `data-pat-reload`)
and `event-trigger` (Patternslib already uses `trigger` and
`trigger-element`).

## Consequences

- pat-inject's contract stays unchanged.
- pat-reload can be used for anything which needs to be reloaded on a
configurable event - toolbars, breadcrumbs, sidebars, portlets.
- pat-reload depends on pat-inject's JavaScript API, which expects jQuery
objects. To interact with it without jQuery events and data, pat-inject
got a native Promise based API. See ADR 0004.
- pat-push already does "reload this element on an event", but only for
push messages and with its own `fetch` and `innerHTML`. It could be built
upon pat-reload later.
- Events triggered via jQuery's `.trigger()` - like pat-structure's
`structure-url-changed` - cannot be used as `trigger-event`, as native
event listeners don't receive them.
60 changes: 60 additions & 0 deletions docs/adr/0002-pat-reload-url-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 0002 - pat-reload URL resolution relative to the current location

Status: Accepted (2026-11-09)

## Context

pat-reload (ADR 0001) needs a URL which depends on the current location, e.g.
`<current URL>/@@render-toolbar` in Plone.

A fixed URL is wrong after a navigation. Standard relative URL resolution
does not work either: `@@render-toolbar` resolved against `/folder/page`
results in `/folder/@@render-toolbar` - the wrong context.

When the `navigate` event fires, the new URL is not yet committed. After
`navigatesuccess`, `window.location` is up to date (ADR 0003).

Hints in the DOM about the actual context - `<base href>`,
`link[rel=canonical]`, Plone's `data-base-url` on `<body>` - are not updated
on same-document navigations. After `pushState`, they still describe the
initially loaded page.

## Decision

- Relative URLs are appended to the current location: the current URL
without query string and hash, with a trailing slash ensured. So
`@@render-toolbar` on `/folder/page?a=1#x` reloads from
`/folder/page/@@render-toolbar`.
- Absolute URLs - with a scheme like `https:`, starting with `//` or with
`/` - are used as-is.
- Resolution is done with the standard `new URL(url, base)`, which already
distinguishes relative and absolute URLs. No custom check, e.g. "starts
with `http` or `/`", which would miss other schemes and `//`.
- Like in pat-inject, a `#selector` appended to the URL defines the
injection source.
- The base URL is computed in a single method, `get_base_url`, so that it
can be made configurable later.

## Consequences

- Relative URLs in pat-reload behave differently from relative URLs in
links. This is documented.
- Plone default pages are not handled yet: there, the actual context is a
content item within the path shown in the URL bar. A configurable base
URL is to be designed separately. Considerations so far:
- An option like `base-url-selector` with a CSS selector, e.g.
`[data-base-url], link[rel=canonical]`. Other standard sources are
`<base href>` (reflected in `document.baseURI`); `og:url` is for
social media and not suitable.
- `querySelector` with a selector list returns the first match in
document order, not in the order of the list. Fallbacks would have
to be tried one by one - and selectors can contain commas themselves.
- The URL is stored in different attributes (`href` vs.
`data-base-url`), so the attribute to read needs a rule.
- The DOM hints are stale after same-document navigations. For
navigation events, the new location or the event itself (e.g.
pat-structure's `path`) are the only up-to-date sources - unless the
navigating code updates the hints.
- Changing pat-inject's `url` handling the same way was rejected. It would
have changed the result for every existing relative `url` and `href`
(ADR 0001).
45 changes: 45 additions & 0 deletions docs/adr/0003-use-navigatesuccess-for-navigation-changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 0003 - React to navigations on navigatesuccess, not navigate

Status: Accepted (2026-11-09)

## Context

The Navigation API's `navigate` event fires _before_ a navigation is
committed:

- `window.location` still has the old URL.
- The navigation might still be cancelled.
- It also fires for navigations which unload the page (link clicks to other
pages, form submissions, reloads). Starting a request there is wasted.
- Passing the event on to code which calls `preventDefault()` - like
pat-inject's `onTrigger` - cancels the navigation.

Patternslib's basic Navigation API polyfill (`src/core/polyfills.js`) for
browsers without the API only fired `navigate`, and - unlike the native
implementation - only _after_ `pushState` and `replaceState` changed the URL.
Native and polyfilled timing differed.

pat-inject also calls `replaceState` without changing the URL to mark
history entries: `"pageload"` at module load and `"anchor"` on hash
navigations. These fire navigation events too.

## Decision

- Code reacting to navigations listens to `navigatesuccess`. Then the
navigation is committed and `window.location` is up to date.
- The polyfill also fires `navigatesuccess` after `pushState` and
`replaceState`. So `navigatesuccess` has the same timing natively and
polyfilled.
- Navigation-triggered reloads only happen if the URL without the hash has
changed. This ignores hash changes and same-URL `replaceState` calls.

## Consequences

- pat-reload uses `navigatesuccess` by default (ADR 0001).
- `navigatesuccess` carries no destination; the new URL is read from
`window.location`.
- Back/forward navigations after pat-inject `history: record` injections
trigger a full page reload by pat-inject's `popstate` handler anyway. A
navigation-triggered request might still be started right before that.
- The Navigation API is supported in all major browsers by now. Whether the
polyfill is still needed should be re-evaluated.
71 changes: 71 additions & 0 deletions docs/adr/0004-pat-inject-execute-returns-a-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 0004 - pat-inject's execute returns a Promise

Status: Accepted (2026-11-09)

## Context

Code which starts injections via pat-inject's JavaScript API - like
pat-reload (ADR 0001) - needs to know when an injection has finished and
whether it succeeded, e.g. to reload once more after a running injection.

The injection state was only available via jQuery:

- pat-ajax triggers `pat-ajax-success` and `pat-ajax-error` with jQuery's
`.trigger()`. Native event listeners - including
`events.add_event_listener` - don't receive jQuery-triggered custom
events.
- The "injection in progress" flag is stored as jQuery data
(`pat-inject-triggered`).

`execute` started the request and returned `undefined`. Errors were handled
by pat-inject itself (error pages, error message), with no way for the
calling code to react.

A first step returned a boolean "started" from `execute` and added a native
`pat-inject-finished` event. Listening to an event after calling a method
needs filtering of events bubbling up from nested injections and still does
not allow `async`/`await`. `events.await_event` does not filter by target and
never settles if the event never comes.

## Decision

`inject.execute(cfgs, $el)` returns a Promise which settles when the
injection has finished:

- It resolves to `true` if the content was injected.
- It resolves to `false` if the injection was not started - invalid
configuration, denied confirmation or an injection already in progress -
or if there was nothing to inject.
- It rejects with an `Error` if the request failed with a network error or
an HTTP error status. The error's `cause` is the jqXHR object.
pat-inject's own error handling is done before.
- It also waits for injections with a `process-delay`.

Additionally, a native `pat-inject-finished` event is dispatched on the
element right before the Promise settles, for code which did not call
`execute` itself.

Usage:

```javascript
try {
const injected = await inject.execute(cfgs, $el);
} catch (error) {
log.warn("Could not load.", error);
}
```

## Consequences

- This is a breaking change: code which calls `execute` and ignores the
return value now gets unhandled Promise rejections ("Uncaught (in
promise)") for failed requests. The internal callers in pat-inject,
pat-collapsible and pat-expandable-tree catch them. No callers were found
in Mockup or the add-ons in `REPOS-pat`.
- pat-reload uses `async`/`await` with `try`/`catch` and does not need any
jQuery events or data. It still needs jQuery objects to call pat-inject's
API.
- The Promise only settles if pat-ajax reports the result of the request.
pat-ajax silently dropped results of concurrent requests to the same URL
from different elements, which would leave the Promise unsettled forever.
This was fixed, see ADR 0005.
37 changes: 37 additions & 0 deletions docs/adr/0005-pat-ajax-latest-request-per-element.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 0005 - pat-ajax handles the latest request per element

Status: Accepted (2026-11-09)

## Context

Since 2016, pat-ajax only handles the result of the latest request: "if this
url is requested multiple time, only return the last result". The intention
is that, e.g. for a search form submitted multiple times in a row, an
outdated response does not overwrite a newer one.

The requests were counted per URL, globally for the whole page. When two
different elements requested the same URL at the same time, the first
element never received a `pat-ajax-success` event:

- pat-inject's "injection in progress" state of that element was never
reset, so it could never inject again.
- The Promise returned by `inject.execute` (ADR 0004) never settled, so
e.g. pat-reload (ADR 0001) would stop reloading.

## Decision

- Requests are counted per element, using a `WeakMap`.
- The latest request of an element wins - regardless of its URL. Counting
per element _and_ URL was rejected, as then an outdated response of the
same element requesting different URLs (e.g. different `formaction`
buttons) could still overwrite a newer one.

## Consequences

- Different elements requesting the same URL at the same time each get
their response.
- The original behavior for repeated requests of the same element is kept.
- Errors are still not filtered: a failed outdated request still triggers
`pat-ajax-error` while a newer request is running. Through pat-inject
this cannot happen, as an element cannot have two injections running at
the same time. It only affects bare `.pat-ajax` elements.
6 changes: 6 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ <h4>Behavioural</h4>
title="Push"
>Push<sup>β</sup></a
>
<a
class="navigation ajax"
href="/src/pat/reload/index.html"
title="Reload"
>Reload<sup>β</sup></a
>
</div>

<div class="navigation">
Expand Down
Loading
Loading