You may also configure overlay targets unobtrusively:
up.layer.config.popup.mainTargets.push('.menu') // for popup overlays
up.layer.config.drawer.mainTargets.push('.menu') // for drawer overlays
up.layer.config.overlay.mainTargets.push('.layout--content') // for all overlay modes
Setting a default transition
I'm not a big fan of animating every fragment update.
But since some of you do this, here is how to set a default transition:
We're going to learn more about up.fragment.config.navigateOptions in a minute.
Reworked Bootstrap integration
Many projects didn't use built-in Bootstrap integration, because it was too opinionated.
For example, Unpoly tried to re-use the Bootstrap modal styles, but most projects simply wanted the white box from the Unpoly default. Projects ended up using their own configuration, which was much more minimal.
Unpoly 2 now ships with a unopinionated Bootstrap integration.
This is all of unpoly-bootstrap4.js:
// Bootstrap expects the class .active instead of .up-active
up.feedback.config.currentClasses.push('active')
// Set .up-current classes in Bootstrap navigation components
up.feedback.config.navs.push('.nav', '.navbar')
// When validating, update the closest form group with the results
up.form.config.validateTargets.unshift('.form-group:has(&)')
// When revealing, scroll far enough so content is not covered by// fixed Bootstrap bars
up.viewport.config.fixedTop.push('.navbar.fixed-top')
up.viewport.config.fixedBottom.push('.navbar.fixed-bottom')
up.viewport.config.anchoredRight.push('.navbar.fixed-top', '.navbar.fixed-bottom')
/// Don't use common utility classes to build selectors
up.fragment.config.badTargetClasses.push(
'row',
/^col(-xs|-sm|-md|-lg|-xl)?(-\d+)?$/,
/^[mp][tblrxy]?-\d+$/
)
Support for Bootstrap 3, 4 and 5
Unpoly 2 now supports the three major Bootstrap versions we're using:
unpoly-bootstrap3.js
unpoly-bootstrap3.css
unpoly-bootstrap4.js
unpoly-bootstrap4.css
unpoly-bootstrap5.js
unpoly-bootstrap5.css
Layer API
Demo
Show in the demo app:
Infinite stacking: Company / Project / Budget
Returning with value: Create budget from project details
New layer terminology
The root page, modals and popups have been consolidated into a single term layer.
There are different layer modes, e.g. modal or popup.
An overlay is any layer that is not the root layer.
Mode
Description
Overlay?
root
The root page
no
modal
A modal dialog box
yes
drawer
A drawer sliding in from the side
yes
popup
A popup menu anchored to a link
yes
cover
Covers entire screen
yes
Layers can be stacked infinitely
In Unpoly 1 you could only open a single modal.
This limited its practical applications.
Example from a real application:
An index page on the root layer shows a list of records
Clicking a record opens a record in a modal overlay. This is useful since the user retains the scroll position of the list in the background.
The details screen cannot open another modal overlay, since one is already open.
Unpoly 2 lets you stack an arbitrary number of layers.
Show demo
Layers are fully isolated
In Unpoly 1 you could accidentally update another layer.
In Unpoly 2 layers are fully isolated. You cannot accidentally target an element in another layer:
<ahref="/path"up-target=".foo"><!-- will only match in current layer -->
If you want to do anything in another layer, use an [up-layer] attribute:
<ahref="/path"up-target=".foo"up-layer="parent"><!-- will only match in parent layer --><ahref="/path"up-target=".foo"up-layer="root"><!-- will only match in root layer --><ahref="/path"up-target=".foo"up-layer="new"><!-- opens a new modal overlay -->
You can always look at [up-layer] to know what layer is going to be updated.
If there is no [up-layer] attribute, you are going to update the current layer.
CSS selectors are matched in the current layer
JavaScript functions that take a CSS selector will only look in the current layer:
up.submit('.user-form') // will only find in the current layer
up.fragment.get('.foo') // will only find in the current layer
If you want to match a selector in another layer, use a { layer } option:
up.fragment.get('.first', { layer: 'any' }) // will find in any layer
up.fragment.get('.first', { layer: 'parent' }) // will find in parent layer
You don't need a { layer } option when you pass DOM elements instead of selectors:
up.submit(form) // will submit the form in the form's layer
up.fragment.get(element, '.child') // will find .child in element's descendants
Referring to layers
Layer name
Description
current
The current layer
any
Any layer, preferring the current
parent
The layer that opened the current layer
closest
The current layer or any ancestor, preferring closer layers
root
The root layer
overlay
Any overlay
origin
The layer of the element that triggered the current action
<Number>
The layer with this index
Layer name
Description
ancestor
Any ancestor layer of the current layer
child
The child layer of the current layer
descendant
Any descendant of the current layer
<Element>
The given element's layer
current root
Space-separated alternatives
front
The frontmost layer (which may not be the current layer)
Most events are associated with a layer
Layers are rarely interested in events of other layers.
Where possible Unpoly 2 will emit its events on associated layers instead of document.
This way you can listen to events on one layer without receiving events from other layers.
Events resulting from user navigation (like up:link:follow, up:request:load) are associated with the layer of the activated element.
Listening to your layer
Unpoly 2 provides convenience functions up.layer.on() and up.layer.emit()
to listen / emit on the current layer:
up.layer.on('up:request:load', callback) // only listen to events from the current layer
up.layer.emit('my:event') // emit my:event on the current layer's element
The current layer is the layer in which you are compiling or navigating.
Listening to everything
Layer events will still bubble up to the document,
so you can still register
a listener for events from any layer:
up.on('up:request:load', callback) // listen to events from all layers
Remembering the current layer
Like most functions, up.layer.dismiss() will affect the "current" layer,
so it's an alias for up.layer.current.dismiss().
up.layer.current is set to the right layer in compilers and most events, even if that layer is not the "front" layer. E.g. if you're compiling a fragment for a background layer, up.layer.current will be
the background layer during compilation.
If you have async code, the current layer may change when your callback is called.
You may also retrieve the current layer for later reference:
Note that fragment updates may target different layers for successful (HTTP status 200 OK) and failed (status 4xx or 5xx) responses. Use up.fail_target to learn about the
layer targeted for a failed update:
Our effective contact object would now be something like { project: 'Hosting 2021' }.
The server can inspect the context in /contacts/index.erb:
<%if up.context[:project] %>
Pick a contact for <%= up.context[:project] %>:
<%else%>
List of contacts
<%end%>
<%@contacts.each do|contact|%>
<li>...</li>
<%end%>
New overlay HTML structure
Unpoly 2 uses a more compact HTML markup for its overlays.
If you have customized your modals and popup with CSS, this is a breaking change for you.
In Unpoly 1, overlays grew with the size of the content.
This was impractical because
a single long line of text would stretch the overlay to its maximum width.
Because of this most projects have configured modals to have a fixed size.
Many projects also have hacks to open modals with different sizes.
In Unpoly 2 all overlays have a given size that sets a maximum width:
<ahref="/path"up-layer="new"up-size="small">open small modal</a><ahref="/path"up-layer="new"up-size="medium">open medium modal</a><ahref="/path"up-layer="new"up-size="large">open large modal</a><ahref="/path"up-layer="new"up-size="auto">open growing modal</a>
Default sizes
Mode
small
medium
large
grow
modal
350px
650px
1000px
grow with content
popup
180px
300px
550px
grow with content
drawer
150px
340px
600px
grow with content
cover
100%
100%
100%
100%
These are generally wider than Bootstrap's counterparts.
Regardless of size, overlays never grow wider than the screen width.
The HTML markup for a given overlay mode is now static.
There is no up.modal.config.template anymore.
Many former use cases for up.modal.config.template are covered by assigning a class or size.
If you do need to customize the overlay HTML, you may use the up:layer:opened event to modify the layer as it becomes visible. The event is emitted
before the opening animation starts.
Drawer overlays used to be a modal "flavor" in Unpoly 1.
They shared their HTML markup with
standard modal overlays.
There is no up.modal.flavors in Unpoly 2 anymore.
Drawers are now a first-class overlay
mode with their own <up-drawer> element.
There is currently no API to define a custom overlay mode.
New layer mode: Cover
Unpoly 2 ships with a new layer mode called cover.
It overlays the entire page, including application layout.
It brings its own scrollbar.
<ahref="/path"up-layer="new cover">
You often see cover overlays in mobile apps, e.g. on settings screens.
Dismissability can be fine-tuned
In Unpoly 1 you could prevent a user from closing a layer with the { closable: false } option.
In Unpoly 2 you may choose which closing methods are available to the user:
Option
Effect
Dismiss value
{ keyDismissable }
Enables dimissing with Escape key
:key
{ outsideDismissable }
Enables dismissing by clicking on the background
:outside
{ buttonDismissable }
Adds an "X" button to the layer
:button
You may also enable or disable all closing methods together with the { dismissable } option.
Overlays without history
Modals without history required too much code in Unpoly 1.
In Unpoly 1 you could use { history: false } to open an overlay without updating the browser history. However, every user navigation within that overlay would affect history, unless you had [up-history=false] on every link. This is impractical, since a link should not need to know whether it is used within an overlay.
Unpoly 2 reworks that behavior to what you would expect:
When an overlay is opened without history, no contained link or form will ever update history.
When an overlay without history opens another overlay, that other overlay will never update history.
Layers without history know their current location URL (up.layer.location).
Layers without history support the .up-current class.
Sub-interactions
Why we need sub-interactions
Show scenario in the demo app
This story is the base use case for a sub-interaction:
User starts filling out the form for a new project
To create a project, the user must select a company. But the desired company does not yet exist.
The user may open a new overlay to create the missing company.
The unfinished project form remains open in the background.
When the company was created in the overlay, the overlay should close.
The project form should now have the newly created company selected.
Unpoly 1 modals didn't work well for this
Unpoly 1 hade no way to communicate the "result" of an overlay back to the parent layer.
Overlay content needed to update fragments in the parent layer to continue the story there.
This required the overlay to know the parent layer's state, coupling the sub-interaction to the parent interaction.
In Unpoly 2 overlays no longer need to know about the parent layer's state.
Acceptance conditions in Unpoly 2
When opening an overlay in Unpoly 2, you may define a condition when the overlay interaction ends.
When the condition occurs, the overlay is automatically closed and a callback is run.
This completely decouples the outer interaction from sub-interactions.
Overlay result values
Overlays in Unpoly 2 may have a result value.
E.g. if the user selects a value, creates a record or confirms an action,
we consider the overlay to be accepted with that value.
up.layer.open({
url: '/select-user',
onAccepted: (event) =>console.log('Got user ', event.value)
})
The following slides examine how an overlay can be accepted.
Accepting when a location is reached
The following will open an overlay that closes once a URL like /companies/123 is reached:
<ahref="/companies/new"up-layer="new"up-accept-location="/companies/$id"up-on-accepted="alert('New company with ID ' + value.id)">
New company
</a>
Placeholders in the URL pattern ($id) become the overlay's acceptance value.
The [up-on-accepted] callback is called with an acceptance value.
Reloading on acceptance
A common callback is to reload an element in the parent layer:
<ahref="/companies/new"up-layer="new"up-accept-location="/companies/$id"up-on-accepted="up.reload('.company-list')">
New company
</a><divclass="company-list">
...
</div>
Adding options to an existing select
Another common callback reloads <select> options and selects the new foreign key:
<selectname="company">...</select><ahref="/companies/new"up-layer="new"up-accept-location="/companies/$id"up-on-accepted="up.validate('select', { params: { company: value.id } })">
New company
</a>
Why this is useful
You already have a CRUD interaction for companies
You can now embed the existing company form into your project form
The embedded interaction does not need to know when it's "done" or
what to do when it's done. Instead the parent layer defines an
acceptance condition and callback action.
Accepting when an event is emitted
Instead of waiting for a location to be reached,
you may accept an overlay
once a given event is observed on the overlay:
<ahref="/users/new"up-layer="new"up-accept-event="user:created"up-on-accepted="alert('Hello user #' + value.id)">
Add a user
</a>
The event object becomes the overlay's acceptance value.
Emitting events from your frontend
Unpoly uses standard DOM events that you can emit with Element#dispatchEvent() or up.emit().
To emit an event on the current layer, your JavaScript code may use up.layer.emit():
up.layer.emit('user:created', { id: 123 })
Emitting when a link is clicked
To emit an event when the user clicks on a link, you may also use [up-emit] in your HTML:
<ahref='/users/5'up-emit='user:selected'up-emit-props='{ "id": 5 }'>
Select user #5
</a>
A parent layer waiting for this event will prevent it, causing the link not to be followed.
Emitting events from the server
In a server-side application, events often occur on the server.
The server-sent event is emitted on the updated layer.
To emit on the document, use up.emit() instead.
Accepting a layer explictly
If you want to be really direct, assign an [up-accept] attribute to a link in an overlay.
The link's layer will be closed when the link is clicked:
<ahref='/users/5'up-accept='{ "id": 5 }'>Choose user #5</a>
The JSON value of the [up-accept] attribute becomes the overlay's acceptance value.
The link's [href] is just a fallback here. It will only be followed if the link is on the root layer (where there is no overlay to accept). You can also omit the [href] attribute.
I'm not a huge fan of this because it couples the overlay interaction to its parent layer.
But some of you asked for it, and I guess sometimes the overlay cannot be decoupled
from its parent.
Accepting from the server
The server can also accept an overlay explicitly:
classUsersController < ApplicationControllerdefcreate
build_user
if@user.save
if up.overlay?
up.layer.accept(id:@user.id)
up.render_nothing
else
redirect_to @userendendend
...
end
Positive vs. negative close intent
We've seen examples where an overlay was accepted with a result value.
But what happens if the user presses ESC or clicks the "X" button?
For this Unpoly 2 distinguishes two kinds close intents:
Accepting a layer (user picks a value, confirms with "OK", etc.), optionally with a value
Dismissing a layer (user clicks "Cancel", "X", presses ESC, clicks on the background)
Acceptance means next step Dismissal means cancelation
When you're waiting for a sub-interaction to finish successfully,
you're probably only interested in layer acceptance, but not dismissal.
When opening a layer you may pass separate { onAccepted } and { onDismissed } callbacks:
up.layer.open({
url: '/users/new',
onAccepted: (event) =>console.log("New user is " + event.value),
onDismissed: (event) =>console.log("User creation was canceled")
})
Overlays are promises
It's useful to think of overlays as promises
which may either be fulfilled (accepted) or
rejected (dismissed).
Instead of using up.layer.open() and passing callbacks, you may use up.layer.ask().
up.layer.ask() returns a promise for the acceptance value, which you can await:
let user = await up.layer.ask({ url: '/users/new' })
console.log("New user is " + user)
Navigation intent
Switching screens vs. updating boxes
Not all fragment updates are user navigation.
When Unpoly 1 updates a fragment, it applies many opinionated defaults:
Update the browser location
Scroll to reveal the new fragment
Cache responses for 5 minutes
Replace <body> for unexpected responses
While these are good defaults when the user follows a link a to navigate to a new screen,
they get in the way when you only want to update a small box.
Actual usage often looked like this:
up.replace('.message-count', '/inbox', {
cache: false, // opt out of cachinghistory: false, // opt out of historyreveal: false, // opt out of scrollingfallback: false// opt out of fallback targets
})
Unpoly 2 defaults are opt-in, not opt-out
Unpoly 2 replaces up.replace(), up.extract() with a unified function up.render().
When updating a fragment in Unpoly 2 with up.render(), you will get very few defaults:
// Will not update history, will not scroll, etc.
up.render('.message-count', { url: '/inbox' })
Options are now opt-in instead of opt-out:
// Will update history, but not scroll, etc.
up.render('.message-count', { url: '/inbox', history: true })
Navigation defaults
You can opt into defaults for user navigation with { navigate: true }:
// Will update history, will scroll, etc.
up.render('.content', { url: '/users/5', navigate: true })
Instead of up.render({ navigate: true }) you may also use up.navigate():
// Will update history, will scroll, etc.
up.navigate('.content', { url: '/inbox' })
Full list of navigation defaults
Option
Effect
Default
Navigation default
{ history }
Update browser location and window title
false
'auto'
{ scroll }
Scroll to the new element
false
'auto'
{ fallback }
Update <body> for unexpected responses
false
true
{ cache }
Cache responses for 5 minutes
false
true
{ feedback }
Set .up-active on the activated link
false
true
{ focus }
Focus after update
false
'auto'
{ solo }
Cancel existing requests
false
true
{ peel }
Close overlays when targeting a layer below
false
true
You can configure your navigation defaults with up.fragment.config.navigateOptions.
Clicking links and submitting forms are still considered navigation
You can opt out with the [up-navigate=false] attribute.
Deprecated render functions are still considered navigation
Deprecated functions like up.replace() and up.extract() navigate by default.
That's was the behavior in Unpoly 1, and we don't want to break your apps.
Accessibility
Types of disabilities
We will mostly talk about two types of disabilities:
Visual (low vision or blindness)
Lack of fine motor control (hard to point mouse)
As a user with one or both of these impairements I might use a screen reader with a keyboard.
Screen readers
Screen readers use speech synthesis that read a webpage aloud.
They come with many additional keyboard shortcuts to jump between page elements and describe what is visible on the screen.
When clicking "Show card #2 authors", Unpoly 2 replaces the first card's authors
When clicking "Show card #2 authors", Unpoly 2 replaces the second card's authors.
Extended URL patterns
Unpoly 1 supported URL wildcards like /users/* for [up-alias].
Unpoly 2 supports extended URL patterns for [up-alias], [up-accept-location] and [up-dismiss-location]:
Pattern
Meaning
/users/*
Any prefix
*/users
Any postfix
/users/* /account
Space-separated alternatives (OR)
/users/* -/users/new
Exclude with minus-prefix
/users/:id
Capture segment (any string)
/users/$id
Capture segment (only integers)
Navigation aborts earlier requests
Unpoly 1 has concurrency issues on slow connections:
User clicks link #1
Server takes long to respond to #1
User clicks another link #2
Server responds with #2
Server responds with #1
User sees effects of #1,
but expected to see effects of #2.
In Unpoly 2 user navigation aborts existing requests,
reducing concurrency:
User clicks link #1
Server takes long to respond to #1
User clicks another link #2
Unpoly cancels request for #1
Server responds with #2
User sees effects of #2
You may disable this with { solo: false } (JS) or [up-solo="false"] (HTML).
Calmer log
Dealing with compiler errors
Unpoly 1 has undefined behavior when a compiler crashes.
In Unpoly 2 a fragment update will always terminate cleanly:
When a compiler throws an error, other compilers will now run anyway.
When a destructor throws an error, other destructors will now run anyway.
In any case errors will be logged.
up:click
It's hard to observe click when an element might be [up-instant].
The up:click event is generally emitted when an element is clicked. However, for elements
with an [up-instant] attribute this event is emitted on mousedown instead.
This is useful to listen to links being activated, without needing to know if
a link is [up-instant].
Example
Assume we have two links, one of which is [up-instant]:
Explain how without revealing, the second page would open with the first page's scroll positions
However, this default also caused scrolling when a smaller fragment was updated.
E.g. when the user switches between tabs, they wouldn't expect scroll changes.
Unpoly 2 no longer scrolls by default
When navigating the new default is { scroll: 'auto' }, which sometimes scrolls:
If the URL has a #hash, scroll to the hash.
If updating a main target, scroll to the top.
The assumption here is that we navigated to a new screen.
Otherwise don't scroll.
You may configure this behavior with up.fragment.config.autoScroll.
Tuning the scroll effect
Option
Meaning
Default
{ revealPadding }
Pixels between element and viewport edge
0
{ revealTop }
Whether to move a revealed element to the top
false
{ revealMax }
How much of a high element to reveal
0.5 * innerHeight
{ revealSnap }
When to snap to the top edge
200
{ scrollBehavior }
auto/smooth
'auto' (no animation)
{ scrollSpeed }
Acceleration of smooth scrolling
1 (mimic Chrome)
History is updated less often
When navigating the new default is { history: 'auto' }, which sometimes updates history:
When swapping a fragment, history is only changed when the fragment is a main target.
The assumption is that we navigated to a new screen.
When opening an overlay, the layer history is set to the mode default
(e.g. up.layer.config.modal.history).
The overlay will keep this setting for its lifetime.
You may configure this behavior with up.fragment.config.autoHistory.
Smarter bandwidth usage
User navigation aborts existing requests
There is a single concurrency setting (default 4) for both regular requests and preload requests.
Bandwidth-friendly polling implementation (see below)
Preload on touch devices starts on touchstart
Preload requests are aborted as the user un-hovers the link
Preloading is now automatically disabled on slow connections
Slow connection?
We consider a connection to be slow if at least one of these conditions are true:
TCP Round Trip Time is >= 750 ms (up.network.config.slowRTT)
Downlink is <= 600 Kbps (up.network.config.slowDownlink)
User has data saving enabled ("Lite mode" in Chrome)
The values above may change during a session.
Unpoly will enable/disable preloading as conditions change.
Polling
️ Polling was a much-requested feature. Userland implementations often don't handle edge cases.
Polling means reloading the same fragment periodically.
Unpoly 2 ships with a polling implementation that handles edge cases.
Basic example
Assign the [up-poll] attribute to any element to reload it every 30 seconds:
<divclass="unread-messages"up-poll>
You have 2 unread messages
</div>
Configuring the reload URL
The polling fragment is reloaded from the URL that originally brought it into the DOM.
You may set an optional [up-source] attribute to reload from a different source:
<divclass="unread-messages"up-pollup-source="/inbox/unread">
You have 2 unread messages
</div>
Configuring the poll interval
You may set an optional [up-interval] attribute to change the reload interval:
<divclass="unread-messages"up-pollup-interval="10_000">
You have 2 unread messages
</div>
Digit groups separators (10_000) are a stage 3 ES6 feature and also supported
in Unpoly 2 number attributes.
You can also configure the interval default globally with up.fragment.config.pollInterval.
Polling pauses while the browser tab is invisible
This prevents unnecessary server load when another tab is in the foreground,
or when users keep apps running over night.
Polling pauses while the connection is slow
Don't DoS slow cellular connections.
Polling pauses while the fragment's layer is in the background
It resumes when the layer is unveiled.
Cheap polling responses
️ Naive polling implementation will often cause unchanged content to be re-rendered.
You may timestamp your fragments with an [up-time] attribute to indicate when the underlying data was last changed. For instance, when the last message in a list was received from December 24th, 1:51:46 PM UTC:
Only rendering when needed saves CPU time on your server,
which spends most of its response time rendering HTML.
This also reduces the bandwidth cost for a request/response exchange to ~1 KB.
For comparison: A typical request/response exchange on Cards eats 25-35 KB.
Improved server integration
In Unpoly 1 the server could not trigger changes in the frontend.
Unpoly always had an optionalprotocol
your server may use to exchange additional
information when Unpoly is updating fragments. The protocol mostly works by adding
additional HTTP headers (like X-Up-Target) to requests and responses.
Unpoly 2 extends the optional protocol with additional headers that the server
may use to interact with the frontend.
Ruby on Rails examples ahead, but don't panic!
The code examples on the following slides are for the unpoly-rails integration.
Expect integrations for other frameworks and languages to be updated for the new protocol
after the release of Unpoly 2.
While you wait for your integration to be updated for Unpoly 2, existing protocol
implementations will keep working with an Unpoly 2 frontend.
This will render a 200 OK response with a header X-Up-Target: :none and an empty body.
Optimizing cacheability
Unpoly sends some additional HTTP headers to provide information about the fragment update:
Request property
Request header
up.Request#target
X-Up-Target
up.Request#failTarget
X-Up-Fail-Target
up.Request#context
X-Up-Context
up.Request#failContext
X-Up-Fail-Context
up.Request#mode
X-Up-Mode
up.Request#failMode
X-Up-Fail-Mode
The server may return an optimized response based on these properties,
e.g. by omitting a navigation bar that is not targeted.
To improve cacheability, you may may set
up.network.config.requestMetaKeys to a shorter list of property keys:
// Server only optimizes for layer mode, but not for target selector
up.network.config.requestMetaKeys = ['mode']'
You may also send different request properties for different URLs:
up.network.config.requestMetaKeys = function(request) {
if (request.url == '/search') {
// The server optimizes responses on the /search route.return ['target', 'failTarget']
} else {
// The server doesn't optimize any other route,// so configure maximum cacheability.return []
}
}
Request meta information persists across redirects
When Unpoly makes a request, it sends along meta information as request headers:
In Unpoly 1, these headers were lost after a redirect.
Subsequent requests would see a non-Unpoly request.
With unpoly-rails 2.0 meta information is persisted across redirects.
Example: Below the #create action saves a Note record and redirects to
that note's #show action. In Unpoly 2 the #show can still see if the
original request to #create was made by Unpoly, and not render an unneeded layout:
Every kind of fragment update accepts the full set of render() options.
Easy programmatic fragment changes
Unpoly 1 required you to pass a target selector with every fragment update, which was very tedious. The selector was often the same, or inferable from context.
Unpoly 2 lets you render fragments with fewer arguments.
Open a layer without content
up.layer.open()
Open a layer without target
up.layer.open({ content: 'Helpful text' }) // opens in main target
If you have compilers that only set default attributes, consider using a single event listener that manipulates event.renderOptions. It's much leaner than a compiler, which needs to be called for every new fragment.
Advanced example
Assume we give a new attribute [require-session] to links that require a signed-in user:
<ahref="/projects"require-session>My projects</a>
When clicking the link without a session, a login form should open in a modal overlay.
When the user has signed in successfully, the overlay closes and the original link is followed.
That means that classic tooltips won't work on mobile. Consider a clickable popup instead:
<spanup-layer="popup"up-content="Locking a user will prevent them from using the application">
Help
</span>
Handling unexpected server responses
Servers sometimes respond with a fatal error, a maintenance page or non-HTML content.
Unpoly 1 didn't have a good way to deal with these exceptions.
Unpoly 2 emits a new event up:fragment:loaded when the server responds with the HTML, before the HTML is used to change a fragment.
Event listeners may call event.preventDefault() on an up:fragment:loaded event to prevent any changes to the DOM and browser history. This is useful to detect an entirely different page layout (like a maintenance page or fatal server error) which should be open with a full page load.
Example
The following will make a full page load if any fragment update responds
with a maintenance page:
up.on('up:fragment:loaded', function(event) {
let isMaintenancePage = event.response.getHeader('X-Maintenance')
if (isMaintenancePage) {
// Prevent the fragment update and don't update browser history
event.preventDefault()
// Make a full page load for the same request.
event.request.loadPage()
}
})
Summary
Breaking changes
There's a short list of changes that we cannot fix with aliases.
Overlays (modals, popups) have different HTML
But it's similar. E.g. <div class="modal"> becomes <up-modal>.
Unpoly only sees the current layer
You can target other layers with { layer: 'any' }.
Async functions no longer wait for animations
You might or might not notice.
Tooltips are no longer built-in
But there are a million better libraries.
Unpoly 2 objectives
Kill boilerplate configuration
Repetitive configuration code is now the default.
New, unopionated Bootstrap integration.
Layer API
A new layer API replaces modals and popups.
Layers are isolated and can be stacked infinitely.
Sub-interactions
Branch off sub-interaction into an overlay.
Overlay results are propagated back to parent layer.
Navigation intent
Not every fragment update means a user navigation.
Switching screens need other defaults than updating a box.
Accessibility
All Unpoly features carefully manage focus.
Keyboard navigation is supported everywhere.
When updating fragments you have two additional options:
| Value | Description |
| ------ | ---------------------------------------- |
| `swap` | Replace the front layer with a new layer |
| `new` | Open a new layer |
**Example:** Our index actions often have a search box to filter entries.
We sometimes persist a search query on the server and restore it for subsequent visits.
```ruby
def NotesController < ApplcationController
def index
if params[:query].nil?
# Restore a query from a previous visit.
query = current_user.last_notes_query
else
query = params[:query]
# Remember the query for the next visit.
current_user.update_attribute(:last_notes_query, query)
# Earlier responses might now be cached with the wrong results,
# so clear the cache for all index URLs.
up.cache.clear('/notes/*')
end
@notes = Note.search(query).paginate
end
...
end
```
To update with a navigation intent, use either `{ navigate: true }` or `up.navigate()`:
```js
up.render({ url: '/path', navigate: true }) // same as below
up.navigate({ url: '/path' }) // same as above
```
- Kill boilerplate configuration
- Layer rewrite
- Sub-interactions
- Navigation intent
- Accessibility
- Quality of live improvements