fix(vue-3): set editor's appContext.provide to forward inject chain (#5397)

Vue internally uses prototype chain to preserve injects across the entire component chain. Thus should avoid Object.assign or spread operator as it won't copy the prototype. All correct provides will be already present on `instance.provides`.
This commit is contained in:
Raman Paulau 2024-07-29 08:02:15 -07:00 committed by GitHub
parent a08bf85cf0
commit f7f644f7b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 318 additions and 6 deletions

View File

@ -0,0 +1,5 @@
---
"@tiptap/vue-3": patch
---
Correctly set editor's appContext.provide to forward full inject chain

View File

@ -14,7 +14,12 @@ export default function init(name: string, source: any) {
import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.vue`)
.then(module => {
createApp(module.default).mount('#app')
const app = createApp(module.default)
if (typeof module.configureApp === 'function') {
module.configureApp(app)
}
app.mount('#app')
debug()
})
}

View File

@ -0,0 +1,50 @@
<template>
<node-view-wrapper class="vue-component">
<label>Vue Component</label>
<ValidateInject />
</node-view-wrapper>
</template>
<script>
import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
import ValidateInject from './ValidateInject.vue'
export default {
components: {
NodeViewWrapper,
ValidateInject,
},
props: nodeViewProps,
}
</script>
<style lang="scss">
.tiptap {
/* Vue component */
.vue-component {
background-color: var(--purple-light);
border: 2px solid var(--purple);
border-radius: 0.5rem;
margin: 2rem 0;
position: relative;
label {
background-color: var(--purple);
border-radius: 0 0 0.5rem 0;
color: var(--white);
font-size: 0.75rem;
font-weight: bold;
padding: 0.25rem 0.5rem;
position: absolute;
top: 0;
}
.content {
margin-top: 1.5rem;
padding: 1rem;
}
}
}
</style>

View File

@ -0,0 +1,144 @@
<template>
<editor-content :editor="editor" />
</template>
<script>
import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
import VueComponent from './Extension.js'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
provide() {
return {
editorValue: 'editorValue',
}
},
mounted() {
this.editor = new Editor({
extensions: [
StarterKit,
VueComponent,
],
content: `
<p>
This is still the text editor youre used to, but enriched with node views.
</p>
<vue-component count="0"></vue-component>
<p>
Did you see that? Thats a Vue component. We are really living in the future.
</p>
`,
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.tiptap {
:first-child {
margin-top: 0;
}
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
}
</style>

View File

@ -0,0 +1,36 @@
import { mergeAttributes, Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3'
import Component from './Component.vue'
export default Node.create({
name: 'vueComponent',
group: 'block',
atom: true,
addAttributes() {
return {
count: {
default: 0,
},
}
},
parseHTML() {
return [
{
tag: 'vue-component',
},
]
},
renderHTML({ HTMLAttributes }) {
return ['vue-component', mergeAttributes(HTMLAttributes)]
},
addNodeView() {
return VueNodeViewRenderer(Component)
},
})

View File

@ -0,0 +1,20 @@
<template>
<div class="validate-inject">
<p>{{ globalValue }}</p>
<p>{{ appValue }} </p>
<p>{{ indexValue }}</p>
<p>{{ editorValue }}</p>
</div>
</template>
<script>
export default {
inject: ['appValue', 'indexValue', 'editorValue'],
}
</script>
<style lang="scss">
.validate-inject {
margin-top: 2rem;
}
</style>

View File

@ -0,0 +1,29 @@
context('/src/Examples/InteractivityComponentProvideInject/Vue/', () => {
beforeEach(() => {
cy.visit('/src/Examples/InteractivityComponentProvideInject/Vue/')
})
it('should have a working tiptap instance', () => {
cy.get('.tiptap').then(([{ editor }]) => {
// eslint-disable-next-line
expect(editor).to.not.be.null
})
})
it('should render a custom node', () => {
cy.get('.tiptap .vue-component').should('have.length', 1)
})
it('should have global and all injected values', () => {
const expectedTexts = [
'globalValue',
'appValue',
'indexValue',
'editorValue',
]
cy.get('.tiptap .vue-component p').each((p, index) => {
cy.wrap(p).should('have.text', expectedTexts[index])
})
})
})

View File

@ -0,0 +1,24 @@
<template>
<Editor />
</template>
<script>
import Editor from './Editor.vue'
export default {
components: {
Editor,
},
provide() {
return {
indexValue: 'indexValue',
}
},
}
export function configureApp(app) {
app.config.globalProperties.globalValue = 'globalValue'
app.provide('appValue', 'appValue')
}
</script>

View File

@ -46,11 +46,10 @@ export const EditorContent = defineComponent({
if (instance) {
editor.appContext = {
...instance.appContext,
provides: Object.assign(
instance.appContext.provides,
// @ts-ignore
instance.provides,
),
// Vue internally uses prototype chain to forward/shadow injects across the entire component chain
// so don't use object spread operator or 'Object.assign' and just set `provides` as is on editor's appContext
// @ts-expect-error forward instance's 'provides' into appContext
provides: instance.provides,
}
}