UNPKG

unpoly

Version:

Progressive enhancement for HTML

964 lines (606 loc) 248 kB
Changelog ========= Changes to this project will be documented in this file. If you're upgrading from an older Unpoly version you should load [`unpoly-migrate.js`](https://unpoly.com/changes/upgrading) to polyfill deprecated APIs. Changes handled by `unpoly-migrate.js` are not considered breaking changes. You may browse a formatted and hyperlinked version of this file at <https://unpoly.com/changes>. 3.10.2 ------ - Fix a bug where Unpoly would sometimes reset the cursor position of inputs outside the rendering fragment - When a popup is dismissed by clicking on a focusable element in the background, focus that element instead of the popup opener (issue #706). - Fix an inconsistency where `up.layer.accept()` and `up.layer.dismiss()` would return a fulfilled promise, despite both being sync functions. 3.10.1 ------ This release fixes an error in the minified Javascript (`unpoly.min.js`) (issue #703). 3.10.0 ------ Unpoly 3.10 is a major feature relase, adding support for [client-side templates](/templates), [arbitrary loading state](/loading-state) and [optimistic rendering](/optimistic-rendering). It also contains many bug fixes and quality-of-life improvements, like [Relaxed JSON](/relaxed-json). This release contains some minor breaking changes, which are marked with the ❌ emoji in this CHANGELOG. All breaking changes are polyfilled by [`unpoly-migrate.js`](https://unpoly.com/changes/upgrading). ### Arbitrary loading state with previews [Previews](/previews) are temporary page changes while waiting for a network request. They signal that the app is working, or provide clues for how the page will ultimately look. Because previews immediately appear after a user interaction, their use increases the perceived responsiveness of your application. When the request ends for [any reason](/previews#ending), all preview changes will be reverted before the server response is processed. This ensures a consistent screen state in cases when a request is aborted, or when we end up updating a different fragment. You can use previews to implement arbitrary [loading state](/loading-state). Two common applications of previews are [placeholders](/placeholders) and [optimistic rendering](/optimistic-rendering). ### Placeholders [Placeholders](/placeholders) are temporary spinners or UI skeletons shown while a fragment is loading: <video src="images/placeholders.webm" controls width="600" aria-label="UI skeletons are shown while screens are loading"></video> To show a placeholder while a link is loading, set an `[up-placeholder]` attribute with the placeholder's HTML as its value: ```html <a href="/path" up-follow up-placeholder="<p>Loading…</p>">Show story</a> <!-- mark-phrase "up-placeholder" --> ``` When the link is clicked, the targeted fragment's content is replaced by the placeholder markup temporarily. When the request ends for [any reason](/previews#ending), the placeholder is removed and the original page state restored. Instead of passing the placeholder HTML directly, you can also refer to any [template](/templates) by its CSS selector: ```html <a href="/path" up-follow up-placeholder="#loading-template">Show story</a> <!-- mark-phrase "#loading-message" --> <template id="loading-template"> <p> Loading… </p> </template> ``` ### Optimistic rendering Unpoly 3.10 supports [optimistic rendering](/optimistic-rendering) as an application of previews and templates. This is a pattern where we update the page without waiting for the server. When the server eventually does respond, the optimistic change is reverted and replaced by the server-confirmed content. For example, this is the [*Tasks* tab](https://demo.unpoly.com/tasks) in the official [demo app](https://demo.unpoly.com) running with 1000 ms latency. Note how the UI updates instantly, without waiting for the server: <video src="images/optimistic-rendering-demo.mp4" controls width="600" aria-label="The demo app reacting instantly under high latency"></video> Since optimistic rendering requires additional code, we recommend to use it for interactions where the duplication is low, or where the extra effort adds ignificant value for the user. Some suitable use cases include: - Forms with few or simple validations (e.g. adding a todo) - Forms where users would expect an immediate effect (e.g. submitting a chat message) - Re-ordering items with drag'n'drop (because most logic is already on the client) - High-value screens where every conversion matters To limit the duplication of view logic, you may [use templates](#client-side-templating). By embedding templates into your responses, the server stays in control of HTML rendering. ### Client-side templating While Unpoly apps render on the server primarily, having client-side templates can be useful for [placeholders](/placeholders), small [overlays](/opening-overlays), or [optimistic rendering](/optimistic-rendering). Unpoly 3.10 allows your server to embed templates into your responses. Your frontend can then clone new fragments from these templates, without making another server request. To refer to a template, pass its CSS selector to any attribute or option that accepts HTML: ```html <a href="#" up-target=".target" up-document="#my-template">Click me</a> <!-- mark-phrase "#my-template" --> <div class="target"> Old content </div> <template id="my-template"> <!-- mark-phrase "my-template" --> <div class="target"> New content </div> </template> ``` #### Template variables Sometimes we want to clone a template, but with variations. For example, we may want to change a piece of text, or vary the size of a component. Unpoly 3.10 offers many methods to implement [dynamic templates with variables](/templates#dynamic). You can even [integrate template engines](/templates#template-engine) like [Mustache.js](https://github.com/janl/mustache.js) or [Handlebars](https://handlebarsjs.com/): ```html <script id="results-template" type="text/mustache"> <!-- mark-phrase "text/mustache" --> <div id="game-results"> <h1>Results of game {{gameCount}}</h1> {{#players}} <p>{{name}} has scored {{score}} points.</p> {{/players}} </div>> </script> ``` ### Relaxed JSON Unpoly now supports [relaxed JSON](/relaxed-json) in all attributes and options where it also accepts JSON. Relaxed JSON is a JSON dialect that that aims to be easier to write by humans. It supports syntactic sugar that you enjoy with JavaScript [object literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer): - Unquoted property names - Single-quoted strings - Trailing commas For example, you can now write `[up-data]` like this: ```html <span class="user" up-data="{ name: 'Bob', age: 18 }">Bob</span> ``` When Unpoly outputs HTML (e.g. for the `X-Up-Context` header) it always produces regular JSON. ### Progress bar Improvements have been made to the [global progress bar](/progress-bar), which appears when requests are tooking long to load: - The progress bar will no longer restart its animation when a request is immediately followed by another request. For example, when a user changes an `[up-autosubmit]` form that is already waiting for a request, a second request is sent after the first request has loaded. The progress bar will now show one uninterrupted animation until all requests have loaded. - Old versions have used a "bad response time" setting to define the delay until the [progress bar](/progress-bar) is shown. This setting has been renamed to "late delay" everywhere: | Old name | New name | |----------|----------| | ❌ `[up-bad-response-time]` | ✅ `[up-late-delay]` | | ❌ `{ badResponseTime }` | ✅ `{ lateDelay }` | | ❌ `up.network.config.badResponseTime` | ✅ `up.network.config.lateDelay` | | ❌ `up.Request.prototype.badResponseTime` | ✅ `up.Request.prototype.lateDelay` | - Foreground requests can now opt out of the progress bar (and `up:network:late` events) by setting `[up-late-delay="false"]` or `{ lateDelay: false }`. ### Disabling form fields Unpoly 3.0 has added the `[up-disable]` attribute to [disable forms while working](/disabling-forms). This release adds the following features: - When fields are [disabled](/disabling-forms) during a render pass, and a field loses its focus, that field is now re-focused when the field is re-renabled afterwards. - You can now [disable form fields when the user activates a hyperlink](/disabling-forms#from-link). - Disabling now also disable non-submit buttons, like an `button[type=button]` or `input[type=button]`. - New configuration `up.form.config.genericButtonSelectors`. - The `{ disable }` option now also accepts an `Element` (or an array of elements) to disable. - Fix a bug where rendering with `up.render({ disable })` would crash unless an `{ origin }` was also passed. ### Tokens are separated by comma When a string contains multiple tokens, Unpoly used to separate those tokens with a space character. While this is still possible, the new canonical way is to separate tokens with a comma: | Old form (still valid) | New form | |------------|-----------| | ✅ `[up-layer="parent root"]` | ✅ `[up-parent="parent, root"]` | | ✅ `[up-show-for="value1 value2"]` | ✅ `[up-show-for="value1, value2"]` | | ✅ `[up-alias="/foo /bar"]` | ✅ `[up-alias="/foo, /bar"]` | | ✅`up.on('event1 event2', fn)` | ✅ `up.on('event1, event2', fn)` | In some cases tokens used to be separated by an `or` operator. This is no longer supported. Use a comma instead: | Old form (now invalid) | New form | |------------|-----------| | ❌ `[up-scroll="target or main"]` | ✅ `[up-scroll="target, main"]` | | ❌ `[up-focus="target or main"]` | ✅ `[up-scroll="target, main"]` | ### Feedback classes Unpoly assigns the `.up-active` class to clicked links and submit buttons, and `.up-loading` to targeted fragments that are loading new content. These [feedback classes](/feedback-classes) have been reworked to make it easier to select working elements from CSS and JavaScript: - `.up-active` and `.up-loading` are now always enabled by default (even when not [navigating](/navigation)). They can still disabled explicitly with an `[up-feedback=false]` attribute or a `{ feedback: false }` option. - When submitting a form, the `<form>` element now also receives the `.up-active` class (in addition to the submit button). - When submitting a form from a focused field, the default submit button now also receives the `.up-active` class (in addition to the field and the `<form>`). - Added a configuration `up.status.config.activeClasses`. This allows to set custom CSS classes for working links and forms. - Added a configuration `up.status.config.loadingClasses`. This allows to set custom CSS classes for targeted fragments that are loading new content. ### Better selector parsing Unpoly 3.10 is smarter when parsing values with structured grammar, and no longer relies on magic strings to split complex expressions. For example, Unpoly can now process more complex target selectors. Selectors like `.foo:has(.bar, .baz)` or `.foo[attr="one, two"]` used to cause quirky behavior, but are now parsed correctly. ### Native `:has()` selector For almost 10 years Unpoly has polyfilled the [`:has()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:has) pseudo-selector for all browsers. Since then the selector has been standardized and has received [great browser support](https://caniuse.com/css-has). Starting with this release, Unpoly will no longer include a polyfill and use the browser's native `:has()` support. Unpoly will no longer boot on old browsers that don't support `:has()` natively. ### Watching fields for changes Unpoly has several methods to detect and process changes in form fields, most notably `[up-watch]`, `[up-autosubmit]` and `[up-validate]`. This release includes the following changes: - The `[up-watch]` callback can now use an `options` argument. It contains an object of all [watch options](/watch-options) parsed from that field, e.g. `{ disable, preview, placeholder }`. - The watch options `{ event, delay }` will no longer be passed to callbacks of `up.watch()` and `[up-watch]`, as these options have already been processed by Unpoly. - ❌ `up.watch()` and `[up-watch]` will no longer process an `[up-watch-disable]` attribute. Instead the attribute is only parsed and passed to the callback as a `{ disable }` option. It is up to the callback to forward the option it to any rendering function that supports `{ disable }`. - Fix a bug where `up.autosubmit()` options did not override options parsed from `[up-watch-...]` prefixed attributes. It is convention in Unpoly that JavaScript options [always take precedence](/attributes-and-options#options) over HTML attributes. ### Target derivation [Target derivation](/target-derivation#derivation-patterns) is the process of finding a discriminating CSS selector for an element. This release includes the following changes: - `up.fragment.toTarget()` now supports a `{ strong: true }` option. This produces a more unique selector by only considering the element's `[id]` and `[up-id]` attributes. Weaker [derivation patterns](/target-derivation#derivation-patterns), like the element's class, are not considered in strong mode. The element's tag name is only considered for singleton elements like `<html>` or `<body>`. - `up.fragment.toTarget()` can now skip [target verifcation](/target-derivation#verification) by passing a `{ verify: false }` option. - When a [validated](/validation#validating-after-changing-a-field) field wants to update its form group, that form group is no longer targeted by its `[class]`, which would often be ambigous. Instead the form group is only targeted by its `[id]` or `[up-id]` attribute. If the form group doesn't have an `[id]` or `[up-id]` attribute, it is targeted with a `.has()` selector referencing the changed field, e.g. `fieldset:has(#changed-field)`. ### [Fragment API](/up.fragment) - You can now [update element's inner HTML](/targeting-fragments#content) using the `.element:content` pseudo-selector. This swaps all children of `.element`, while preserving the element itself. This feature was previously documented, but didn't work yet. - New configuration `up.fragment.config.renderOptions`. This is an object of default render options to always apply, even when not [navigating](/navigation). - When calling `up.fragment.get()` with multiple search layers (e.g. `{ layer: "current, parent"}`), Unpoly will now search those layers in the given order. - Calling `up.fragment.get()` with an `Element`, that element is returned without further lookups. - New `[up-use-data]` attribute allows to [override data](/data#overriding) for the targeted fragment. The corresponding render options is `{ data }`. - The render option `{ useHungry }` has been renamed to `{ hungry }`, but `{ useHungry }` is still accepted as an alias. The corresponding HTML attribute remains `[up-use-hungry]` as to not conflict with a link's or form's own `[up-hungry]` modifier. - The render option `{ useKeep }` has been renamed to `{ keep }`, but `{ useKeep }` is still accepted as an alias. The corresponding HTML attribute remains `[up-use-keep]` as to not conflict with a link's or form's own `[up-keep]` modifier. ### [Layers](/up.layer) - Opening a new overlay with only a `{ mode }` option has been deprecated. Always pass a `{ layer: 'new' }` option in addition to `{ mode }`. - The layer option `{ dismissAriaLabel }` has been renamed to `{ dismissARIALabel }` - When opening a new layer with `[up-use-data]` or `{ data }`, that data is now applied to the topmost swappable element (instead of to the overlay container). For example, `up.layer.open({ target: '#target', url: '/path', data: { ... }})` will apply the data object to the `#target` element. - When [opening an overlay](/opening-overlays), the property `Request#layer` is now set to `'new'` (instead of to the parent layer). Also the `#fragments` property is now set to `[]` (instead of to the parent layer's main element) ### Working with node lists Many Unpoly functions have traditionally expected content with a single DOM element at its root. Unpoly 3.10 makes it easier to render lists of mixed `Text` and `Element` nodes: - `[up-content]` now also accepts multiple elements, or a mix of `Text` and `Element` siblings. - `{ content }` now accepts any `List<Node>`, e.g. the `NodeList` returned by `querySelectorAll()`. - New experimental function `up.element.createNodesFromHTML()`. This parses a [list](/List) of [nodes](https://developer.mozilla.org/en-US/docs/Web/API/Node) from a string of HTML. Unlike `up.element.createFromHTML()`, this new function does not require a single root element in the HTML. It can parse `Text` nodes, or a mixed list of `Text` and `Element` siblings. ### Bootstrap plugin - Clicked links and submit buttons now receive the `.active` class. ### `up.feedback` is now `up.status` The `up.feedback` package has been renamed to `up.status`. This package exposed no public JavaScript functions, but does have some configuration settings you need to rename: | Old name | New name | |------------|-----------| | ❌ `up.feedback.config.currentClasses` | ✅ `up.status.config.currentClasses` | | ❌ `up.feedback.config.navSelectors` | ✅ `up.status.config.navSelectors` | | ❌ `up.feedback.config.noNavSelectors` | ✅ `up.status.config.noNavSelectors` | ### Other changes - You can now [embed CSP nonces](/csp) into the attribute callbacks `[up-on-keep]`, `[up-on-hungry]` and `[up-on-opened]`. - New property `up.Request#ended` indicates whether this request is no longer waiting for the network for any reason. It is `true` when the server has responded or when the request [failed](/failed-responses) or was [aborted](/aborting-requests). - The attribute `[up-flashes]` is now stable (discussion #679) - Clicking a link with a page-local `#hash` in the `[href]` will now honor [fixed layout obstructions](/up-fixed-top) if the browser location is already on that `#hash`. - Fix a bug where a link with `[up-confirm]` would show the confirmation dialog before [preloading](/preloading). - Fix a crash with `up.submit({ submitButton: false })`. - Fix a bug where rendering with `{ focus: 'keep' }` would sometimes re-focus elements that never lost focus. - Fix a bug where opening an overlay would stop infinite scrolling (discussion #694). - Fix a bug where Unpoly would create duplicate cache entries when the server redirects to a fully qualified URL (with protocol and hostname). - Fix a bug where when a link with `[up-preload]` renders expired content from the cache, unhovering the link would abort the revalidation request. 3.9.5 ----- This is another maintenance release that addresses some bugs while we're working on the next major feature update. ### Changes - Fix a bug where targeted fragments would show the default [focus outline](/focus-visibility) on Safari. Regression introduced in 3.9.4. - Fix a bug where an overlay would show double scrollbars if `<html>` was chosen as the overflow element. Regression introduced in 3.8.0. - Prevent Unpoly from following or preloading links with the `tel:` scheme when Unpoly is configured to [handle all links](/handling-everything) (by @begerdom). 3.9.4 ----- - Fix `.up-focus-hidden` style from causing flickering outlines when there is a `transition` on `outline-color` (by @foobear). 3.9.3 ----- This is a maintenance release that addresses some bugs while we're working on the next major feature update. ### Changes - Fix an error being thrown when a caching request is tracking an existing request to the same URL, and that existing request responds with an [error status](/failed-responses) (issue #676). - Fix a bug where a modal overlay could not be closed when a child popup would be open below the screen fold. - Focus is no longer trapped in popup overlays. Focus remains trapped in all other overlay modes, but this can be disabled by setting `up.layer.config.overlay.trapFocus = false`. - The dismiss button in overlays now has a hand cursor (by @apollo13). - Fix a bug where links with relative URLs were sometimes [revalidated](/caching#revalidation) against the wrong base URL (issue #669). 3.9.2 ----- - Fix a bug where `up:fragment:loaded` listeners could not open a new layer by setting `event.renderOptions.layer = "new"`. 3.9.1 ----- - Fix a bug where any `form[up-target]` would receive a `[role=button]` attribute (issue #668). 3.9.0 ----- This release brings many fixes and quality-of-life improvements that were requested by the [community](/community). The vast majority of these changes are backward compatible. One breaking change can be found with [making links followable](#making-links-followable). Existing usage is polyfilled by [`unpoly-migrate.js`](/changes/upgrading). ### Emitting events on buttons - You can now use `[up-emit]` to emit an event when any element is clicked. In particular this works with a `<button>` or any [faux-interactive element](/faux-interactive-elements) (issue #416). ### Improvements to faux-interactive elements Sometimes you need to add a `click` listener to non-interactive elements (like `<span>`). Unpoly helps you [prevent accessibility issues](/faux-interactive-elements#accessibility) with such "faux-interactive" elements, by offering the `[up-clickable]` attribute and `up.link.config.clickableSelectors` configuration. Unpoly also leverages this for its own faux-interactive elements, such as `[up-emit]` or `[up-dismiss]`. This release improves the handling of faux-interactive elements: - A new documentation guide [Clicking non-interactive elements](/faux-interactive-elements) details all the methods to emulate interactivity on non-interactive elements like `<span>` or `<div>`. - You can now define exceptions to `up.link.config.clickableSelectors`, by setting an `[up-clickable=false]` attribute or configuring `up.link.config.noClickableSelectors`. - Adjusted the handling of keyboard input to better match the behavior of real buttons and links. In particular, faux-interactive elements with a [button role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role) (default) can be activated with both `Space` and `Enter` keys. Faux-interactive elements with a `[role=link]` can only be activated with the `Enter` key. - Faux-interactive elements that also have the `[up-follow]` attribute now default to `[role=link]` (instead of the default `[role=button]`). - Faux-interactive elements with a button role no longer have the "hand" (or "pointer") cursor. - Fix a bug where faux-interactive elements inside popups could not be activated with the keyboard (#653). ### Making links followable - Links with only an `[up-href]` attribute are no longer followable by default. They also require an `[up-follow]` attribute or a match in `up.link.config.followSelectors`. This change was made to remove confusion with other features that use `[up-href]`, such as `[up-defer]` and (since this release) `[up-poll]`. - Links with only an `[up-instant]` attribute are no longer followable by default. They also require an `[up-follow]` attribute or a match in `up.link.config.followSelectors`. This change was made to remove confusion with other features that use `[up-instant]`, in particular `up:click` on [faux-interactive elements](/faux-interactive-elements). ### Polling - Listeners to the `up:fragment:poll` event can now inspect or mutate `event.renderOptions`. This allows more control over the polling request and sub-sequent render passes. - `[up-poll]` elements can now use the `[up-href]` attribute to poll from a different URL. By default Unpoly will poll the URL from which the element was originally loaded. The old method over overriding `[up-source]` is still supported, but `[up-href]` is the preferred way of doing this going forward. - `[up-poll]` elements can now use the `[up-method]` attribute to choose a different HTTP method for polling requests. - `[up-poll]` elements can now use the `[up-params]` attribute to add custom params to polling requests. - `[up-poll]` elements can now use the `[up-headers]` attribute to add custom headers to polling requests. ### Forms - Focus is now preserved when submitting a form by pressing `Enter` from a focused field ([discussion #658](https://github.com/unpoly/unpoly/discussions/658)). - The `up.submit()` now includes the `[name]` and `[value]` of the default submit button in the submitted params. By default the form's first submit button will be assumed. You can prevent this with `{ submitButton: false }`, or pass a different button element as `{ submitButton }`. - Fix an interop issue with the [Shoelace](https://shoelace.style/) web component library, where a failed response could not be processed when the form was submitted with an `<sl-button>` ([discussion #643](https://github.com/unpoly/unpoly/discussions/643)). ### Smooth scrolling - Support [smooth scrolling](/scroll-tuning#animating-the-scroll-motion) when swapping a fragment. - Fix smooth scrolling when [prepending or appending](/targeting-fragments#appending-or-prepending) content. ### Various - Fix: up-alias not matching URL query string with asterix after shash (#542) - Fix a bug where an overlay with viewport would not correctly shift multiple right-fixed elements - `[up-defer]` elements no longer have a hand cursor - Events like `up:link:follow` can now [open a layer with a given mode](/opening-overlays#modes) using the shorthand notation `event.renderOptions.layer = "new drawer"`. - Avoid logging `Uncaught AbortError` when the user presses the back button, but a script prevents the `up:location:restore` event. - Avoid logging `Uncaught AbortError` when the user closes the overlay, but a script prevents the `up:layer:dismiss` or `up:layer:accept` event. - Reduce the number of [layer lookups](/up.layer.get) during a render pass. 3.8.0 ----- This release brings many improvements that were requested by the [community](/community). The vast majority of these changes are backward compatible. Some breaking changes can be found with the [Reworked style helpers](#reworked-style-helpers). Existing calls are polyfilled by [`unpoly-migrate.js`](/changes/upgrading). ### Lazy loading content You can now [lazy load additional fragments](/lazy-loading) when a placeholder enters the DOM or viewport. By deferring the loading of non-[critical](https://developer.mozilla.org/en-US/docs/Web/Performance/Critical_rendering_path) fragments with a separate URL, you can paint important content earlier. For example, you may have a large navigation menu that only appears once the user clicks a menu icon: ```html <div id="menu"> Hundreds of links here </div> ``` To remove the menu from the initial render pass, extract its contents to its own route, like `/menu`. In the initial view, only leave a placeholder element and mark it with an `[up-defer]` attribute. Also set an `[up-href]` attribute with the URL from which to load the deferred content: ```html <div id="menu" up-defer up-href="/menu"> <!-- mark-phrase "up-defer" --> Loading... </div> ``` When the `[up-defer]` placeholder is rendered, it will immediately make a request to fetch its content from `/menu`. You may also delay the request until the placeholder is [scrolled into the viewport](/lazy-loading#on-reveal) or [control the timing from JavaScript](/lazy-loading#scripted). See [lazy loading content](/lazy-loading) for a full example and more details. ### Preloading links eagerly or lazily For many years Unpoly has supported the `[up-preload]` attribute. This would preload a link when the user [hovers](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseover_event) over it: ```html <a href="/path" up-preload>Hover over me to preload my content</a> ``` You can now preload a link *as soon as it appears in the DOM*, by setting an [`[up-preload="insert"]`](/up-preload#up-preload) attribute. This is useful for links with a high probability of being clicked, like a navigation menu: ```html <a href="/menu" up-layer="new drawer" up-preload="insert">≡ Menu</a> <!-- mark-phrase "insert" --> ``` To "lazy preload" a link when it is scrolled into the [viewport](/up-viewport), you can now set an [`[up-preload="reveal"]`](/up-preload#up-preload) attribute. This is useful when an element is [below the fold](https://www.optimizely.com/optimization-glossary/below-the-fold/) and is unlikely to be clicked until the the user scrolls: ```html <a href="/stories/106" up-preload="reveal">Full story</a> <!-- mark-phrase "reveal" --> ``` ### Infinite scrolling [Deferred fragments](/lazy-loading) that [load when revealed](/lazy-loading#on-reveal) can implement [infinite scrolling](/infinite-scrolling) without custom JavaScript. All you need is an HTML structure like this: ```html <div id="pages"> <div class="page">items for page 1</div> </div> <a id="next-page" href="/items?page=2" up-defer="reveal" up-target="#next-page, #pages:after"> load next page </div> ``` See [infinite scrolling](/infinite-scrolling) for a full example and more details. ### Enabling or disabling Unpoly features with boolean attributes Most Unpoly attributes can now be enabled with a value `"true"` and be disabled with a value `"false"`: ```html <a href="/path" up-follow="true">Click for single-page navigation</a> <!-- mark-phrase "true" --> <a href="/path" up-follow="false">Click for full page load</a> <!-- mark-phrase "false" --> ``` Instead of setting a `true` you can also set an empty value: ```html <a href="/path" up-follow>Click for single-page navigation</a> <a href="/path" up-follow="">Click for single-page navigation</a> <a href="/path" up-follow="true">Click for single-page navigation</a> ``` Boolean values can be helpful with a server-side templating language like ERB, Liquid or Haml, when the attribute value is set from a boolean variable: ```erb <a href="/path" up-follow="<%= is_signed_in %>">Click me</a> <%# mark-phrase "is_signed_in" %> ``` This can also help when you're generating HTML from a different programming language and want to pass a `true` literal as an attribute value: ```ruby link_to 'Click me', '/path', 'up-follow': true ``` This behavior is available for most attributes: - `[up-follow]` - `[up-submit]` - `[up-instant]` - `[up-preload]` - `[up-nav]` - `[up-expand]` - `[up-keep]` - `[up-hungry]` - `[up-poll]` - `[up-defer]` - `[up-validate]` - `[up-autosubmit]` - `[up-watch]` ### Request batching When queueing multiple requests to the same URL, Unpoly will now send a single request with a [merged `X-Up-Target` header](/X-Up-Target#merging). For example, these two render passes render different selectors from `/path`: ```js up.render('.foo', { url: '/path', cache: true }) up.render('.bar', { url: '/path', cache: true }) ``` Unpoly will send a single request with both targets: ```http GET /path HTTP/1.1 X-Up-Target: .foo, .bar ``` This allows you to have multiple [deferred placeholders](/lazy-loading#loading-multiple-fragments-from-the-same-url) that load from the same URL efficiently. ### More cache hits for tailored responses The following is a change for server routes that use the `Vary` header to optimize their responses to only include the requested `X-Up-Target`. When requests [target multiple fragments](/targeting-fragments#multiple) and the server responds with a `Vary` header, that response is now a cache hit for each individual selector: <table> <tr> <th class="split-table-head"> </th> <th align="left"> 🠦 <code>X-Up-Target: .foo, .bar</code><br> 🠤 <code>Vary: X-Up-Target</code> </th> </tr> <tr> <th align="left">🠦 <code>X-Up-Target: .foo</code></th> <td>✔️ cache hit</td> </tr> <tr> <th align="left">🠦 <code>X-Up-Target: .bar</code></th> <td>✔️ cache hit</td> </tr> <tr> <th align="left">🠦 <code>X-Up-Target: .foo, .bar</code></th> <td>✔️ cache hit</td> </tr> <tr> <th align="left">🠦 <code>X-Up-Target: .bar, .foo</code></th> <td>✔️ cache hit</td> </tr> <tr> <th align="left">🠦 <code>X-Up-Target: .baz</code></th> <td>❌ cache miss</td> </tr> <tr> <th align="left">🠦 <code>X-Up-Target: .foo, .baz</code></th> <td>❌ cache miss</td> </tr> <tr> <th align="left">🠦 <i>No <code autolink="false">X-Up-Target</code></i></th> <td>❌ cache miss</td> </tr> </table> See [how cache entries are matched](/caching#how-cache-entries-are-matched) for a detailed example. ### Cached content is retained while offline This release fixes some long-standing issues where the cache was evicted when a request failed due to [network issues](/network-issues), or when the server responds with an empty response. This fix restores the indented behavior that, even without a connection, [cached content](/caching) will remain navigatable for [90 minutes](/up.network.config#config.cacheEvictAge). This means that an offline user can instantly access pages that they already visited this session. ### Quick access to the form element in form events Form-related events like `up:form:submit` and `up:form:validate` are emitted on the element that caused the event. For example, `up:form:submit` is emitted on the submit button that was pressed. This made it somewhat inconvenient to access the form element: ```js up.on('up:form:submit', function(event) { let form = event.target.closest('form') console.log("form is", form) }) ``` You can now access the form element through a `{ form }` property on the event object: ```js up.on('up:form:submit', function({ form }) { console.log("form is", form) }) ``` ### Improvements to history restoration Several improvements have been made to the way Unpoly [handles the browser's "back" button](/restoring-history). #### Ensuring fresh content In earlier versions, when the user pressed the back button, Unpoly would sometimes restore the page with stale content. Starting with 3.8.0, restored content is now [revalidated](/caching#revalidation) with the server. This ensures that content is shown with the most recent data. #### Custom restoration behavior Listeners to `up:location:restore` may now mutate the `event.renderOptions` event to customize the render pass that is about to restore content: ```js up.on('up:location:restore', function(event) { // Update a different fragment when restoring /special-path if (event.location === '/special-path') { event.renderOptions.target = '#other' } }) ``` As a reminder, you can also completely substitute Unpoly's render pass with your own restoration behavior, by preventing `up:location:restore`. This will prevent Unpoly from changing any element. Your event handler can then restore the page with your own custom code: ```js up.on('up:location:restore', function(event) { // Stop Unpoly from rendering anything event.preventDefault() // We will render ourselves document.body.innerText = `Restored content for ${event.location}!` }) ``` ### Reworked style helpers This release reworks all functions that work with CSS properties: - `up.element.setStyle(element, props)` - `up.element.styleNumber(element, prop)` - `up.element.style(element, propOrProps)` - `up.element.createFromSelector(selector, { style })` - `up.element.affix(container, selector, { style })` - `up.animate(element, lastFrameProps)` #### Support for custom properties All functions that work with CSS properties now also support [custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) ("CSS variables"): ```js // Returns the computed value of the `--custom-prop` property. up.element.style(div, '--custom-prop') // Sets the `--custom-prop` property as an inline `[style]` attribute up.element.setStyle(div, { '--custom-prop': 'value' }) ``` #### Property names must be in kebab-case In earlier versions Unpoly functions accepted property names in either [camelCase](https://developer.mozilla.org/en-US/docs/Glossary/Camel_case) or [kebab-case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case). As custom properties don't have a camelCase equivalent, now only kebab-case is supported: ```js // ❌ camelCase property names are no longer supported up.element.setStyle(div, { backgroundColor: 'red' }) // ✔️ Property names must now be in kebab-case up.element.setStyle(div, { 'background-color': 'red' }) ``` To help with upgrading, [`unpoly-migrate.js`](/changes/upgrading) Unpoly will rename camelCase keys for you. #### Length values must have a unit CSS requires length values (like `width`, `top` or `margin`) to have a unit, e.g. `width: 200px`. In earlier versions Unpoly silently added a `px` unit to length values that were missing a unit. This approach required Unpoly to keep a list of CSS properties that denote lengths, which was unsustainable. You now always need to pass length values with a unit: ```js // ❌ Length values without unit is uo longer supported up.element.setStyle(div, { height: 50 }) // ✔️ Length values now require a unit up.element.setStyle(div, { height: '50px' }) ``` To help with upgrading, [`unpoly-migrate.js`](/changes/upgrading) Unpoly will add `px` units to unit-less length values. ### Rebrushed unpoly.com The design of [unpoly.com](https://unpoly.com) was reworked with fresh colors, better spacing and clearer fonts. All documentation pages now have a table of contents to quickly find the section you're looking for. Several new guides were also added: - [Attributes and options](/attributes-and-options) - [Preloading](/preloading) - [Lazy loading](/lazy-loading) - [Infinite scrolling](/infinite-scrolling) ### Other changes - `up.element.numberAttr()` now parses negative numbers. - When [updating history](/updating-history), the `html[lang]` is now also updated. This can be prevented by setting an `[up-lang=false]` attribute or passing a `{ lang: false }` option. - The function `up.util.microtask()` was deprecated. Use the browser's built-in [`queueMicrotask()`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) instead. - [Right-anchored](/up-anchored-right) can now control their appearance while a scrolling overlay is open, by styling the `.up-scrollbar-away` class. - Fix a bug where the back button did not work after following a link that contains an anchor starting with a number (fixes #603). - Clickable elements now get an ARIA role of `button`. In earlier versions these elements received a link `link` role. - Fix a bug where animating with `{ duration: 0 }` would apply the default duration instead of skipping the animation (fixes #588). - You can now exclude navigational containers from applying `.up-current` by adding a selector to `up.status.config.noNavSelectors`. 3.7.3 ----- - Fix a bug where, when rendering multiple fragments from a [cached](/caching) response, the new fragments would not be [revalidated](/caching#revalidation). This also affected render passes with `[up-hungry]` fragments. - [Targeting sibling elements](/targeting-fragments#targeting-a-sibling-element) now supports union selectors like `.parent .foo, .parent .bar`. 3.7.2 ----- ### Validation This change addresses multiple edge cases with concurrent user input during [form validations](/validation): - It is now possible to queue a validation for a fragment while a validation request for the same target is still loading. - Validations no longer throw an error if a targeted fragment is destroyed while a validation request is loading. Instead Unpoly will only update the fragments that are still present on the page (if any). - Validations are now aborted if the entire `<form>` element is [aborted](/aborting-requests). Previously individual validations were aborted when their target was aborted. - `up.validate()` now rejects with an `up.Aborted` error if a debounce delay was aborted (by aborting the `<form>` element). - When a new validation is queued while a previous validation request is still loading, the full debounce delay of the new validation is now honored. ### Autosubmit fixes This change fixes two more regressions for `[up-autosubmit]`, introduced by [3.7.0](https://unpoly.com/changes/3.7.0): - When the user changes a form field while a previous autosubmission is still loading, prevent that new change from being lost. - A debounce delay is now aborted if the entire `<form>` element is aborted. It no longer aborts the delay when the form's target is aborted. ### Fragment API - [Optional target selectors](/targeting-fragments#optional-targets) (with `:maybe` suffix) are now included in the `X-Up-Target` header if they match in the current page. Previously optional selector parts were always omitted from `X-Up-Target`. - The event `up:fragment:aborted` now has a new `{ reason }` property. Its a value is a string describing the reason for the fragment being aborted. 3.7.1 ----- This change fixes two regressions for form field watchers, introduced by [3.7.0](https://unpoly.com/changes/3.7.0): - When a change is detected while waiting for an async callback, prevent the new callback from crashing with `Cannot destructure property { disable } of null`. - When a change is detected while waiting for an async callback, the full debounce delay of that new change is honored. 3.7.0 ----- ### Focus ring visibility You can now control whether a focused fragment shows a [visible focus ring](/focus-visibility). Because Unpoly [often focuses new content](/focus#default-strategy), you may see focus outline appear in unexpected places. Focus rings are important for users of keyboards and screen readers to be able to orient themselves as the focus moves on the page. However, mouse and touch users often dislike the visual effect of a focus ring. To help your CSS show or hide focus rings in the right situation, Unpoly assigns CSS classes to the elements it focuses: - If the user [interacted with the keyboard](/up.event.inputDevice) or if the focused element is a [form field](/up.form.config#config.fieldSelectors), Unpoly will set an `.up-focus-visible` class. - If the user interacted with via mouse, touch or stylus, Unpoly will set an `.up-focus-hidden` class instead. You can use these classes to [hide unwanted focus rings](/focus-visibility#hide), or [style focus rings on new components](/focus-visibility#show). The following supporting changes have also been made: - You can set `up.viewport.config.autoFocusVisible` to a function that decides if an element should get a `.up-focus-visible` or `.up-focus-hidden` class. - Added a new property `up.event.inputDevice`. Its value is a string describing the class of input device used for the current task. - Unpoly will try to force or unset [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) as it sets focus classes, but can only do so in [some browsers](https://caniuse.com/mdn-api_htmlelement_focus_options_focusvisible_parameter). - The `up.focus()` function accepts a new `{ focusVisible }` option to control whether `.up-focus-hidden` or `.up-focus-visible` is set on a focused element. See [Focus ring visibility](/focus-visibility) for more details and examples. ### Reacting to form changes This release addresses many edge cases with features that watch form fields for changes, in particular `[up-watch]`, `[up-autosubmit]` and `up.watch()`: - Watchers now detect changes in fields that were inserted dynamically later. This regression was introduced by Unpoly 3.0. - Watchers now detect changes when the form is [reset](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/reset). - Fix an issue where `[up-autosubmit]` would not work on forms that also have [dependent fields](/dependent-fields) using `[up-validate]`. - Watchers no longer run callbacks if the form was [aborted](/aborting-requests) or detached while [waiting for a previous async callback](/up-watch#async-callbacks). - Watchers now abort their [debounce delay](/watch-options#debouncing) if the entire form is aborted. Previously it would abort the delay if any watched field was aborted. - `[up-autosubmit]` now aborts a debounce delay if either the form element or the [form's target](/up-submit#up-target) are aborted. It no longer aborts the delay if any watched field is aborted. ### Other changes - `up.on()` takes a `{ capture: true }` option to register a listener that runs before the event is emitted on the element. - Scrolling now defaults to `{ behavior: 'instant' }` to prevent picking up a `scroll-behavior` CSS property. To do pick up the property, pass `{ behavior: 'auto' }`. - New function `up.form.isField()`. It returns whether the given element is a [form field](/up.form.config#config.fieldSelectors). 3.6.1 ----- - Fix a bug where [new overlays](/opening-overlays) would not have history if the initial fragment matches a [layer-specific main target](https://unpoly.com/up-main#overlays) like `[up-main=modal]`. 3.6.0 ----- ### Targeting fragments - Unpoly-specific pseudo selectors like `:main` or `:layer` can now be used in a compound target, e.g. `:main .child`. - Targeting `:main` will no longer [match in the region of the interaction origin](/targeting-fragments#ambiguous-selectors)). It will now always use the first matching selector in `up.fragment.config.mainTargets`. - Fix a bug where following a navigation item outside a [main](/main) element would focus the `<body>` instead of the main element. ### Performance improvements - Unpoy now uses the native `:has()` selector [where available](https://developer.mozilla.org/en-US/docs/Web/CSS/:has). Unpoly's polyfill for `:has()` will remain included for the time being. It will be removed as Firefox' `:has()` support has reached the majority of users (available on Nightly now). - Improve performance of many element lookups, by finding elements via CSS selectors (vs. filtering lists with JavaScript). ### Support for [structured data markup](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data) - Structured data in `script[type="application/ld+json"]` elements is considered a meta tag that will be [updated with history changes](/updating-history#history-state). - `script[type="application/ld+json"]` elements in are now preserved in new fragments with `up.fragment.config.runScripts = false`. ### Bugfixes and minor improvements - CSP nonces [embedded into attribute callbacks](https://unpoly.com/csp#nonceable-attributes) now work with [`Content-Security-Policy-Report-Only`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only). - When the `X-Up-Validate` header value exceeds 2048 characters, it is now set to `:unknown`. This is to prevent web infrastructure from rejecting an overly long request line with an `413 Entity Too Large` error. - Fix a bug where `up:assets:changed` would be emitted for every response when configuring `up.fragment.config.runScripts = false`. - `up.form.isSubmittable()` now returns `false` for forms with a cross-origin URL in their `[action]` attribute. - `up.util.contains()` now works on `NodeList` objects. - You can now [configure](/up.script.config#config.scriptSelectors) which elements are removed by `up.fragment.config.runScripts = false`. 3.5.2 ----- Continuing our focus on stability, this release addresses some long-standing issues: - Fix a bug where `<video>` and `<audio>` elements would render incorrectly in Safari ([#432](https://github.com/unpoly/unpoly/issues/432)). - Fix a bug where `<script up-keep>` elements would re-run during subsequent render passes. - Fix a bug where `<script>` elements would not run when [targeted](/targeting-fragments) directly. - Fix a bug where `<noscript up-keep>` elements would not be persisted during fragment updated. - Fix a bug where `<noscript>` elements would lose their text content when targeted directly. 3.5.1 ----- This releases fixes two regressions introduced by [3.5.0](https://unpoly.com/changes/3.5.0): - Fix a bug where a [new overlay](/opening-overlays) would immediately close if the *parent* layer's location happened to match the overlay's location-based close condition. - When a new overlay's initial location matches its [location-based close condition](/closing-overlays#location-condition), the overlay again immediately closes without rendering its initial content. 3.5.0 ----- Unpoly 3.5 brings major quality-of-life improvements and addresses numerous edge cases in existing functionality. ### Notification flashes You can now use an `[up-flashes]` element to render confirmations, alerts or warnings: ![A confirmation flash, an error flash and a warning flash](images/flashes.png){:width='480'} To render a flash message, include an `[up-flashes]` element in your response. The element's content should be the messages you want to render: ```html <div up-flashes> <strong>User was updated!</strong> <!-- mark-line --> </div> <main> Main response content ... </main> ``` An `[up-flashes]` element comes with useful default behavior for rendering notifications: - Flashes will always be updated when rendering, even if they aren't targeted directly (like `[up-hungry]`). - Flashes are kept until new messages are rendered. They will not be cleared by an empty `[up-flashes]` container. You can use a compiler to [clear messages after a delay](/flashes#clearing-after-delay). - You are free to place the flashes anywhere in your layout, inside or outside the [main](/main) element you're usually updating. - You can have a single flashes container on your [root layer](/up.layer), or one on each layer. - When a response [causes an overlay to close](/closing-overlays#close-conditions), the flashes from the dis