Programatically create and mount components with vue 3 - javascript

I need to programatically create and mount components on the fly from parent component.
It works fine with Vue 2.
import Vue from 'vue'
// ParentComponent.vue
export default {
methods: {
createChild() {
const Child = Vue.extend(ChildComponent)
const child = new Child({
propsData,
store: this.$store,
i18n: this.$i18n
}).$mount()
}
}
}
But I cannot figure out how-to do with Vue 3.

I have finally ended with this, after finding some info here:
https://github.com/vuejs/vue-next/issues/1802
https://github.com/pearofducks/mount-vue-component
// ParentComponent.vue
import { h, render } from 'vue'
export default {
methods: {
createChild() {
const child = h(ChildComponent, propsData)
child.appContext = this.$.appContext // use store, i18n
const el = document.createElement('div')
render(node, el)
}
}
}

Related

Navigo Javascript routing: Every route I register didn't match any of the registered routes

I'm learning how to make a single page app with javascript.
My javascript teacher provided a beautiful tutorial how to create a single page application from scratch. I followed the tutorial and everything went well untill the part where the routing came in..
He uses a library which is called navigo. I don't know why but it seems to not working for me at all.
The moment I've written the final line of code. My homepage disappeared and the console gave a warning that my route '/' which is my homepage, didn't match any of the registered routes, but it looks like there is no route registered at all, while I'm definitly registering them..
here is my code
My root index.js
import './sass/main.scss';
import App from './App';
import { HomeComponent, NewEventComponent } from './Components';
// Retrieve appComponent
const initApp = () => {
const appContainer = document.getElementById('appContainer');
const app = new App(appContainer);
app.addComponent(new HomeComponent());
app.addComponent(new NewEventComponent());
};
window.addEventListener('load', initApp);
My App.js (here is where my route is defined for every component. routerPath makes it dynamic )
// The App Wrapper
import Component from './lib/Component';
import Router from './Router';
class App {
constructor(parent) {
this.parent = parent;
this.components = [];
}
clearparent() {
while (this.parent.firstChild) {
this.parent.removeChild(this.parent.lastChild);
}
}
addComponent(component) {
if (!(component instanceof Component)) return;
// get the name from our component
const { name, routerPath } = component;
// when a component asks to reRender
component.reRender = () => this.showComponent(component);
// add to internal class
this.components.push(component);
// add to router
Router.getRouter().on(routerPath, () => {
this.showComponent({ name });
}).resolve();
}
showComponent({ name }) {
const foundComponent = this.components.find((component) => component.name === name);
if (!foundComponent) return;
this.clearparent();
this.parent.appendChild(foundComponent.render());
}
}
export default App;
The Home Component
// The Home Component
import Component from '../lib/Component';
import Elements from '../lib/Elements';
class HomeComponent extends Component {
constructor() {
super({
name: 'home',
model: {
counter: 0,
},
routerPath: '/',
});
}
incrementCounter() {
this.model.counter += 1;
}
render() {
const { counter } = this.model;
// create home container
const homeContainer = document.createElement('div');
// append header
homeContainer.appendChild(
Elements.createHeader({
textContent: `Current value is: ${counter}`,
}),
);
// append button
homeContainer.appendChild(
Elements.createButton({
textContent: 'increase',
onClick: () => { this.incrementCounter(); },
}),
);
return homeContainer;
}
}
export default HomeComponent;
A Component
// My components
class Component {
constructor({
name,
model,
routerPath,
}) {
this.name = name;
this.model = this.proxyModel(model);
this.routerPath = routerPath;
this.reRender = null;
}
proxyModel(model) {
return new Proxy(model, {
set: (obj, prop, value) => {
obj[prop] = value;
if (this.reRender) this.reRender();
return true;
},
});
}
}
export default Component;
The Router
// My Router
import Navigo from 'navigo';
const Router = {
router: null,
getRouter() {
if (!this.router) {
const rootUrl = `${window.location.protocol}//${window.location.host}`;
this.router = new Navigo(rootUrl, false);
}
return this.router;
},
};
export default Router;
Solution: I switched to Navigo(^7.0.0) and it works!
I seem to have the same problem as you. I'm also using navigo (^8.11.1). The problem is fixed for me when I declare a new router like this: new Navigo('/', false).
It still gives me the warning now, but it loads the page. sadly, this will only work in a dev environment

How to import/include plugin in testing with Jest / Vue-test-utils?

When testing a base button implementation using Jest and Vue-test-utils, the tests work but I am getting the following warning:
[Vue warn]: Unknown custom element: b-button - did you register the component correctly? For recursive components, make sure to provide the "name" option.
I am confident is because I am not including the Buefy plugin dependencies correctly, and I don't have a lot of experience here.
Here is my single file component for the base button:
<template>
<b-button data-testid="base-button" #click="$emit('click')">
{{ buttonLabel }}
</b-button>
</template>
<script>
export default {
props: {
buttonLabel: {
type: String,
default: 'Button',
},
},
}
</script>
<style></style>
And here is my testing:
import { mount } from '#vue/test-utils'
import BaseButton from '#/components/base/BaseButton'
const Component = BaseButton
const ComponentName = 'BaseButton'
const global_wrapper = mount(Component, {})
describe(ComponentName, () => {
it('should render the button', () => {
const wrapper = global_wrapper
const button = wrapper.find('[data-testid="base-button"]')
expect(button.exists()).toBeTruthy()
}),
it('should emit the click event on a click', async () => {
const wrapper = global_wrapper
console.log(wrapper.html())
const button = wrapper.find('[data-testid="base-button"]')
button.trigger('click')
const clickCalls = wrapper.emitted('click')
expect(clickCalls).toHaveLength(1)
})
})
I would appreciate help understanding the appropriate way to include the Buefy b-button component in the test.
Posting for a Vue 3 solution. As StevenSiebert mentioned, createLocalVue is no longer available with v2 #vue/test-utils (needed for Vue 3 tests). You'll need to use the global object in your tests.
Per the docs for mount, to register the plugin for one-off mounts, you can use:
import foo from 'foo'
mount(Component, {
global: {
plugins: [foo]
}
})
To register the plugin in all tests, you can add to your test setup file, i.e. setup.js. See docs
import foo from 'foo'
config.global.plugins = [foo]
To include the Buefy plugin (or any other plugin), you can use something like const localVue = createLocalVue() from vue-test-utils to create a local vue and use the Buefy plugin, localVue.use(Buefy) as below. This localVue can be included when mounting the wrapper.
import { createLocalVue, mount } from '#vue/test-utils'
import Component from '../Component'
import Buefy from 'buefy'
const localVue = createLocalVue()
localVue.use(Buefy)
const global_wrapper = mount(Component, {
localVue,
})
If you only have one or two components you want to use from the plugin, you can import the individual components and run localVue.use multiple times as such:
import { createLocalVue, mount } from '#vue/test-utils'
import Component from '../Component'
import { Button, Checkbox } from 'buefy'
const localVue = createLocalVue()
localVue.use(Button)
localVue.use(Checkbox)
const global_wrapper = mount(Component, {
localVue,
})

How to access Vue 3 global properties from the store

In Vue 2 I used to import Vue and access global properties like this (from the store):
import Vue from 'vue'
Vue.config.myGlobalProperty
According to the new documentation, in Vue 3 the global properties are declared using the app object returned by createApp:
const app = createApp({})
app.config.globalProperties.myGlobalProperty
And then accessed in the child component by simply calling this.myglobalProperty
But how to access that global property from the store? I tried exporting/importing the app object but it doesn't work (probably due to the app being created after its import in the store).
With Vue 2 I used to use global properties in the store like this:
Declaration in the main.js file:
import Vue from 'vue'
Vue.config.myglobalProperty = 'value'
Usage in the store:
import Vue from 'vue'
Vue.config.myglobalProperty
Is there a good way to do that in Vue3?
I noticed a better way to provide/inject properties but it works with child component only and not with the store.
You could pass the app instance to a store factory:
// store.js
import { createStore as createVuexStore } from 'vuex'
export const createStore = (app) => {
return createVuexStore({
actions: {
doSomething() {
if (app.config.globalProperties.myGlobal) {
//...
}
}
}
})
}
And use it in main.js like this:
// main.js
import { createApp } from 'vue'
import { createStore } from './store'
const app = createApp({})
const store = createStore(app)
app.use(store)
app.mount('#app')
If your store modules need access to the app instance, you could use the same technique above with a module factory:
// moduleA.js
export default (app) => {
return {
namespaced: true,
actions: {
doSomething() {
if (app.config.globalProperties.myOtherGlobal) {
//...
}
}
}
}
}
And import it into your store like this:
// store.js
import moduleA from './moduleA'
import { createStore as createVuexStore } from 'vuex'
export const createStore = (app) => {
return createVuexStore({
modules: {
moduleA: moduleA(app),
}
})
}
If you do not use Vuex, etc., you can easily create your store via provide/inject on the application itself, as in the example (the example is simplified for understanding):
const createState = () => reactive({counter: 0, anyVariable: 1});
const state = createState();
const key = 'myState';
// example of reactivity outside a component
setInterval(() => {
state.counter++;
}, 1000);
const app = createApp({});
app.provide('myState', state); // provide is used on the whole application
As you can see, your own store can be completely used outside the component.
Inside components, you can use (example):
setup() {
...
const globalState = inject('myStore'); // reactive => {counter: 0, anyVariable: 1}
...
return {globalState, ...}
}
Accordingly, you can have multiple stores in multiple Vue applications.
I hope this example will help you somehow.

Pass component as a prop in Reactjs

I got an article showing how to pass a component as a prop, but I could not make it works, can anyone help me on it?
This is the example I got.
import React from "react";
import Foo from "./components/Foo";
import Bar from "./components/Bar";
const Components = {
foo: Foo,
bar: Bar
};
export default block => {
// component does exist
if (typeof Components[block.component] !== "undefined") {
return React.createElement(Components[block.component], {
key: block._uid,
block: block
});
}
}
And this is my code
I have one file called routes.js, that has the state called routes.
var routes = [
{
path: "/user-profile",
name: "Quem Somos",
icon: "FaIdBadge",
component: UserProfile,
layout: "/admin"
}
And another component called Sidebar, where I receive routes and need to change the icon based in what is configured at the 'routes' prop.
const Components = {
fa:FaIdBadge
}
<i>{prop => Components(prop.icon)}</i>
But the property with the icon is not recognized.
You're pretty close.
Choosing the Type as Runtime
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// Correct! JSX type can be a capitalized variable.
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
So more specific to your example
// component map in the file for lookup
const components = {
fa: FaIdBadge,
}
...
// In the render function
// fetch the component, i.e. props.icon === 'fa'
const IconComponent = components[props.icon];
...
<IconComponent />

How to share a class instance across a react application?

How to share a class instance across a react application?
I need to use an external library which requires me to instantiate it and use that instance in the application to access various methods/properties,
const instance = new LibraryModule(someInitData);
I am using react functional components in my project. I don't want to re-create instance every time I need to use it (can be thought of a singleton).
How can I share the same instance with all of my components?
Imports are already global, you just need to import the module:
class LibraryModule {
x = 5;
}
const instance = new LibraryModule();
export default instance;
import instance from "./MyLibrary";
import Component from "./Component";
console.log(instance.x);
const App = () => {
useEffect(() => {
console.log(`from App, called last`, instance.x);
}, []);
return <Component />;
};
import instance from "./MyLibrary";
const Component = () => {
useEffect(() => {
instance.x = 10;
console.log(`from component`, instance.x);
}, []);
return <>Component</>;
};
it seems like you are looking for some sort of Dependency Injection,
a thing that isn't really in the react way.
I'd suggest to use the Context Api.
https://reactjs.org/docs/hooks-reference.html#usecontext
import React from 'react';
export const LibraryModuleContext = React.createContext(new LibraryModule);
import React from 'react';
import { LibraryModuleContext } from './library-module';
const App = () => (
<LibraryModuleContextt.Provider>
<Toolbar />
</LibraryModuleContext.Provider>
)
and then later in your code
import React from 'react';
import { LibraryModuleContext } from './library-module';
const FooComponent = () => {
const LibraryModule = useContext(LibraryModuleContext);
return (
<div>Hello {LibraryModule.x}</div>
);
};

Categories