How to pass data to custom Vue Extension in TipTap? - javascript

I'm trying to pass data to a custom vue component that gets rendered inside the tiptap editor. I can pass default properties but assigning reactive values to it doesn't seem to work.
This is the tiptap-node-extension.js file:
import {Node, mergeAttributes} from '#tiptap/core'
import {VueNodeViewRenderer} from '#tiptap/vue-3'
import Component from '#/views/components/vue-component.vue'
export default Node.create({
parseHTML() {
return [{ tag: 'vue-component' }]
},
renderHTML({ HTMLAttributes }) {
return ['vue-component', mergeAttributes(HTMLAttributes)]
},
addNodeView() {
return VueNodeViewRenderer(Component)
},
})
the script setup portion of the editor component:
<script setup>
import {useEditor, EditorContent, BubbleMenu} from '#tiptap/vue-3'
import StarterKit from '#tiptap/starter-kit'
import {Underline} from "#tiptap/extension-underline";
import {TextAlign} from "#tiptap/extension-text-align";
import {Link} from "#tiptap/extension-link";
import VueComponent from '#/js/tiptap-node-extension.js'
const editor = useEditor({
extensions: [
StarterKit,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
Underline,
Link,
VueComponent.extend({
name: 'vueComponent',
group: 'block',
draggable: true,
addAttributes() {
return {
src: {
default: '123',
}
}
},
}
),
],
content: props.modelValue,
onUpdate: ({ editor }) => {
emit('update:modelValue', editor.getHTML())
},
editable: props.locked ? false : store.admin
})
const sendDataToExtension = async (editor, event) => {
// Triggered upon event
...
state.src = '123'
editor.chain().focus().insertContent('<vue-component/>').run()
}
</script>
and the vue component:
<script setup>
import {NodeViewWrapper} from '#tiptap/vue-3'
const props = defineProps({
node: {
type: Object,
required: true
},
updateAttributes: {
type: Function,
required: true,
}
})
</script>
<template>
<node-view-wrapper class="vue-component" data-drag-handle="">
<p>{{ node.attrs.src }}</p>
</node-view-wrapper>
</template>
The default of src gets through but when I try to assign a reactive object (that gets created after mounting the editor component) it ends up being undefined.
This works:
src: {
default: '123'
}
but this doesn't:
...
src: {
default: state.src
}
...
const sendDataToExtension = async (editor, event) => {
// triggered upon event
...
state.src = '123'
editor.chain().focus().insertContent('<vue-component/>').run()
}
How do I send data to the vue component that is created after mounting editor?
Attempt:
editor.chain().focus().insertContent('<vue-component/>', {src: state.src}).run()

First I would say that I would recommend creating a purpose built extension, instead of having the general VueComponent that you have now. If you extend more based on that extension you will have several extension competing for the tag. Move all code that you set in extend to the actual extentions, you can set any tag-name you want.
Now to what I believe is the problem here: insertContent look like this:
insertContent: (value: Content, options?: {
parseOptions?: ParseOptions;
updateSelection?: boolean;
})
Content is declared as
export declare type Content = HTMLContent | JSONContent | JSONContent[] | null;
export declare type HTMLContent = string;
export declare type JSONContent = {
type?: string;
attrs?: Record<string, any>;
content?: JSONContent[];
marks?: {
type: string;
attrs?: Record<string, any>;
[key: string]: any;
}[];
text?: string;
[key: string]: any;
};
In your case you will have to add the src attribute to your html string, however I would recommend using the JSONContent type in your case then:
editor.chain().focus().insertContent({type: "vueComponent", attrs:{src: state.src}}).run()
Here the type is the name that you set of the component.
Hope this makes sense, the documentation on tiptap is kind of good as well https://tiptap.dev/guide/custom-extensions/#attributes
Let me know if you have further issues.

Related

Stencil camelCase #Prop is not working in Storybook

I am writing a story in Storybook for the Stencil component. Seems like all props work as expected if they are all lowercase. However, if the prop is camelCase, it is not working. I tried both camelCase and 'dash-case' .nothing works.
Stencil component:
import {Component, Prop, h} from '#stencil/core';
#Component({
tag: 'text-input',
shadow: true,
})
export class TextInput {
#Prop() label: string;
#Prop() inputId: string;
render() {
return (
<div class="text-input">
<label htmlFor={this.inputId}>{this.label}</label>
<input type="text" id={this.inputId} />
</div>
)
}
}
Storybook story:
export default {
title: 'Components/Text Input',
argTypes: {
label: {control: 'text'},
inputid : {control: 'text'},
}
};
const Template = (args) => {
let tag = `<text-input label="${args.label}" inputId="${args.inputid}"></text-input>`;
return tag
};
export const Default = Template.bind({});
Default.args = {
label: 'First name:',
inputid: 'Input1'
};
inputId is the one that doesn't work while label is fine. Renaming inputId="${args.inputid}" to input-id="${args.inputid}"is not helping. It only works if I rename the Stencil prop to the all-lowercase inputid.
How to solve this issue? How can I keep Stencil props camelCase and make it work in Storybook?
Might be something to do with the casing of your argTypes and Default args, i.e. "inputid" is all lowercase. Maybe try the below?
export default {
title: 'Components/Text Input',
argTypes: {
label: {control: 'text'},
inputId : {control: 'text'},
}
};
const Template = (args) => {
let tag = `<text-input label="${args.label}" inputId="${args.inputId}"></text-input>`;
return tag
};
export const Default = Template.bind({});
Default.args = {
label: 'First name:',
inputId: 'Input1'
};
I would also try and create your template without string concatenation. EG:
const Template = ({ label, inputId }) => {
const element = document.createElement('text-input');
element.label = label;
element.inputId = inputId;
return rootEl;
};
This will make it easier when you pass in params of another type than string

Nuxt.js cannot update head() properties from asyncData()

I created a server-side rendered Vue.js blog using Nuxt.js with Typescript and Ghost but I'm having some issues to update html metatag using data from asyncData().
From Nuxt.js documentation I know that asyncData() is called every time before loading page components and merges with component data.
I'm getting this error:
Property 'title' does not exist on type '{ asyncData({ app, params }: Context): Promise<{ title: string | undefined; excerpt: string | undefined; feature_image: Nullable | undefined; html: Nullable | undefined; }>; head(): any; }'.
This is my code:
<script lang="ts">
import { Context } from '#nuxt/types'
import { PostOrPage } from 'tryghost__content-api'
export default {
async asyncData ({ app, params }: Context) {
const post: PostOrPage = await app.$ghost.posts.read(
{
slug: params.slug
},
{ include: 'tags' }
)
return {
title: post.title,
excerpt: post.excerpt,
feature_image: post.feature_image,
html: post.html
}
},
head () {
return {
title: this.title,
meta: [
{
hid: 'description',
name: 'description',
content: this.excerpt
}
]
}
}
}
</script>
I already tried some solutions like using data() to set a default value for each item but nothing. Do you have any suggestion?
Without a typescript plugin like nuxt-property-decorator you won't have Typescript support for nuxt (either way, type checking and autocomplete still won't work).
That's why asyncData & fetch should be in Component options.
#Component({
asyncData (ctx) {
...
}
})
export default class YourClass extends Vue {
...
}
instead of
#Component
export default class YourClass extends Vue {
asyncData (ctx) {
...
}
}
If you still want to use asyncData() inside of your component class instead of setting the option, see this working example using the npm module nuxt-property-decorator.
Here is the working code after implementing the suggestion from #nonNumericalFloat :
import { Component, Vue } from 'nuxt-property-decorator'
import { Context } from '#nuxt/types'
import { PostOrPage } from 'tryghost__content-api'
import Title from '~/components/Title.vue'
#Component({
components: {
Title
}
})
export default class Page extends Vue {
post!: PostOrPage
async asyncData ({ app, params }: Context) {
const post: PostOrPage = await app.$ghost.posts.read(
{
slug: params.slug
},
{ include: 'tags' }
)
return {
post
}
}
head () {
return {
title: this.post.title,
meta: [
{
hid: 'description',
name: 'description',
content: this.post.excerpt
}
]
}
}
}

Extending react-select v5 in Storybook props results in unknown types

I'm looking to extend the typescript definition for react-select such that I can pass custom ...args in Storybook to the component.
Select.data
export interface FishOption {
readonly value: string;
readonly label: string;
readonly isFixed?: boolean;
readonly isDisabled?: boolean;
}
export const fishOptions: readonly FishOption[] = [
{ value: 'Maguro', label: 'Maguro' },
{ value: 'Unagi', label: 'Unagi' },
{ value: 'Shishamo', label: 'Shishamo' },
{ value: 'Toro', label: 'Toro' },
{ value: 'Iwashi', label: 'Iwashi' }
];
node_modules/react-select/dist/declarations/src/index.d.ts
export type { StateManagerProps as Props } from './useStateManager';
Select.stories.tsx
import { Story, Meta } from '#storybook/react';
import { useState } from 'react';
import Select, { Props } from 'react-select';
import { FishOption, fishOptions } from './Select.data';
export default {
component: Select,
title: 'Components/Select',
argTypes: { onChange: { action: '' } }
} as Meta;
// Extended interface.
interface ExtendedProps extends Props {
someProp: boolean;
}
// Use extended interface.
const Template: Story<ExtendedProps> = args => {
const [value, setValue] = useState<FishOption>();
const handleChange = (newValue: FishOption) => {
setValue(newValue);
};
return <Select defaultValue={value} onChange={handleChange} {...args} />;
};
export const Single = Template.bind({});
Single.args = {
someProp: true,
options: fishOptions
};
I get the following typescript error for my onChange handler:
Type '(newValue: FishOption) => void' is not assignable to type '(newValue: unknown, actionMeta: ActionMeta<unknown>) => void'.
Types of parameters 'newValue' and 'newValue' are incompatible.
Type 'unknown' is not assignable to type 'FishOption'.ts(2322)
Looking at the other properties, the other prop (defaultValue) typings is also set to unknown as well. I'm not sure if i'm importing the types wrongly or some sort of erasure? is happening.
How can I extend react-select v5 props such that I can use them in Storybook or otherwise?
Related link: To extend the react-select interface property in typescript
CodeSandbox: https://codesandbox.io/s/interesting-hypatia-2vv6p (No storybook but observe ExtendedSelect to see that the props are unknown)

can't 'convert' typescript definition in javascript

I am trying to rewrite this Apollo typescript repository to javascript.
I try to learn typescript but there is one thing called 'type casting' or 'type assertion' which makes me confuse.
let me explain it with code:
//cache.tsx
import { InMemoryCache, ReactiveVar, makeVar } from "#apollo/client";
import { Todos } from "./models/Todos";
import { VisibilityFilter, VisibilityFilters } from "./models/VisibilityFilter";
export const cache...{
...
...
}
export const todosVar: ReactiveVar<Todos> = makeVar<Todos>( //how i can 'convert' this type caster value to vanilla js?
todosInitialValue
);
export const visibilityFilterVar = makeVar<VisibilityFilter>( //how i can 'convert' this type caster value to vanilla js?
VisibilityFilters.SHOW_ALL
)
other 2 files which are used by this cache.tsx are:
//todos.tsx
export interface Todo {
text: string;
completed: boolean;
id: number
}
export type Todos = Todo[];
and
VisibilityFilter.tsx
export type VisibilityFilter = {
id: string;
displayName: string;
}
export const VisibilityFilters: { [filter: string]: VisibilityFilter } = {
SHOW_ALL: {
id: "show_all",
displayName: "All"
},
SHOW_COMPLETED: {
id: "show_completed",
displayName: "Completed"
},
SHOW_ACTIVE: {
id: "show_active",
displayName: "Active"
}
}
How can I avoid typescript type checking in this situation and more important, how can I use ReactiveVar and makeVar imports properly?
Just remove the generic like this:
From:
makeVar<Todos>(
To:
makeVar(
Change this:
export const todosVar: ReactiveVar<Todos> = makeVar<Todos>( //how i can 'convert' this type caster value to vanilla js?
todosInitialValue
);
export const visibilityFilterVar = makeVar<VisibilityFilter>( //how i can 'convert' this type caster value to vanilla js?
VisibilityFilters.SHOW_ALL
)
To this:
export const todosVar = makeVar(todosInitialValue);
export const visibilityFilterVar = makeVar(VisibilityFilters.SHOW_ALL);
How to avoid typescript checking - replace your extensions from .ts and .tsx to .js and .jsx respectively.

ngrx dealing with nested array in object

I am learning the redux pattern and using ngrx with angular 2. I am creating a sample blog site which has following shape.
export interface BlogContent {
id: string;
header: string;
tags: string[];
title: string;
actualContent: ActualContent[];
}
and my reducer and actions are as following:
import { ActionReducer, Action } from '#ngrx/store';
import * as _ from 'lodash';
export interface ActualContent {
id: string;
type: string;
data: string;
}
export interface BlogContent {
id: string;
header: string;
tags: string[];
title: string;
actualContent: ActualContent[];
}
export const initialState: BlogContent = {
id: '',
header: '',
tags: [],
title: '',
actualContent: [],
};
export const ADD_OPERATION = 'ADD_OPERATION';
export const REMOVE_OPERATION = 'REMOVE_OPERATION';
export const RESET_OPERATION = 'RESET_OPERATION';
export const ADD_IMAGE_ID = 'ADD_IMAGE_ID';
export const ADD_FULL_BLOG = 'ADD_FULL_BLOG';
export const ADD_BLOG_CONTENT_OPERATION = 'ADD_BLOG_CONTENT_OPERATION';
export const ADD_BLOG_TAG_OPERATION = 'ADD_BLOG_TAG_OPERATION';
export const blogContent: ActionReducer<BlogContent> = (state: BlogContent= initialState, action: Action ) => {
switch (action.type) {
case ADD_OPERATION :
return Object.assign({}, state, action.payload );
case ADD_BLOG_CONTENT_OPERATION :
return Object.assign({}, state, { actualContent: [...state.actualContent, action.payload]});
case ADD_BLOG_TAG_OPERATION :
return Object.assign({}, state, { tags: [...state.tags, action.payload]});
case REMOVE_OPERATION :
return Object.assign({}, state, { actualContent: state.actualContent.filter((blog) => blog.id !== action.payload.id) });
case ADD_IMAGE_ID : {
let index = _.findIndex(state.actualContent, {id: action.payload.id});
console.log(index);
if ( index >= 0 ) {
return Object.assign({}, state, {
actualContent : [
...state.actualContent.slice(0, index),
action.payload,
...state.actualContent.slice(index + 1)
]
});
}
return state;
}
default :
return state;
}
};
and this is working fine but i am not sure if its the right approach or should i somehow separate the ActualContent into its own reducer and actions and then merge them.
Sorry if this post does not belong here and you can guide me where should put this post and i will remove it from here. Thanks in advance.
P.S. I have done some research but couldnt find any article that has complex nested objects so that i can refer. Please add any useful blog links of ngrx or related topic which can help me out.
Instead of having a nested structure
export interface BlogContent {
id: string;
header: string;
tags: string[];
title: string;
actualContent: ActualContent[]; <------ NESTED
}
You should have a normalized state.
For example here you should have something like :
// this should be into your store
export interface BlogContents {
byId: { [key: string]: BlogContent };
allIds: string[];
}
// this is made to type the objects you'll find in the byId
export interface BlogContent {
id: string;
// ...
actualContentIds: string[];
}
// ----------------------------------------------------------
// this should be into your store
export interface ActualContents {
byId: { [key: string]: ActualContent };
allIds: string[];
}
export interface ActualContent {
id: string;
// ...
}
So if you try to populate your store it'd look like that :
const blogContentsState: BlogContents = {
byId: {
blogContentId0: {
id: 'idBlogContent0',
// ...
actualContentIds: ['actualContentId0', 'actualContentId1', 'actualContentId2']
}
},
allIds: ['blogContentId0']
};
const actualContentState: ActualContents = {
byId: {
actualContentId0: {
id: 'actualContentId0',
// ...
},
actualContentId1: {
id: 'actualContentId1',
// ...
},
actualContentId2: {
id: 'actualContentId2',
// ...
}
},
allIds: ['actualContentId0', 'actualContentId1', 'actualContentId2']
};
In your logic or view (for example with Angular), you need your nested structure so you can iterate over your array and thus, you don't want to iterate on a string array of IDs. Instead you'd like actualContent: ActualContent[];.
For that, you create a selector. Every time your store change, your selector will kicks in and generate a new "view" of your raw data.
// assuming that you can blogContentsState and actualContentsState from your store
const getBlogContents = (blogContentsState, actualContentsState) =>
blogContentsState
.allIds
.map(blogContentId => ({
...blogContentsState.byId[blogContentId],
actualContent: blogContentsState
.byId[blogContentId]
.actualContentIds
.map(actualContentId => actualContentsState.byId[actualContentId])
}));
I know it can be a lot to process at the beginning and I invite you to read the official doc about selectors and normalized state
As you're learning ngrx, you might want to take a look into a small project I've made called Pizza-Sync. Code source is on Github. It's a project were I've done something like that to demo :). (You should also definitely install the ReduxDevTools app to see how is the store).
I made a small video focus only on Redux with Pizza-Sync if you're interested : https://youtu.be/I28m9lwp15Y

Categories