Skip to content

Commit d88594d

Browse files
authored
Add View Transitions API support (#18)
1 parent 9836079 commit d88594d

File tree

3 files changed

+62
-8
lines changed

3 files changed

+62
-8
lines changed

docs/examples/inline-edit.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,37 @@ Lastly, we'll update the edit form and the "Cancel" link so that focus is return
6969

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

72+
<style>
73+
@keyframes fade-in {
74+
from { opacity: 0; }
75+
}
76+
77+
@keyframes fade-out {
78+
to { opacity: 0; }
79+
}
80+
81+
@keyframes slide-from-right {
82+
from { transform: translateX(25%); }
83+
}
84+
85+
@keyframes slide-to-left {
86+
to { transform: translateX(25%); }
87+
}
88+
89+
/* define animations for the old and new content */
90+
::view-transition-old(slide-fade) {
91+
animation: 200ms ease 150ms both fade-out, 200ms ease 150ms both slide-to-left;
92+
}
93+
::view-transition-new(slide-fade) {
94+
animation: 300ms ease 50ms both fade-in, 300ms ease 50ms both slide-from-right;
95+
}
96+
97+
form {
98+
background: #fff;
99+
view-transition-name: slide-fade;
100+
}
101+
</style>
102+
72103

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

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

111142
function show(contact) {
112-
return `<div id="contact_1">
143+
return `<div id="contact_1" x-merge.transition>
113144
<p><strong>First Name</strong>: ${contact.first_name}</p>
114145
<p><strong>Last Name</strong>: ${contact.last_name}</p>
115146
<p><strong>Email</strong>: ${contact.email}</p>

docs/reference.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ The `morph` option uses a DOM diffing algorithm to update HTML, it's a bit more
188188

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

191+
### View transitions & animations
192+
193+
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.
194+
195+
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).
196+
191197
## x-focus
192198

193199
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:

src/index.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ function Ajax(Alpine) {
3232
}
3333
})
3434

35-
Alpine.directive('merge', (el, { expression }) => {
35+
Alpine.directive('merge', (el, { modifiers, expression }) => {
3636
mergeConfig.set(el, {
3737
strategy: expression,
38+
transition: transition(modifiers)
3839
})
3940
})
4041

@@ -118,6 +119,10 @@ function parseModifiers(modifiers = []) {
118119
}
119120
}
120121

122+
function transition(modifiers = []) {
123+
return globalConfig.transitions || modifiers.includes('transition')
124+
}
125+
121126
function isLocalLink(el) {
122127
return el.href &&
123128
!el.hash &&
@@ -274,7 +279,7 @@ async function render(request, targets, el, config) {
274279
if (!response.html) return
275280

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

289294
if (response.ok) {
290-
return merge(strategy, target, target.cloneNode(false))
295+
return await merge(strategy, target, target.cloneNode(false))
291296
}
292297

293298
throw new FailedResponseError(el)
294299
}
295300

296-
let freshEl = merge(strategy, target, template)
301+
let freshEl = await merge(strategy, target, template)
297302

298303
if (freshEl) {
299304
freshEl.removeAttribute('aria-busy')
@@ -303,6 +308,7 @@ async function render(request, targets, el, config) {
303308
return freshEl
304309
})
305310

311+
targets = await Promise.all(renders)
306312
let focus = el.getAttribute('x-focus')
307313
if (focus) {
308314
focusOn(document.getElementById(focus))
@@ -315,7 +321,7 @@ async function render(request, targets, el, config) {
315321
return targets
316322
}
317323

318-
function merge(strategy, from, to) {
324+
async function merge(strategy, from, to) {
319325
let strategies = {
320326
before(from, to) {
321327
from.before(...to.childNodes)
@@ -354,7 +360,18 @@ function merge(strategy, from, to) {
354360
}
355361
}
356362

357-
return strategies[strategy](from, to)
363+
if (!mergeConfig.get(from)?.transition || !document.startViewTransition) {
364+
return strategies[strategy](from, to)
365+
}
366+
367+
let freshEl = null
368+
let transition = document.startViewTransition(() => {
369+
freshEl = strategies[strategy](from, to)
370+
return Promise.resolve()
371+
})
372+
await transition.updateCallbackDone
373+
374+
return freshEl
358375
}
359376

360377
function focusOn(el) {

0 commit comments

Comments
 (0)