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
}
]
}
}
}
Related
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.
I have components like these
type TestComponentProps = {
title: string;
}
const TestComponent: React.FC<TestComponentProps> = ({
title,
}) => {
return <div>TestComponent: {title}</div>;
};
type TestComponent2Props = {
body: string;
}
const TestComponent2: React.FC<TestComponent2Props> = ({ body }) => {
return <div>TestComponent2: {body}</div>;
};
I would need an interface that would allow me to configure which component to render and get the props of that particular component
const dataToRender:Array<{
component: TestComponent | TestComponent2,
data: propsOf<component>
}> = [
{
component: TestComponent,
data: { title: '123' }
},
{
component: TestComponent2,
data: { body: 'lorem ipsum' }
}
];
Ideally I'd need to get the props of the particular component in a way "I want to render this component and I can only accept the correct props based on the props of that component"
You can do this, but not with plain typesciprt type annotations. In vue js for particular, for better typing you need to use defineComponent wrapper, that just return it argument (id) but with types.
In you case you can use this function.
function defineComponent<TProps, TModal extends React.ComponentType<TProps>>(x: {
component: TModal & React.ComponentType<TProps>
data: TProps
}) {
return x
}
And use it like so:
const dataToRender = [
defineComponent({
component: TestComponent,
data: { title: "123" },
}),
defineComponent({
component: TestComponent2,
data: { title: "lorem ipsum" }, // error here
}),
] as const
I am new to Typescript with vuex. I simply want to fetch user list from the backend. Put in the store. I declared custom user type
export interface User {
id: number;
firstName: string;
lastName: string;
email: string;
}
in my vuex.d.ts file, I declare store module like:
import { Store } from "vuex";
import { User } from "./customTypes/user";
declare module "#vue/runtime-core" {
interface State {
loading: boolean;
users: Array<User>;
}
interface ComponentCustomProperties {
$store: Store<State>;
}
}
in my store I fetch the users successfully and commit the state:
import { createStore } from "vuex";
import axios from "axios";
import { User, Response } from "./customTypes/user";
export default createStore({
state: {
users: [] as User[], // Type Assertion
loading: false,
},
mutations: {
SET_LOADING(state, status) {
state.loading = status;
},
SET_USERS(state, users) {
state.users = users;
},
},
actions: {
async fetchUsers({ commit }) {
commit("SET_LOADING", true);
const users: Response = await axios.get(
"http://localhost:8000/api/get-friends"
);
commit("SET_LOADING", false);
commit("SET_USERS", users.data);
},
},
getters: {
userList: (state) => {
return state.users;
},
loadingStatus: (state) => {
return state.loading;
},
},
});
I set the getters, I sense that I don't need to set getter for just returning state however this is the only way I could reach the data in my component. Please advise if there is a better way to do it. In my component I accessed the data like:
<div class="friends">
<h1 class="header">Friends</h1>
<loading v-if="loadingStatus" />
<div v-else>
<user-card v-for="user in userList" :user="user" :key="user.id" />
<pagination />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { mapGetters } from "vuex";
import { User } from "../store/customTypes/user";
=import UserCard from "../components/UserCard.vue";
import Loading from "../components/Loading.vue";
import Pagination from "../components/Pagination.vue";
export default defineComponent({
name: "Friends",
components: {
UserCard,
Loading,
Pagination,
},
static: {
visibleUsersPerPageCount: 10,
},
data() {
return {
users: [] as User[],
currentPage: 1,
pageCount: 0,
};
},
computed: {
...mapGetters(["loadingStatus", "userList"]),
},
mounted() {
this.$store.dispatch("fetchUsers");
this.paginate()
},
methods: {
paginate () {
// this.users = this.$store.state.users
console.log(this.$store.state.users)
console.log(this.userList)
}
}
});
</script>
Now when I get userList with getters, I successfully get the data and display in the template. However When I want to use it in the method, I can't access it when component is mounted. I need to paginate it in the methods. So I guess I need to wait until promise is resolved however I couldn't figure out how. I tried
this.$store.dispatch("fetchUsers").then((res) => console.log(res)) didn't work.
What I am doing wrong here?
An action is supposed to return a promise of undefined, it's incorrectly to use it like this.$store.dispatch("fetchUsers").then(res => ...).
The store needs to be accessed after dispatching an action:
this.$store.dispatch("fetchUsers").then(() => {
this.paginate();
});
I am able to fetch the list which we see on the landing screen of the site-content. However, when I am trying to fetch data by finding a particular item by its title I get an error CANNOT FIND LIST 'EmployeeList' IN THE URL.
I have built a React Web-part and here are the files and code
ListOfSprintStories.tsx
private _getListData(): Promise<ISPLists> {
return this.props.context.spHttpClient.get(this.props.context.pageContext.web.absoluteUrl + `/_api/web/lists/GetByTitle('EmployeeList')/Items`, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
return response.json();
});
}
private _renderListAsync(): void {
// Local environment
if (Environment.type === EnvironmentType.Local) {
this._getMockListData().then((response) => {
this.setState({ finalList: response.value });
});
}
else if (Environment.type == EnvironmentType.SharePoint ||
Environment.type == EnvironmentType.ClassicSharePoint) {
this._getListData()
.then((response) => {
console.log('======>', response.value)
this.setState({ finalList: response.value });
});
}
}
componentDidMount() {
this._renderListAsync()
}
IListOfSprintStoriesProps.ts
import { WebPartContext } from '#microsoft/sp-webpart-base';
export interface IListOfSprintStoriesProps {
description: string;
context: WebPartContext;
}
ListOfSprintStoriesWebPart.ts
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '#microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '#microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '#microsoft/sp-webpart-base';
import * as strings from 'ListOfSprintStoriesWebPartStrings';
import ListOfSprintStories from './components/ListOfSprintStories';
import { IListOfSprintStoriesProps } from './components/IListOfSprintStoriesProps';
import { WebPartContext } from '#microsoft/sp-webpart-base';
export interface IListOfSprintStoriesWebPartProps {
description: string;
context: WebPartContext;
}
export default class ListOfSprintStoriesWebPart extends BaseClientSideWebPart<IListOfSprintStoriesWebPartProps> {
public render(): void {
const element: React.ReactElement<IListOfSprintStoriesProps> = React.createElement(
ListOfSprintStories,
{
description: this.properties.description,
context: this.context
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}
I have followed the documentation.
https://learn.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/connect-to-sharepoint
I was able to fetch the complete site content list(Document library), but when I am trying to fetch a particular list using getByTitle('EmployeeList'), it fails.
Here is the error message:
{"error":{"code":"-1, System.ArgumentException","message":"List 'EmployeeList' does not exist at site with URL 'https://myTenant.sharepoint.com'."}}
Please Advice.
Issue fixed: The WebPart was working fine locally by fetching data from _getMockListData.
However, it wasn't when I was trying to test the WebPart on the https://MyOffice365.sharepoint.com/sites/**InCorrectSPSite**/_layouts/15/workbench.aspx
I later noticed that I was pointing to wrong SP-site.
EDIT on #Lesiak request:
Here is my getProducts call
// #api/shopifyProducts.ts
import Client from 'shopify-buy'
const client = Client.buildClient({
// TODO: add to dotenv
domain: 'some-domain.myshopify.com',
storefrontAccessToken: 'example-token-2597293846729587293875'
})
export const getProducts = async () => {
try {
const data = await client.product.fetchAll()
const products = await data.map((item) => {
return {
title: item.title,
description: item.description
// images: item.images
}
})
return products
} catch (error) {
throw new Error(`Product API fetch failed: ${error}`)
}
}
I have also refactored my component like this:
import React, { Component } from 'react'
import { getProducts } from '#api/shopifyProducts'
class TheListProducts extends Component<{}> {
constructor(props) {
super(props)
this.state = {
products: null
}
}
async componentDidMount() {
this.setState({
products: await getProducts()
})
console.log(this.state.products) ==> Error: Property 'products' does not exist on type 'Readonly<{}>'
}
render() {
return <p>Hey</p>
}
}
export default TheListProducts
Initial question:
Morning fellow developers,
I stumbled upon an issue I can't solve on my own even by doing an extensive research on the web. Since I am new to TS, I can see I don't understand in 100% what is really happening.
I have a component in React, where I async fetch content from Shopify content and I want to print it inside render function.
import React, { Component } from 'react'
import { getProducts } from '#api/shopifyProducts'
interface Product {
title: any
description: any
}
interface ListState {
products: {
[key: string]: Product | Function
}
}
class TheListProducts extends Component<{}, ListState> {
async componentDidMount() {
this.setState({
products: await getProducts()
})
}
render() {
return <p>{this.state.products}</p>
}
}
export default TheListProducts
I receive the following error:
(property) products: {
title: string;
description: string;
}[]
Type '{ title: string; description: string; }[]' is not assignable to type '{ [key: string]: Product; }'.
Index signature is missing in type '{ title: string; description: string; }[]'.ts(2322)
Here is how it looks in the browser:
I can read, and I think I understand the error, but have no clue whatsoever what to do about it.
If anyone could explain it to me in plain english it would be awesome.
Based on the web research I tried enhancing the Product interface like this:
interface Product {
title: string
description: string
[key: string]: string | number | undefined | Function
}
but it's like going in blind...
If you are using class component you should define the type of the state and props:
class App extends React.Component<MyProps, MyState> {
You can look here for more details