Mount Vue 3 Instance on an on the fly created DIV - javascript

I'm trying to mount a vue instance on a div that I'm creating on the fly on the main.js file:
import { createApp } from "vue";
import "#/assets/styles/index.scss";
import App from "./App.vue";
import store from "./store";
const app = createApp(App).use(store);
let div = document.createElement("div");
document.body.appendChild(div);
div.setAttribute("id", "widget");
app.mount("#widget");
When I do this I'm getting this error
value cannot be made reactive: null
I'm assuming this is because I'm not waiting for the DOM element to actually load in the DOM in order to mount the Vue instance but I can't find an approach to do this correctly. Something like this is also not working for me :
setup() {
onMounted(() => {
if(document.getElementById("new-div")) {
const app = createApp({});
app.mount('#new-div');
}
});
}
I was just wondering if I have to mount the app somewhere else (maybe in the App component or in my wrapper comoponent on the onMounted hook).

You can fix this issue by moving the code that creates the div element and mount the Vue app inside a DOMContentLoaded event listener like this:
document.addEventListener("DOMContentLoaded", () => {
let div = document.createElement("div");
document.body.appendChild(div);
div.setAttribute("id", "widget");
app.mount("#widget");
});
Alternatively, you can move the div element that you want to mount the Vue app on it to the HTML file instead of creating it in JavaScript.
<div id="widget"></div>
app.mount("#widget");

Related

Vue 3 runtime bundle without initial component/render

I'm trying to implement some vue components to existing page.
Vue comes with multiple bundles:
Runtime + Compiler
Runtime Only
Currently I use Runtime + Compiler
My HTML looks something like this:
<div id="app">
<h1>Some Text<h1>
<p>Long Text</p>
<v-component-table/>
<p>Long Text</p>
<p>Long Text</p>
<v-other-component/>
...more normal HTML
</div>
JS
import { ref, createApp, defineAsyncComponent } from 'vue'
import store from './store'
const app = createApp({
setup(){
const page = ref({})
return { page }
}
})
app.use(store)
app.component('v-component-table', defineAsyncComponent(() => import(/* webpackPrefetch: true */ '#components/VComponentTable')))
app.component('v-other-component', defineAsyncComponent(() => import(/* webpackPrefetch: true */ '#components/VOtherComponent')))
const vm = app.mount('#app')
Using above (with runtime + compiler) Vue will parse/compile everything inside #app.
When I tried to change to only Runtime bundle, this requires to create initial render component/function
import App from 'App'
import { h } from 'vue'
import store from './store'
const app = createApp({
render(){
return h(App)
}
})
Above replaces the current content inside #app (logical).
So my question is: it is possible to mount Vue, without initial App with only Runtime Bundle, or If I don't want a compiler, I need to create Vue Instance per component that I will use on page? (If I need to create Vue Instance per component how can I share Vuex, between this instances?)
Reason: The page is big, and I need to inject to this page 2-3 components of Vue. and want to avoid Vue parsing all the content inside #app

Javascript working before page rendering in Gatsby

I try to convert a HTML template (Bootstrap 5) to Gatsby template. CSS and pages working expected but in HTML template there is a main.js file and it need to load after page rendered.
I modify the main.js file like that;
import { Swiper } from "swiper/swiper-react.cjs.js";
import GLightbox from "glightbox/dist/js/glightbox.min.js";
import AOS from "aos";
AOS.init();
export const onClientEntry = () => {
window.onload = () => {
console.log("deneme");
/*rest of code*/
};
};
In here I try two way. One of them, I create main.js file inside src->components->assets->js folder. Then in layout.js I try to import that file.
import React from "react";
import PropTypes from "prop-types";
import { Breadcrumb } from "gatsby-plugin-breadcrumb";
import Header from "./partials/header";
import { Helmet } from "react-helmet";
import useSiteMetadata from "./hooks/siteMetadata";
import "./assets/css/style.css";
import "./assets/js/main.js"
However in here in debug not hit the any method inside onClientEntry. So I decide to change my way.
Secondly, I try to add code inside main.js to gatsby-browser.js. That's time again getting Cannot read property 'addEventListener' of null because of html is not ready yet.
My file structure:
window (and other global objects like document) are not available during the SSR (Server-Side Rendering) because this action is performed by the Node server (where for obvious reasons there's no window, yet) so you can't access directly to onload function. In addition, accessing these global objects outside the scope of React (without hooks) can potentially break React's hydration process.
That said, you have a few approaches:
Using React hooks. Specifically, useEffect with empty dependencies ([]) fits your specifications, since the effect will be fired once the DOM tree is loaded (that's what empty deps means):
const Layout = ({children}) => {
useEffect(()=>{
mainJs();
}, [])
return <main>{children}</main>
}
Assuming that your ./assets/js/main.js file has a mainJs() function exported, this approach will load it when the DOM tree is loaded. For example:
const mainJs= ()=> console.log("deneme");
The console.log() will be triggered when the HTML tree is built by the browser. Tweak it to adapt it to your needs.
Adding a window-availability condition like:
export const onClientEntry = () => {
if(typeof window !== 'undefined'){
window.onload = () => {
console.log("deneme");
/*rest of code*/
};
}
};
Alternatively, you can output the console.log directly in your onClientEntry, depending on your needs:
export const onClientEntry = () => {
console.log("deneme");
/*rest of code*/
};
You can even combine both approaches by adding a useEffect in your gatsby-browser if it works for you.

How to get access to the underlying component of a react html element?

I'd like to create a <Breadcrumb> component which receives a <Link> component as a dependency.
This could be a <Link> component from react-router-dom or an <a> component.
How can I get direct access to the <a> component so that I may inject it into <Breadcrumb>?
Here is a brief example of what I'm trying to do.
const createBreadcrumbComponent = (Link) => {
return function Breadcrumb(props) {
// return JSX that renders this Link component
// A basic example:
return <Link href="/">{"Home"}</Link>
}
}
An example of using this with react-router-doms own <Link component:
// import {Link} from 'react-router-dom';
const Breadcrumb = createBreadcrumbComponent(Link);
I would like do the same as above, but substitute Link with a. Unfortunately there doesn't seem to be an actual component that represents a (with React.createElement() we pass the string "a" instead).
You can always use ref to access the underlying DOM element from a React Component. But, this is not always the case.
Following are the cases, which will enable you to use ref -
The component is a native HTML element, for eg - a, div, img etc.
The React native component is wrapped with a forwardRef. This doc explains it nicely - https://reactjs.org/docs/forwarding-refs.html
An example for using ref can be seen below -
import React, {useRef} from 'react';
const Comp = () => {
const elRef = useRef();
useEffect(() => {
console.log(elRef.current); // -> the p element
}, []);
return <p ref={elRef}>Hello</p>;
}
There is a third way of getting the underlying element, but I don't recommend it since it is deprecated. You can use ReactDOM.findDOMNode(Component). This is a part of react-dom package.
You can read more about it here - https://reactjs.org/docs/react-dom.html#finddomnode
I think the solution is to create a wrapper around <a>, for instance:
const A = (props) => <a {...props}/>;
const Breadcrumb = createBreadcrumbComponent(A);
I was hoping there would be a way to obtain a component directly from React that already encompasses the <a> element

How to trigger event with Vue Test Utils on a BootstrapVue element?

This issue gives me a hard time and I can't understand how to make Vue Test Utils and BootstrapVue play nice together.
A minimal example would look like this:
MyComponent.vue
<template>
<div>
<b-button variant="primary" #click="play">PLAY</b-button>
</div>
</template>
<script>
export default {
name: 'MyComponent',
methods: {
play() {
console.log("Let's play!");
}
}
}
</script>
In the main.js we use BootstrapVue: Vue.use(BootstrapVue).
This is how I'm trying to test that the click event has been triggered:
import { expect } from 'chai';
import sinon from 'sinon';
import Vue from 'vue';
import { shallowMount, createLocalVue } from '#vue/test-utils';
import BootstrapVue, { BButton } from 'bootstrap-vue';
import MyComponent from '#/components/MyComponent.vue';
const localVue = createLocalVue();
localVue.use(BootstrapVue);
describe('MyComponent.vue', () => {
it('should call the method play when button is clicked', () => {
const playSpy = sinon.spy();
const wrapper = shallowMount(MyComponent, {
localVue,
methods: {
play: playSpy,
},
});
wrapper.find(BButton).trigger('click');
expect(playSpy.called).to.equal(true);
});
});
This gives me:
AssertionError: expected false to equal true
+ expected - actual
-false
+true
I checked How to test for the existance of a bootstrap vue component in unit tests with jest?, but it doesn't apply to BButton.
When running the test I also don't see any output on the command line, where I would expect this line to be executed:
console.log("Let's play!");
What's wrong here?
The reason why the event click couldn't be triggered is the way how shallowMount works in contrast to mount.
As we know, Vue Test Utils provide two methods to mount a component, i.e. render the template and generate a DOM tree:
mount
shallowMount
The first method mount generates a complete DOM tree and traverses through all child components. Most of the time this is not necessary, so the method shallowMount is preferred - it stubs the child components just one level below the parent component.
In my case this was also the root of the problem. BootstrapVue provides components, like BButton, which can be used in your own Vue templates. That means that in the following template:
<template>
<div>
<b-button variant="primary" #click="play">PLAY</b-button>
</div>
</template>
the b-button is a child component, which is stubbed when using shallowMount in the unit tests for our component. That's the reason why we can't find an element button:
const wrapper = shallowMount(MyComponent);
wrapper.find('button'); // won't work
We can find the child component like this:
wrapper.find(BButton); // BButton has to be imported from bootstrap-vue
but if we try to output the rendered element:
console.log(wrapper.find(BButton).element);
we'll get:
HTMLUnknownElement {}
The BButton as a child component hasn't been fully rendered and there is no button element in the DOM tree. But when we use mount we have this behaviour:
const wrapper = mount(MyComponent);
console.log(wrapper.find(BButton).element);
we'll get:
HTMLButtonElement { _prevClass: 'btn btn-primary' }
We see that mount has rendered the child component. When we use mount we can directly access the button element:
wrapper.find('button');
Now that we have the button we can trigger an event like click on it.
I hope this helps other beginners too. The examples are very simplified, don't forget to create localVue using createLocalVue and pass it to the mount method as illustrated in the question.
When using BootstrapVue think very carefully which mounting method you need.
While still performing a shallowMount you should be able to do this:
wrapper.find(BButton).vm.$listeners.click();

Add all vue components to window array

I currently have a strange Vue setup due to our websites all using an old system.
What we have had to do is create an instance of Vue for each component (usually not many). What I want to do for all components is to pass their name and reference to the element into an array, just for reference when debugging issues on live issues.
app.js
import Vue from "vue";
import Axios from 'axios';
import inViewportDirective from 'vue-in-viewport-directive';
window.components = [];
Vue.component( 'video-frame', () => import('./components/VideoFrame.vue' /* webpackChunkName: "video-frame" */) );
Vue.prototype.$event = new Vue();
Vue.prototype.$http = Axios;
Array.prototype.forEach.call(document.querySelectorAll(".app"), (el, index) => new Vue({el}));
Now i'm adding the following code to each component, is there not a way I can do this once within my app.js and have all the components automatically do the following:
mounted() {
window.components.push({
tag: this.$vnode.tag,
elm: this.$vnode.elm
});
},
You can use a global mixin like this:
Vue.mixin({
mounted: function() {
window.components.push({
tag: this.$vnode.tag,
elm: this.$vnode.elm
});
}
});
That will ensure that code will run on the mounted hook on every single one of your Vue instances.
Reference: https://v2.vuejs.org/v2/guide/mixins.html

Categories