-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Change portal plugin to x-teleport and add to core (#2431)
* wip * wip * aip
- Loading branch information
1 parent
b1c0170
commit c1b4574
Showing
11 changed files
with
225 additions
and
272 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -169,8 +169,7 @@ let directiveOrder = [ | |
'show', | ||
'if', | ||
DEFAULT, | ||
'portal', | ||
'portal-target', | ||
'teleport', | ||
'element', | ||
] | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import './x-transition' | ||
import './x-teleport' | ||
import './x-ignore' | ||
import './x-effect' | ||
import './x-model' | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { directive } from "../directives" | ||
import { addInitSelector, initTree } from "../lifecycle" | ||
import { mutateDom } from "../mutation" | ||
import { addScopeToNode } from "../scope" | ||
|
||
directive('teleport', (el, { expression }, { cleanup }) => { | ||
let target = document.querySelector(expression) | ||
let clone = el.content.cloneNode(true).firstElementChild | ||
|
||
// Add reference to element on <template x-portal, and visa versa. | ||
el._x_teleport = clone | ||
clone._x_teleportBack = el | ||
|
||
// Forward event listeners: | ||
if (el._x_forwardEvents) { | ||
el._x_forwardEvents.forEach(eventName => { | ||
clone.addEventListener(eventName, e => { | ||
e.stopPropagation() | ||
|
||
el.dispatchEvent(new e.constructor(e.type, e)) | ||
}) | ||
}) | ||
} | ||
|
||
addScopeToNode(clone, {}, el) | ||
|
||
mutateDom(() => { | ||
target.appendChild(clone) | ||
|
||
initTree(clone) | ||
|
||
clone._x_ignore = true | ||
}) | ||
|
||
cleanup(() => clone.remove()) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
--- | ||
order: 12 | ||
title: teleport | ||
description: Send Alpine templates to other parts of the DOM | ||
graph_image: https://alpinejs.dev/social_teleport.jpg | ||
--- | ||
|
||
# Teleport Plugin | ||
|
||
Alpine's Teleport plugin allows you to transport part of your Alpine template to another part of the DOM on the page entirely. | ||
|
||
This is useful for things like modals (especially nesting them), where it's helpful to break out of the z-index of the current Alpine component. | ||
|
||
<a name="x-teleport"></a> | ||
## x-teleport | ||
|
||
By attaching `x-teleport` to a `<template>` element, you are telling Alpine to "append" that element to the provided selector. | ||
|
||
> The `x-template` selector can be any string you would normally pass into something like `document.querySelector` | ||
Here's a contrived modal example: | ||
|
||
```alpine | ||
<body> | ||
<div x-data="{ open: false }"> | ||
<button @click="open = ! open">Toggle Modal</button> | ||
<template x-teleport="body"> | ||
<div x-show="open"> | ||
Modal contents... | ||
</div> | ||
</template> | ||
</div> | ||
<div>Some other content placed AFTER the modal markup.</div> | ||
... | ||
</body> | ||
``` | ||
|
||
<!-- START_VERBATIM --> | ||
<div class="demo" x-ref="root" id="modal2"> | ||
<div x-data="{ open: false }"> | ||
<button @click="open = ! open">Toggle Modal</button> | ||
|
||
<template x-teleport="#modal2"> | ||
<div x-show="open"> | ||
Modal contents... | ||
</div> | ||
</template> | ||
|
||
</div> | ||
|
||
<div class="py-4">Some other content...</div> | ||
</div> | ||
<!-- END_VERBATIM --> | ||
|
||
Notice how when toggling the modal, the actual modal contents show up AFTER the "Some other content..." element? This is because when Alpine is initializing, it sees `x-teleport="body"` and appends and initializes that element to the provided element selector. | ||
|
||
<a name="forwarding-events"></a> | ||
## Forwarding events | ||
|
||
Alpine tries it's best to make the experience of telporting seemless. Anything you would normally do in a template, you should be able to do inside an `x-teleport` template. Teleported content can access the normal Alpine scope of the component as well as other features like `$refs`, `$root`, etc... | ||
|
||
However, native DOM events have no concept of teleportation, so if, for example, you trigger a "click" event from inside a teleported element, that event will bubble up the DOM tree as it normally would. | ||
|
||
To make this experience more seemless, you can "forward" events by simply registering event listeners on the `<template x-teleport...>` element itself like so: | ||
|
||
```alpine | ||
<div x-data="{ open: false }"> | ||
<button @click="open = ! open">Toggle Modal</button> | ||
<template x-teleport="body" @click="open = false"> | ||
<div x-show="open"> | ||
Modal contents... | ||
(click to close) | ||
</div> | ||
</template> | ||
</div> | ||
``` | ||
|
||
<!-- START_VERBATIM --> | ||
<div class="demo" x-ref="root" id="modal3"> | ||
<div x-data="{ open: false }"> | ||
<button @click="open = ! open">Toggle Modal</button> | ||
|
||
<template x-teleport="#modal3" @click="open = false"> | ||
<div x-show="open"> | ||
Modal contents... | ||
<div>(click to close)</div> | ||
</div> | ||
</template> | ||
</div> | ||
</div> | ||
<!-- END_VERBATIM --> | ||
|
||
Notice how we are now able to listen for events dispatched from within the teleported element from outside the `<template>` element itself? | ||
|
||
Alpine does this by looking for event listeners registered on `<template x-teleport...>` and stops those events from propogating past the live, teleported, DOM element. Then, it creates a copy of that event and re-dispatches it from `<template x-teleport...>`. | ||
|
||
<a name="nesting"></a> | ||
## Nesting | ||
|
||
Teleporting is especially helpful if you are trying to nest one modal within another. Alpine makes it simple to do so: | ||
|
||
```alpine | ||
<div x-data="{ open: false }"> | ||
<button @click="open = ! open">Toggle Modal</button> | ||
<template x-teleport="body"> | ||
<div x-show="open"> | ||
Modal contents... | ||
<div x-data="{ open: false }"> | ||
<button @click="open = ! open">Toggle Nested Modal</button> | ||
<template x-teleport="body"> | ||
<div x-show="open"> | ||
Nested modal contents... | ||
</div> | ||
</template> | ||
</div> | ||
</div> | ||
</template> | ||
</div> | ||
``` | ||
|
||
<!-- START_VERBATIM --> | ||
<div class="demo" x-ref="root" id="modal4"> | ||
<div x-data="{ open: false }"> | ||
<button @click="open = ! open">Toggle Modal</button> | ||
|
||
<template x-teleport="#modal4"> | ||
<div x-show="open"> | ||
<div class="py-4">Modal contents...</div> | ||
<div x-data="{ open: false }"> | ||
<button @click="open = ! open">Toggle Nested Modal</button> | ||
|
||
<template x-teleport="#modal4"> | ||
<div class="pt-4" x-show="open"> | ||
Nested modal contents... | ||
</div> | ||
</template> | ||
</div> | ||
</div> | ||
</template> | ||
</div> | ||
|
||
<template x-teleport-target="modals3"></template> | ||
</div> | ||
<!-- END_VERBATIM --> | ||
|
||
After toggling "on" both modals, they are authored as children, but will be rendered as sibling elements on the page, not within one another. |
Oops, something went wrong.