Skip to content

Commit

Permalink
Add View Transitions API support (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
imacrayon authored Nov 18, 2023
1 parent 9836079 commit d88594d
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 8 deletions.
35 changes: 33 additions & 2 deletions docs/examples/inline-edit.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,37 @@ Lastly, we'll update the edit form and the "Cancel" link so that focus is return

Try using the keyboard in the following demo and notice how keyboard focus is maintained as your navigate between modes.

<style>
@keyframes fade-in {
from { opacity: 0; }
}

@keyframes fade-out {
to { opacity: 0; }
}

@keyframes slide-from-right {
from { transform: translateX(25%); }
}

@keyframes slide-to-left {
to { transform: translateX(25%); }
}

/* define animations for the old and new content */
::view-transition-old(slide-fade) {
animation: 200ms ease 150ms both fade-out, 200ms ease 150ms both slide-to-left;
}
::view-transition-new(slide-fade) {
animation: 300ms ease 50ms both fade-in, 300ms ease 50ms both slide-from-right;
}

form {
background: #fff;
view-transition-name: slide-fade;
}
</style>


<script type="module">
let contact = {
Expand All @@ -90,7 +121,7 @@ Try using the keyboard in the following demo and notice how keyboard focus is ma
window.example('/contacts/1')

function edit(contact) {
return `<form id="contact_1" x-init x-target method="put" action="/contacts/1" x-focus="contact_1_edit" aria-label="Contact Information">
return `<form id="contact_1" x-init x-target x-merge.transition method="put" action="/contacts/1" x-focus="contact_1_edit" aria-label="Contact Information">
<div>
<label for="first_name">First Name</label>
<input id="first_name" name="first_name" value="${contact.first_name}" style="width:18ch">
Expand All @@ -109,7 +140,7 @@ Try using the keyboard in the following demo and notice how keyboard focus is ma
}

function show(contact) {
return `<div id="contact_1">
return `<div id="contact_1" x-merge.transition>
<p><strong>First Name</strong>: ${contact.first_name}</p>
<p><strong>Last Name</strong>: ${contact.last_name}</p>
<p><strong>Email</strong>: ${contact.email}</p>
Expand Down
6 changes: 6 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ The `morph` option uses a DOM diffing algorithm to update HTML, it's a bit more

You can change the default merge strategy for all AJAX requests using the `mergeStrategy` global [configuration option](#configuration).

### View transitions & animations

You can animate transitions between different DOM states using the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API). This API is still in active development and is currently only supported in Chrome browsers. Alpine AJAX provides support for View Transitions and gracefully falls back to no animations if the API is not available in a browser.

To enable View Transitions on an element use the `x-merge.transition` modifier. When enabled in a supported browser, you should see content automatically animate as it is merged onto the page. You can customize any transition animation via CSS by following [Chrome's documentation for customizing transitions](https://developer.chrome.com/docs/web-platform/view-transitions/#simple-customization).

## x-focus

Add `x-focus` to a form or link to control keyboard focus after an AJAX request has completed. The `x-focus` attribute accepts an element `id` that will be focused. Consider the following markup, we'll assume that clicking the "Edit" link will load a form to change the listed email address:
Expand Down
29 changes: 23 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ function Ajax(Alpine) {
}
})

Alpine.directive('merge', (el, { expression }) => {
Alpine.directive('merge', (el, { modifiers, expression }) => {
mergeConfig.set(el, {
strategy: expression,
transition: transition(modifiers)
})
})

Expand Down Expand Up @@ -118,6 +119,10 @@ function parseModifiers(modifiers = []) {
}
}

function transition(modifiers = []) {
return globalConfig.transitions || modifiers.includes('transition')
}

function isLocalLink(el) {
return el.href &&
!el.hash &&
Expand Down Expand Up @@ -274,7 +279,7 @@ async function render(request, targets, el, config) {
if (!response.html) return

let fragment = document.createRange().createContextualFragment(response.html)
targets = targets.map(target => {
let renders = targets.map(async target => {
let template = fragment.getElementById(target.id)
let strategy = mergeConfig.get(target)?.strategy || globalConfig.mergeStrategy
if (!template) {
Expand All @@ -287,13 +292,13 @@ async function render(request, targets, el, config) {
}

if (response.ok) {
return merge(strategy, target, target.cloneNode(false))
return await merge(strategy, target, target.cloneNode(false))
}

throw new FailedResponseError(el)
}

let freshEl = merge(strategy, target, template)
let freshEl = await merge(strategy, target, template)

if (freshEl) {
freshEl.removeAttribute('aria-busy')
Expand All @@ -303,6 +308,7 @@ async function render(request, targets, el, config) {
return freshEl
})

targets = await Promise.all(renders)
let focus = el.getAttribute('x-focus')
if (focus) {
focusOn(document.getElementById(focus))
Expand All @@ -315,7 +321,7 @@ async function render(request, targets, el, config) {
return targets
}

function merge(strategy, from, to) {
async function merge(strategy, from, to) {
let strategies = {
before(from, to) {
from.before(...to.childNodes)
Expand Down Expand Up @@ -354,7 +360,18 @@ function merge(strategy, from, to) {
}
}

return strategies[strategy](from, to)
if (!mergeConfig.get(from)?.transition || !document.startViewTransition) {
return strategies[strategy](from, to)
}

let freshEl = null
let transition = document.startViewTransition(() => {
freshEl = strategies[strategy](from, to)
return Promise.resolve()
})
await transition.updateCallbackDone

return freshEl
}

function focusOn(el) {
Expand Down

0 comments on commit d88594d

Please sign in to comment.