瀏覽代碼

Change portal plugin to x-teleport and add to core (#2431)

* wip

* wip

* aip
Caleb Porzio 3 年之前
父節點
當前提交
c1b4574c54

+ 1 - 2
packages/alpinejs/src/directives.js

@@ -169,8 +169,7 @@ let directiveOrder = [
     'show',
     'if',
     DEFAULT,
-    'portal',
-    'portal-target',
+    'teleport',
     'element',
 ]
 

+ 1 - 0
packages/alpinejs/src/directives/index.js

@@ -1,4 +1,5 @@
 import './x-transition'
+import './x-teleport'
 import './x-ignore'
 import './x-effect'
 import './x-model'

+ 36 - 0
packages/alpinejs/src/directives/x-teleport.js

@@ -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())
+})

+ 1 - 1
packages/alpinejs/src/lifecycle.js

@@ -58,7 +58,7 @@ export function findClosest(el, callback) {
     if (callback(el)) return el
 
     // Support crawling up portals.
-    if (el._x_portal_back) el = el._x_portal_back
+    if (el._x_teleportBack) el = el._x_teleportBack
     
     if (! el.parentElement) return
 

+ 155 - 0
packages/docs/src/en/directives/teleport.md

@@ -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.

+ 0 - 220
packages/docs/src/en/plugins/portal.md

@@ -1,220 +0,0 @@
----
-order: 6
-title: Portal
-description: Send Alpine templates to other parts of the DOM
-graph_image: https://alpinejs.dev/social_portal.jpg
----
-
-# Portal Plugin
-
-Alpine's Portal 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.
-
-> Note: this plugin is currently in beta while it is being tested in the public. Be warned that it may change before being officially released.
-
-<a name="installation"></a>
-## Installation
-
-You can use this plugin by either including it from a `<script>` tag or installing it via NPM:
-
-### Via CDN
-
-You can include the CDN build of this plugin as a `<script>` tag, just make sure to include it BEFORE Alpine's core JS file.
-
-```alpine
-<!-- Alpine Plugins -->
-<script defer src="https://unpkg.com/@alpinejs/portal@3.6.1-beta.0/dist/cdn.min.js"></script>
-
-<!-- Alpine Core -->
-<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
-```
-
-### Via NPM
-
-You can install Portal from NPM for use inside your bundle like so:
-
-```shell
-npm install @alpinejs/portal
-```
-
-Then initialize it from your bundle:
-
-```js
-import Alpine from 'alpinejs'
-import portal from '@alpinejs/portal'
-
-Alpine.plugin(portal)
-
-...
-```
-
-<a name="usage"></a>
-## Usage
-
-Everytime you use a portal, you will need two different directives: `x-portal` and `x-portal-target`.
-
-By attaching `x-portal` to a `<template>` element, you are telling Alpine to send that DOM content to another template element that has a matching `x-portal-target` on it.
-
-Here's a contrived modal example using portals:
-
-```alpine
-<div x-data="{ open: false }">
-    <button @click="open = ! open">Toggle Modal</button>
-
-    <template x-portal="modals">
-        <div x-show="open">
-            Modal contents...
-        </div>
-    </template>
-
-    <div class="py-4">Some other content placed AFTER the modal markup.</div>
-</div>
-
-<template x-portal-target="modals"></template>
-```
-
-<!-- START_VERBATIM -->
-<div class="demo" x-ref="root">
-    <div x-data="{ open: false }">
-        <button @click="open = ! open">Toggle Modal</button>
-
-        <template x-portal="modals1">
-            <div x-show="open">
-                Modal contents...
-            </div>
-        </template>
-
-        <div class="py-4">Some other content...</div>
-    </div>
-
-    <template x-portal-target="modals1"></template>
-</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-portal="modals"` and takes that markup out of the page waiting until it finds an element with `x-portal-target="modals"` to insert it into.
-
-<a name="forwarding-events"></a>
-## Forwarding events
-
-Alpine tries it's best to make the experience of using portals seemless. Anything you would normally do in a template, you should be able to do inside a portal. Portal 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 portals, so if, for example, you trigger a "click" event from inside a portal, that event will bubble up the DOM tree as it normally would ignoring the fact that it is within a portal.
-
-To make this experience more seemless, you can "forward" events by simply registering event listeners on the portal's `<template>` element itself like so:
-
-```alpine
-<div x-data="{ open: false }">
-    <button @click="open = ! open">Toggle Modal</button>
-
-    <template x-portal="modals" @click="open = false">
-        <div x-show="open">
-            Modal contents...
-            (click to close)
-        </div>
-    </template>
-</div>
-
-<template x-portal-target="modals"></template>
-```
-
-<!-- START_VERBATIM -->
-<div class="demo" x-ref="root">
-    <div x-data="{ open: false }">
-        <button @click="open = ! open">Toggle Modal</button>
-
-        <template x-portal="modals2" @click="open = false">
-            <div x-show="open">
-                Modal contents...<br>
-                (click to close)
-            </div>
-        </template>
-    </div>
-
-    <template x-portal-target="modals2"></template>
-</div>
-<!-- END_VERBATIM -->
-
-Notice how we are now able to listen for events dispatched from within the portal from outside the portal itself?
-
-Alpine does this by looking for event listeners registered on `<template x-portal...` and stops those events from propogating past the `<template x-portal-target...` element. Then it creates a copy of that event and re-dispatches it from `<template x-portal`.
-
-<a name="nesting-portals"></a>
-## Nesting portals
-
-Portals are 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-portal="modals">
-        <div x-show="open">
-            Modal contents...
-            
-            <div x-data="{ open: false }">
-                <button @click="open = ! open">Toggle Nested Modal</button>
-
-                <template x-portal="modals">
-                    <div x-show="open">
-                        Nested modal contents...
-                    </div>
-                </template>
-            </div>
-        </div>
-    </template>
-</div>
-
-<template x-portal-target="modals"></template>
-```
-
-<!-- START_VERBATIM -->
-<div class="demo" x-ref="root">
-    <div x-data="{ open: false }">
-        <button @click="open = ! open">Toggle Modal</button>
-
-        <template x-portal="modals3">
-            <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-portal="modals3">
-                        <div class="pt-4" x-show="open">
-                            Nested modal contents...
-                        </div>
-                    </template>
-                </div>
-            </div>
-        </template>
-    </div>
-
-    <template x-portal-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.
-
-<a name="multiple-portals"></a>
-## Handling multiple portals
-
-Suppose you have multiple modals on a page, but a single `<template x-portal-target="modal">` element.
-
-Alpine automatically appends extra elements with `x-portal="modals"` at the target. No need for any extra syntax:
-
-```alpine
-<template x-portal="modals">
-    ...
-</template>
-
-<template x-portal="modals">
-    ...
-</template>
-
-...
-
-<template x-portal-target="modals"></template>
-```
-
-Now both of these modals will be rendered where `<template x-portal-target="modals">` lives.

+ 6 - 6
packages/morph/src/morph.js

@@ -372,11 +372,11 @@ class DomManager {
     }
 
     first() {
-        return this.portalTo(this.el[this.traversals['first']])
+        return this.teleportTo(this.el[this.traversals['first']])
     }
 
     next() {
-        return this.portalTo(this.portalBack(this.el[this.traversals['next']]))
+        return this.teleportTo(this.teleportBack(this.el[this.traversals['next']]))
     }
 
     before(insertee) {
@@ -391,15 +391,15 @@ class DomManager {
         this.el.appendChild(appendee); return appendee
     }
 
-    portalTo(el) {
+    teleportTo(el) {
         if (! el) return el
-        if (el._x_portal) return el._x_portal
+        if (el._x_teleport) return el._x_teleport
         return el
     }
 
-    portalBack(el) {
+    teleportBack(el) {
         if (! el) return el
-        if (el._x_portal_back) return el._x_portal_back
+        if (el._x_teleportBack) return el._x_teleportBack
         return el
     }
 }

+ 0 - 1
scripts/build.js

@@ -11,7 +11,6 @@ let brotliSize = require('brotli-size');
     'persist',
     'collapse',
     'morph',
-    'portal',
     'trap',
 ]).forEach(package => {
     if (! fs.existsSync(`./packages/${package}/dist`)) {

+ 22 - 36
tests/cypress/integration/plugins/portal.spec.js → tests/cypress/integration/directives/x-teleport.spec.js

@@ -1,18 +1,16 @@
 import { beEqualTo, beVisible, haveText, html, notBeVisible, test } from '../../utils'
 
-test('can use a portal',
+test('can use a x-teleport',
     [html`
         <div x-data="{ count: 1 }" id="a">
             <button @click="count++">Inc</button>
 
-            <template x-portal="foo">
+            <template x-teleport="#b">
                 <span x-text="count"></span>
             </template>
         </div>
 
-        <div id="b">
-            <template x-portal-target="foo"></template>
-        </div>
+        <div id="b"></div>
     `],
     ({ get }) => {
         get('#b span').should(haveText('1'))
@@ -21,23 +19,21 @@ test('can use a portal',
     },
 )
 
-test('can send multiple to a portal',
+test('can teleport multiple',
     [html`
         <div x-data="{ count: 1 }" id="a">
             <button @click="count++">Inc</button>
 
-            <template x-portal="foo">
+            <template x-teleport="#b">
                 <h1 x-text="count"></h1>
             </template>
 
-            <template x-portal="foo">
+            <template x-teleport="#b">
                 <h2 x-text="count + 1"></h2>
             </template>
         </div>
 
-        <div id="b">
-            <template x-portal-target="foo"></template>
-        </div>
+        <div id="b"></div>
     `],
     ({ get }) => {
         get('#b h1').should(haveText('1'))
@@ -48,19 +44,17 @@ test('can send multiple to a portal',
     },
 )
 
-test('portal targets forward events to portal source if listeners are attached',
+test('teleported targets forward events to teleport source if listeners are attached',
     [html`
         <div x-data="{ count: 1 }" id="a">
             <button @click="count++">Inc</button>
 
-            <template x-portal="foo" @click="count++">
+            <template x-teleport="#b" @click="count++">
                 <h1 x-text="count"></h1>
             </template>
         </div>
 
-        <div id="b">
-            <template x-portal-target="foo"></template>
-        </div>
+        <div id="b"></div>
     `],
     ({ get }) => {
         get('#b h1').should(haveText('1'))
@@ -71,19 +65,17 @@ test('portal targets forward events to portal source if listeners are attached',
     },
 )
 
-test('removing portal source removes portal target',
+test('removing teleport source removes teleported target',
     [html`
         <div x-data="{ count: 1 }" id="a">
             <button @click="$refs.template.remove()">Remove</button>
 
-            <template x-portal="foo" @click="count++" x-ref="template">
+            <template x-teleport="#b" @click="count++" x-ref="template">
                 <h1 x-text="count"></h1>
             </template>
         </div>
 
-        <div id="b">
-            <template x-portal-target="foo"></template>
-        </div>
+        <div id="b"></div>
     `],
     ({ get }) => {
         get('#b h1').should(beVisible())
@@ -92,19 +84,17 @@ test('removing portal source removes portal target',
     },
 )
 
-test('$refs inside portal can be accessed outside',
+test('$refs inside telport can be accessed outside',
     [html`
         <div x-data="{ count: 1 }" id="a">
             <button @click="$refs.count.remove()">Remove</button>
 
-            <template x-portal="foo">
+            <template x-teleport="#b">
                 <h1 x-text="count" x-ref="count"></h1>
             </template>
         </div>
 
-        <div id="b">
-            <template x-portal-target="foo"></template>
-        </div>
+        <div id="b"></div>
     `],
     ({ get }) => {
         get('#b h1').should(beVisible())
@@ -113,17 +103,15 @@ test('$refs inside portal can be accessed outside',
     },
 )
 
-test('$root is accessed outside portal',
+test('$root is accessed outside teleport',
     [html`
         <div x-data="{ count: 1 }" id="a">
-            <template x-portal="foo">
+            <template x-teleport="#b">
                 <h1 x-text="$root.id"></h1>
             </template>
         </div>
 
-        <div id="b">
-            <template x-portal-target="foo"></template>
-        </div>
+        <div id="b"></div>
     `],
     ({ get }) => {
         get('#b h1').should(beVisible())
@@ -131,19 +119,17 @@ test('$root is accessed outside portal',
     },
 )
 
-test('$id honors x-id outside portal',
+test('$id honors x-id outside telport',
     [html`
         <div x-data="{ count: 1 }" id="a" x-id="['foo']">
             <h1 x-text="$id('foo')"></h1>
 
-            <template x-portal="foo">
+            <template x-teleport="#b">
                 <h1 x-text="$id('foo')"></h1>
             </template>
         </div>
 
-        <div id="b">
-            <template x-portal-target="foo"></template>
-        </div>
+        <div id="b"></div>
     `],
     ({ get }) => {
         get('#b h1').should(haveText('foo-1'))

+ 3 - 5
tests/cypress/integration/plugins/morph.spec.js

@@ -98,7 +98,7 @@ test('can morph portals',
         <div x-data="{ count: 1 }" id="a">
             <button @click="count++">Inc</button>
 
-            <template x-portal="foo">
+            <template x-teleport="#b">
                 <div>
                     <h1 x-text="count"></h1>
                     <h2>hey</h2>
@@ -106,16 +106,14 @@ test('can morph portals',
             </template>
         </div>
 
-        <div id="b">
-            <template x-portal-target="foo"></template>
-        </div>
+        <div id="b"></div>
     `],
     ({ get }, reload, window, document) => {
         let toHtml = html`
         <div x-data="{ count: 1 }" id="a">
             <button @click="count++">Inc</button>
 
-            <template x-portal="foo">
+            <template x-teleport="#b">
                 <div>
                     <h1 x-text="count"></h1>
                     <h2>there</h2>

+ 0 - 1
tests/cypress/spec.html

@@ -8,7 +8,6 @@
 
     <script src="/../../packages/history/dist/cdn.js"></script>
     <script src="/../../packages/morph/dist/cdn.js"></script>
-    <script src="/../../packages/portal/dist/cdn.js"></script>
     <script src="/../../packages/persist/dist/cdn.js"></script>
     <script src="/../../packages/trap/dist/cdn.js"></script>
     <script src="/../../packages/intersect/dist/cdn.js"></script>