I'm quite new with Vue, and I'm trying to lazy load a Component, I'm getting this error which I don't understand, there should probably be a syntax error.
Here's the code:
<template>
<div class="container">
<h1>Async testing</h1>
<button #click="showModal=!showModal">show modal</button>
<Modal v-if=showModal />
</div>
</template>
<script>
import { defineAsyncComponent, ref } from 'vue';
export default {
components: {
Modal: () => defineAsyncComponent(
() => import('#/components/Modal.vue')
)
},
setup(){
const showModal = ref(false);
return {
showModal
}
}
}
</script>
The Modal Comp is just a simple h2 and a p with a red border (I already tried the same code without lazy load, and it works fine).
Here's the error:
By wrapping defineAsyncComponent() in a function, you're incorrectly declaring the Modal component as a function:
export default {
components: {
/* 👇 don't do this */
Modal: () => defineAsyncComponent(() => import('#/components/Modal.vue'))
},
}
The simplest solution is to remove the function wrapper:
export default {
components: {
Modal: defineAsyncComponent(() => import('#/components/Modal.vue'))
},
}
demo
Looks like you're mixing composition and options API
Try the following in the <script>
import { defineAsyncComponent, ref } from 'vue';
export default {
setup() {
const Modal = defineAsyncComponent(() => import('#/components/Modal.vue'));
const showModal = ref(false);
return {
showModal,
Modal
}
}
}
Related
I am trying to import grapesjs in a nextjs project and I get the error TypeError: Cannot read properties of null (reading 'querySelector')
This seems to be that grapesjs wants to target the "#gjs" container referenced through it's id in order to load the editor inside, and it cannot find the corresponding element as the DOM is not rendered yet.
This is the code in my Editor.js component
import React, { useEffect, useState } from "react";
import grapesjs from "grapesjs";
const Editor = () => {
const [editor, setEditor] = useState(null);
useEffect(() => {
const editor = grapesjs.init({
container: "#gjs",
});
setEditor(editor);
}, []);
return (
<>
<div id="gjs"></div>
</>
);
};
export default Editor;
This is how I try to render the Editor component in the corresponding page for "/editor" route
import { getSession } from "next-auth/react";
import "../i18n/config/config";
import "grapesjs/dist/css/grapes.min.css";
import dynamic from "next/dynamic";
import Editor from "../features/Editor/components/Editor";
// const EditorComponent = dynamic(
// () => import("../features/Editor/components/Editor"),
// {
// ssr: false,
// }
// );
export default function Home() {
return <Editor />;
}
export async function getServerSideProps(context) {
const session = await getSession(context);
return {
props: {
session,
},
};
}
As you can see from the commented section, I have tried to dynamically import the editor component as I have seen this as a fix for alot of issues where an element could not be found because the DOM was not yet loaded, but it does not seem to work for me.
Edit: Adding <script src="//unpkg.com/grapesjs"></script> before the component to be rendered either in Editor.js component or in editor.js page while removing the grapejs import statement from Editor.js component import grapesjs from "grapesjs" allows the application to run but I still get the error in the console.
import { getSession } from "next-auth/react";
import "../i18n/config/config";
import "grapesjs/dist/css/grapes.min.css";
import dynamic from "next/dynamic";
import Editor from "../features/Editor/components/Editor";
// const EditorComponent = dynamic(
// () => import("../features/Editor/components/Editor"),
// {
// ssr: false,
// }
// );
export default function Home() {
return (
<>
<script src="//unpkg.com/grapesjs"></script>
<Editor />
</>
);
}
export async function getServerSideProps(context) {
const session = await getSession(context);
return {
props: {
session,
},
};
}
Edit: Screenshot of the produced error
Because it is wrong type in useState. U can try this.
const Editor = () => {
const [editor, setEditor] = useState<Record<string, any> | null>(null);
useEffect(() => {
const editor = grapesjs.init({
container: "#gjs",
});
setEditor(editor);
}, []);
return (
<>
<div id="gjs"></div>
</>
);
};
export default Editor;
Search the library sources..
Using vue-test-utils to test the component using pinia, I need to modify the value of the state stored in pinia, but I have tried many methods to no avail. The original component and store files are as follows.
// HelloWorld.vue
<template>
<h1>{{ title }}</h1>
</template>
<script>
import { useTestStore } from "#/stores/test";
import { mapState } from "pinia";
export default {
name: "HelloWorld",
computed: {
...mapState(useTestStore, ["title"]),
},
};
</script>
// #/stores/test.js
import { defineStore } from "pinia";
export const useTestStore = defineStore("test", {
state: () => {
return { title: "hhhhh" };
},
});
The following methods have been tried.
Import the store used within the component to the test code and make changes directly, but the changes cannot affect the component.
// test.spec.js
import { mount } from "#vue/test-utils";
import { createTestingPinia } from "#pinia/testing";
import HelloWorld from "#/components/HelloWorld.vue";
import { useTestStore } from "#/stores/test";
test("pinia in component test", () => {
const wrapper = mount(HelloWorld, {
global: {
plugins: [createTestingPinia()],
},
});
const store = useTestStore();
store.title = "xxxxx";
console.log(wrapper.text()) //"hhhhh";
});
Using the initialState in an attempt to overwrite the contents of the original store, but again without any effect.
// test.spec.js
import { mount } from "#vue/test-utils";
import { createTestingPinia } from "#pinia/testing";
import HelloWorld from "#/components/HelloWorld.vue";
test("pinia in component test", () => {
const wrapper = mount(HelloWorld, {
global: {
plugins: [createTestingPinia({ initialState: { title: "xxxxx" } })],
},
});
console.log(wrapper.text()) //"hhhhh";
});
Modify the TestingPinia object passed to global.plugins in the test code, but again has no effect.
// test.spec.js
import { mount } from "#vue/test-utils";
import { createTestingPinia } from "#pinia/testing";
import HelloWorld from "#/components/HelloWorld.vue";
test("pinia in component test", () => {
const pinia = createTestingPinia();
pinia.state.value.title = "xxxxx";
const wrapper = mount(HelloWorld, {
global: {
plugins: [pinia],
},
});
console.log(wrapper.text()) //"hhhhh";
});
Use global.mocks to mock the states used in the component, but this only works for the states passed in with setup() in the component, while the ones passed in with mapState() have no effect.
// test.spec.js
import { mount } from "#vue/test-utils";
import { createTestingPinia } from "#pinia/testing";
import HelloWorld from "#/components/HelloWorld.vue";
test("pinia in component test", () => {
const wrapper = mount(HelloWorld, {
global: {
plugins: [createTestingPinia()],
mocks: { title: "xxxxx" },
},
});
console.log(wrapper.text()) //"hhhhh"
});
This has been resolved using jest.mock().
import { mount } from "#vue/test-utils";
import { createPinia } from "pinia";
import HelloWorld from "#/components/HelloWorld.vue";
jest.mock("#/stores/test", () => {
const { defineStore } = require("pinia");
const useTestStore = defineStore("test", { state: () => ({ title: "xxxxx" }) });
return { useTestStore };
});
test("pinia in component test", () => {
const wrapper = mount(HelloWorld, {
global: { plugins: [createPinia()] },
});
expect(wrapper.text()).toBe("xxxxx");
});
Thanks to Red Panda for this topic. I use "testing-library", and "vue-testing-library" instead of "vue-test-utils" and "jest", but the problem is the same - couldn't change pinia initial data of the store.
I finally found a solution for this issue without mocking the function.
When you $patch data, you just need to await for it. Somehow it helps. My code looks like this and it totally works:
Popup.test.js
import { render, screen } from '#testing-library/vue'
import { createTestingPinia } from '#pinia/testing'
import { popup } from '#/store1/popup/index'
import Popup from '../../components/Popup/index.vue'
describe('Popup component', () => {
test('displays popup with group component', async () => {
render(Popup, {
global: { plugins: [createTestingPinia()] }
})
const store = popup()
await store.$patch({ popupData: 'new name' })
screen.debug()
})
})
OR you can set initialState using this scheme:
import { render, screen } from '#testing-library/vue'
import { createTestingPinia } from '#pinia/testing'
import { popup } from '#/store1/popup/index'
import Popup from '../../components/Popup/index.vue'
test('displays popup with no inner component', async () => {
const { getByTestId } = render(Popup, {
global: {
plugins: [
createTestingPinia({
initialState: {
popup: {
popupData: 'new name'
}
}
})
]
}
})
const store = popup()
screen.debug()
})
Where popup in initialState - is the imported pinia store from #/store1/popup. You can specify any of them there the same way.
Popup.vue
<script>
import { defineAsyncComponent, markRaw } from 'vue'
import { mapState, mapActions } from 'pinia'
import { popup } from '#/store1/popup/index'
export default {
data () {
return {}
},
computed: {
...mapState(popup, ['popupData'])
},
....
I'm working on a project using Vue 3 with composition API styling.
Composition API is used for both components and defining my store.
Here is my store
player.js
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
export const usePlayerStore = defineStore('player',()=>{
const isMainBtnGameClicked = ref(false)
return { isMainBtnGameClicked }
})
MyComponent.vue
//import { usePlayerStore } from '...'
const playerStore = usePlayerStore()
playerStore.isMainBtnGameClicked = true
isMainBtnGameClicked from my store is updated properly.
You can also update variables from components by passing them by reference to the pinia store. It's working in my project.
For sake of saving future me many hours of trouble, there is a non-obvious thing in play here - the event loop. Vue reactivity relies on the event loop running to trigger the cascade of state changes.
When you mount/shallowMount/render a component with vue-test-utils, there is no event loop running automatically. You have to trigger it manually for the reactivity to fire, e.g.
await component.vm.$nextTick;
If you don't want to mess around with ticks, you have to mock the store state/getters/etc. (which the docs strongly lean toward, without explaining the necessity). Here OP mocked the whole store.
See also: Vue-test-utils: using $nextTick multiple times in a single test
I'm using Vue composition-api with Vue2.
I ran into a problem when I tried to call a method of a component with a render function from its parent.
Without render function, it's ok.
TemplateComponent.vue
<template>
...
</template>
<script lang="ts">
import { defineComponent } from '#vue/composition-api'
export default defineComponent({
setup (props, context) {
const doSomething = () => {
console.log('doSomething')
}
return {
// publish doSomething method.
doSomething
}
}
})
</script>
So, parent component can call TemplateComponent's method like this.
TopPage.vue
<template>
<TemplateComponent ref="componentRef" />
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from '#vue/composition-api'
import TemplateComponent from '#/components/TemplateComponent.vue'
export default defineComponent({
components: { TemplateComponent },
setup (props, context) {
const componentRef = ref()
onMounted(() => {
componentRef.value.doSomething()
})
}
})
</script>
With render function, I can't find way to call method.
RenderComponent.vue
<script lang="ts">
import { defineComponent, h } from '#vue/composition-api'
export default defineComponent({
components: { TemplateComponent },
setup (props, context) {
const doSomething = () => {
console.log('doSomething')
}
// setup method should return render function.
return () => h('div', 'Hello world!!')
}
})
</script>
When declare render function with composition api, we should return render function in setup method.
https://v3.vuejs.org/guide/composition-api-setup.html#usage-with-render-functions
In this case, I don't understand how to publish doSomething method.
Is there a way to solve this problem?
expose context method exists to combine render function and public instance methods in setup:
context.expose({ doSomething })
return () => ...
I use Vue 3.1.1
I am using script setup in the experimental stage with single file components.
Using the script setup, I understand defineProps, defineEmit, and useContext, but I don't understand how to use the render function.
<script lang="ts" setup>
import { defineProps } from 'vue'
const props = defineProps<{
text: string
}>()
const handleClick = () => {
console.log('click!!')
}
// Render function...
/* The template I want to create.
<button
class="btn btn-primary"
type="button"
#click="handleClick"
>
{{ props.text }}
</button>
*/
</script>
try it.
<script lang="tsx" setup>
import { h } from 'vue';
const render = () => {
return h('div', []);
};
const jsxNode = () => {
return <div> text </div>;
};
</script>
<template>
<render />
<jsxNode />
</template>
Try to use the h function to create your element then render it inside the template section as follows :
<script setup lang="ts">
import { ref,h } from 'vue'
const props = defineProps<{
text: string
}>()
const handleClick = () => {
console.log('click!!')
}
const root=h('button',
{type:'button',onClick:handleClick,class:'btn btn-primary'},props.text)
</script>
<template>
<root/>
</template>
DEMO
You can assign the render function directly to your component instance via getCurrentInstance
Custom hook
// useRender.ts
import type { VNode } from 'vue';
import { getCurrentInstance } from 'vue';
export function useRender(render: () => Arrayable<VNode | null>): void {
const vm = getCurrentInstance();
if (!vm) {
throw new Error('[useRender] must be called from inside a setup function');
}
/**
* In development mode, assignment render property works fine
* but in production SFC overwrites it with an empty function
* because no <template> section defined.
*
* Filthy hack to avoid this in production.
* https://github.com/vuejs/core/issues/4980
*/
if (import.meta.env.DEV) {
(vm as any).render = render;
} else {
Object.defineProperty(vm, 'render', {
get: () => render,
set: () => {},
});
}
}
Usage
<script setup>
import { h } from 'vue';
import { useRender } from './useRender';
useRender(() => h('span', 'Hello from script setup'));
</script>
You may try an extra normal script.
<script lang="tsx" setup>
import { defineProps, defineExpose } from 'vue'
const props = defineProps<{
text: string
}>()
const handleClick = () => {
console.log('click!!')
}
defineExpose({
handleClick,
// other data,method
})
</script>
<script lang="tsx">
import { defineComponent } from 'vue'
export default defineComponent({
render() {
return (
<button
class="btn btn-primary"
type="button"
onClick={this.handleClick}
>
{{ this.text }}
</button>
)
}
})
</script>
Any data, methods except the props should be exposed using defineExpose.
BTW, setup script and normal script should have the same lang attribute.
I have an application in which I have an export feature. if the user clicks on the menu 'export', I will call a function in my export store and get the data and export it into a Excel file. I am using the library XLSX for exporting and I wanted to use code splitting to have lazy loading mechanism for loading this dependency.
import React, { Fragment } from 'react';
import { connect } from 'react-redux';
import { translate, Trans } from 'react-i18next';
import { exportAccounts } from '../../store/export';
import { Button, Menu, Dropdown } from 'antd';
const mapDispatchToProps = dispatch => ({
exportAccounts: translator => dispatch(exportAccounts(translator)),
});
class MyComponent extends React.Component {
handleMenuOnClick = e => {
switch (e.key) {
case 'export':
this.props.exportAccounts(this.props.t);
break;
default:
}
};
options = (
<Menu onClick={this.handleMenuOnClick}>
<Menu.Item key="export">
<Trans i18nKey="52918">Export</Trans>
</Menu.Item>
</Menu>
);
render = () => {
const { isSettingVisible } = this.state;
const { columnsToShow, t } = this.props;
return (
<Dropdown overlay={this.options} trigger={['click']}>
<Button icon="ellipsis">
<Trans i18nKey="-1">More Options</Trans>
</Button>
</Dropdown>
);
}
export default connect( mapDispatchToProps)(translate()(MyComponent));
as I read the article on react loader library, I see that we can wrap out component into loader:
import Loadable from 'react-loadable';
const LoadableOtherComponent = Loadable({
loader: () => import('./OtherComponent'),
loading: () => <div>Loading...</div>,
});
const MyComponent = () => (
<LoadableOtherComponent/>
);
but in my case, I would like to load the function in my store and not the whole component.
how can I achieve this?