-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Teleport does not work for elements controlled by vue itself #2015
Comments
I think the following approach should work correctly: createApp({
setup() {
const refDom = ref(null)
return () => [
// ref the DOM
h('div', { ref: refDom }),
h(Teleport, {
// set the target to refDom
to: refDom.value,
// disable teleport when refDom is not set
disabled: !refDom.value
}, h('h1', 'hcy'))
]
}
}).mount('#app') But that will still get warning messages:
Maybe we need to improve it. |
Interesting approach. But it becomes more complicated when you dont have access to the dom ref because the target is in some different component. How would you go about that? |
works well const useTele = () => {
const target = ref(null)
return () => target
}
const useThisTele = useTele()
createApp({
setup() {
return { target: useThisTele() }
},
template: `
<div>
<h1>App</h1>
<div id="dest" :ref="d => target = d"></div>
<parent-comp/>
</div>`
}).component('parent-comp', {
template: `
<div>
<child-comp/>
</div>`
}).component('child-comp', {
setup() {
return { target: useThisTele() }
},
template: `
<div>
<Teleport :to="target" :disabled="!target">
Hello From Portal
</Teleport>
</div>`
}).mount('#app') |
That still a bit fragile, however. When the target's parent gets removed from the DOM, the portal content is removed from the DOM with it - but the source component won't be informed about this and consequently, in it's vdom, it assumes that the elements still are in the DOM. That will likely result in update errors when the source component does update later. |
@unbyte so you basically pass (or import) useThisRef to the components which define it and which uses it as target. Is that correct? (so in easy terms you pass around a reference) |
@LinusBorg , this discussion might help, where the terminology "reparenting" is used: Generally the discussion (incl. linked gists+rfc) revolves around the use of keys/instances to map components to their destinations but two libraries also stick out there lately:
Has any similar discussion taken place in vue or its rfcs or is there even a vue lib that solves this problem already? |
@unbyte Should this problem should be fixed? |
Is there an accepted best practices approach to this issue that's different from the solution provided in https://stackoverflow.com/a/63665828/1764728? |
@kadiryazici the example @unbyte provided is a good solution. It maybe looks a bit complicated but it boils down to:
With the |
I've bundled @Fuzzyma solution in to a component: TeleportWrapper.vue
Then you can use it like this:
|
@patforg super useful thanks! |
Modified @patforg's component to use the mutation observer so it works even when the teleport wrapper component gets mounted before the target element is rendered: <template>
<Teleport :to="target" v-if="target" :disabled="!target || disabled">
<slot></slot>
</Teleport>
</template>
<script setup lang="ts">
const props = defineProps<{ to: string; disabled?: boolean }>()
const target = ref<Element>(null)
onMounted(() => {
const observer = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type !== 'childList') continue
const el = document.querySelector(props.to)
if (!el) continue
target.value = el
observer.disconnect()
break
}
})
observer.observe(document, { childList: true, subtree: true })
return () => observer.disconnect()
})
</script>
|
Wrapper hack works well. Thank you @patforg @Obapelumi One more alternative with pooling <script setup>
const target = ref(null)
const trySetTarget = () => {
try {
const element = document.querySelector(props.to)
if (!element) throw new Error('not ready')
target.value = element
} catch {
setTimeout(tryToGetTarget, 100)
}
}
onMounted(() => {
trySetTarget()
})
</script> Related issue that I initially searched for: |
I think we should have vue-safe-teleport built-in. Johnson Chu tweet reply. |
Here was my take: import type { VNodeRef } from 'vue'
import { Teleport, defineComponent, h, shallowRef } from 'vue'
export function createPortal() {
const target = shallowRef<VNodeRef>()
const Source = defineComponent((_props, context) => {
return () =>
h(
Teleport,
{
...context.attrs,
to: target.value,
disabled: !target.value,
},
context.slots
)
})
const Target = defineComponent((_props, context) => {
return () =>
h('div', {
...context.attrs,
ref: target,
})
})
return { Source, Target }
} Every time I want to create a portal, it's just a new file: import { createPortal } from '@/util'
const { Source, Target } = createPortal()
export const XyzSource = Source
export const XyzTarget = Target <XyzSource>
blah
</XyzSource> Else where: <template>
<div>
<XyzTarget />
<UnrelatedComponent />
</div>
</template> |
I offer my solution based on the suggestion above: import {defineComponent, DefineComponent, h, ShallowRef, shallowRef, Teleport, VNode} from 'vue'
type PortalKey = PropertyKey
const portals: Map<PortalKey, ShallowRef<VNode | undefined>> = new Map()
function getTargetRef(key: PortalKey): ShallowRef<VNode | undefined> {
if (!portals.has(key)) {
portals.set(key, shallowRef<VNode>())
}
return portals.get(key) as ShallowRef<VNode | undefined>
}
export function useSourcePortal(key: PortalKey): DefineComponent {
const target = getTargetRef(key)
const sourceComponent = defineComponent((_props, context) => {
return (): VNode =>
h(
Teleport,
{
...context.attrs,
to: target.value,
disabled: !target.value,
},
context.slots
)
})
return sourceComponent
}
export function useTargetPortal(key: PortalKey): DefineComponent {
const target = getTargetRef(key)
const targetComponent = defineComponent((_props, context) => {
return (): VNode =>
h('div', {
...context.attrs,
ref: target,
})
})
return targetComponent
} Use:Data from SourceComponent move to TargetComponent The key must be unique, ideally a symbol: Component A <template>
<SourceComponent>
TEST
</SourceComponent>
</template>
<script lang="ts" setup>
import { useSourcePortal } from "@/hooks/usePortal"
import { customPortalKey } from "@/constants/CustomPortal"
const SourceComponent = useSourcePortal(customPortalKey)
</script> Component B <template>
<TargetComponent/>
</template>
<script lang="ts" setup>
import { useSourcePortal } from "@/hooks/usePortal"
import { customPortalKey } from "@/constants/CustomPortal"
const TargetComponent = useTargetPortal(customPortalKey)
</script> |
A couple pathological behaviors can happen with the TabGroup that this commit attempts to resolve: - The `TabGroupItem` elements are always mounted *after* the `TabGroup` itself, and may be mounted much later than other reactive portions of the page. This means that the call to `selectTab` can fail because the `TabGroupItem` has not yet had a chance to call `registerTab` on the TabGroup ref. To solve this I've added a ref that tracks a failed selectTab. If that tab is registered after the select call, and no other tab selection has occurred, we automatically select the "pending" tab. For the AssetEditorTabs this also means that calls to `nextTick` are unnecessary since the missing tab will just be marked as pending and will be selected when the `registerTab` call succeeds. - Teleports into other components rendered by Vue (instead of to portions of the DOM not handled by Vue) are unreliable. Consider the following chain of events: 1. a TabGroup is rendered. 2. the TabGroup items are rendered, and the active one attempts to render itself via a teleport. 3. Before that happens, the TabGroup itself is re-rendered for some reason. 4. At that point in time, the teleport target might not be present in the DOM, and the Teleport will fail (I saw this often while building the AssetEditorTabs), crashing the frontend JS execution. While in some cases this happens because too many renders are occuring, it's still possible for it to happen via perfectly normal rendering paths, for example if you switch from one Asset to the next one before one of the TabGroupItem teleports has had the chance to teleport into the (now destroyed) TabGroup. `vue-safe-teleport` solves this by providing a teleport target that is queried for its ready state, and only mounting the `Teleport` element when the target is ready. See vuejs/core#2015 for details on this issue. This also adds the ability to mark a single tab as uncloseable if the tab group has closeable = true.
This commit created a container `div` outside of Vue app to store teleported modal components. It has to be outside of the Vue app, else it won't work because it has not been mounted yet. See more -> vuejs/core#2015
What problem does this feature solve?
Oftentimes I find myself writing components that semantically belong where they are but need to be displayed somewhere else in my app. Teleport is there to solve the problem - but you can only port outside of the app. Whenever I target an element which is rendered by vue itself it echos a warning that the element needs to be mounted first. Unfortunately, not every usecase can be rewritten in a way, that it falls into the simply modal-button category.
I opened a stackoverflow issue with a reproduction: https://stackoverflow.com/questions/63652288/does-vue-3-teleport-only-works-to-port-outside-vue
What does the proposed API look like?
Either have a teleport target component as in portal-vue or allow targeting ids of other components
The text was updated successfully, but these errors were encountered: