Skip to content

Commit

Permalink
fix(vue-3): set editor's appContext.provide to forward inject chain (#…
Browse files Browse the repository at this point in the history
…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`.
  • Loading branch information
romansp authored and nperez0111 committed Aug 6, 2024
1 parent a786810 commit da35cf5
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/odd-paws-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tiptap/vue-3": patch
---

Correctly set editor's appContext.provide to forward full inject chain
7 changes: 6 additions & 1 deletion demos/setup/vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
}
Original file line number Diff line number Diff line change
@@ -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>
144 changes: 144 additions & 0 deletions demos/src/Examples/InteractivityComponentProvideInject/Vue/Editor.vue
Original file line number Diff line number Diff line change
@@ -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 you’re used to, but enriched with node views.
</p>
<vue-component count="0"></vue-component>
<p>
Did you see that? That’s 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>
Original file line number Diff line number Diff line change
@@ -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)
},
})
Original file line number Diff line number Diff line change
@@ -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>
Empty file.
Original file line number Diff line number Diff line change
@@ -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])
})
})
})
Original file line number Diff line number Diff line change
@@ -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>
9 changes: 4 additions & 5 deletions packages/vue-3/src/EditorContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down

0 comments on commit da35cf5

Please sign in to comment.