I am creating a custom component library that i want to share across multiple vue ssr projects.
Problem:
Using the current option, i get a hydration error(The client-side rendered virtual DOM tree is not matching server-rendered content.) which is fixed if i wrap the components within <no-ssr> tags which i do not want to do. I had these component files in one of the vue projects directly before and they worked without any issue.
Example file that does not work:
<template>
<div class="collapse">
<div class="trigger" #click="handleClickEvent">
<slot name="trigger"></slot>
</div>
<div :id="uniqueKey" class="details">
<slot></slot>
</div>
</div>
</template>
<style lang="scss" scoped>
.collapse {
.trigger {
cursor: pointer;
}
.details {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
}
</style>
<script>
import $ from 'bucks-js'
export default {
name: 'collapse',
data: () => ({
uniqueKey: null,
}),
props: {
// if collapse should be triggered from outside
manual: { type: Boolean, default: false },
},
created () {
this.uniqueKey = 'mapify-collapse-' + $.string.random()
},
methods: {
/**
* Handles click event.
*/
handleClickEvent () {
// if collapse should be manual triggered , fire event and open from outside
this.$emit('triggered')
if (!this.manual) this.openCloseDetails()
},
/**
* Open / close details collapse.
*/
openCloseDetails () {
if (!$.isClient()) return
let detailsContainer = document.getElementById(this.uniqueKey)
// check if open
if (detailsContainer.style.maxHeight) {
detailsContainer.style.maxHeight = null
} else {
detailsContainer.style.maxHeight = detailsContainer.scrollHeight + 'px'
}
},
/**
* Open details.
*/
openDetails () {
if (!$.isClient()) return
let detailsContainer = document.getElementById(this.uniqueKey)
detailsContainer.style.maxHeight = detailsContainer.scrollHeight + 'px'
},
/**
* Close details.
*/
closeDetails () {
if (!$.isClient()) return
let detailsContainer = document.getElementById(this.uniqueKey)
detailsContainer.style.maxHeight = null
},
},
}
</script>
This is part of my package.json:
"scripts": {
"build": "rm -rf ./lib && vue-cli-service build --target lib src/index.js"
},
"dependencies": {
"bucks-js": "^1.5.3",
"vue": "^2.6.11"
},
"devDependencies": {
"#vue/cli-plugin-babel": "^4.2.3",
"#vue/cli-service": "^4.2.3",
"node-sass": "^4.13.1",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.11"
}
I also tried to setup the package with this very helpful repository https://github.com/team-innovation/vue-sfc-rollup but the components do not work anymore as they should.
Related
I have been killing myself trying to figure out how to test a Vue component with a v-dialog, something which worked perfectly fine in Vue2. Currently I am using Vue3, Vitest, Vuetify3.
here is a very simple component which demonstrates a problem
<template>
<div>
<v-btn #click.stop="dialog=true" class="open-dialog-btn">click me please</v-btn>
<v-dialog v-model="dialog" max-width="290" >
<div class="dialog-content">
<v-card>welcome to dialog</v-card>
</div>
</v-dialog>
</div>
</template>
<script setup>
import {ref} from "vue";
const dialog = ref(false);
</script>
and here is a unit test for it:
import '../setup';
import { mount } from '#vue/test-utils';
import { createVuetify } from "vuetify";
import HelloDialog from "#/components/HelloDialog.vue";
describe('HelloDialog', () => {
let wrapper;
let vuetify;
beforeEach(() => {
vuetify = createVuetify();
});
describe('dialog tests', () => {
beforeEach(async () => {
wrapper = await mount(HelloDialog, {
global: {
plugins: [vuetify],
},
});
});
test('test dialog', async () => {
expect(wrapper.find('.dialog-content').exists()).toBeFalsy();
await wrapper.find('.open-dialog-btn').trigger('click');
console.log(wrapper.html());
expect(wrapper.find('.dialog-content').exists()).toBeTruthy();
});
});
});
the last line in unit test is not working - dialog content is not displayed. Here is an output from wrapper.html() after button is clicked:
<div><button type="button" class="v-btn v-btn--elevated v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-elevated open-dialog-btn"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
<!----><span class="v-btn__content" data-no-activator="">click me please</span>
<!---->
<!---->
</button>
<!---->
<!--teleport start-->
<!--teleport end-->
</div>
AssertionError: expected false to be truthy
at ....../HelloDialog.spec.js:27:56
here is test section from vite.config.js:
test: {
// https://vitest.dev/config/
globals:true,
environment: 'happy-dom',
setupFiles: "vuetify.config.js",
deps: {
inline: ["vuetify"],
},
},
and here is vuetify.config.js:
global.CSS = { supports: () => false };
here some versions from package.json:
"dependencies": {
"#mdi/font": "7.1.96",
"#pinia/testing": "^0.0.14",
"axios": "^1.2.0",
"dotenv": "^16.0.3",
"happy-dom": "^8.1.1",
"jsdom": "^20.0.3",
"lodash": "^4.17.21",
"pinia": "^2.0.27",
"roboto-fontface": "*",
"vue": "^3.2.45",
"vuetify": "3.0.6",
"webfontloader": "^1.0.0"
},
"devDependencies": {
"#vitejs/plugin-vue": "^4.0.0",
"#vue/test-utils": "^2.2.6",
"vite": "^4.0.3",
"vite-plugin-vuetify": "^1.0.0-alpha.12",
"vitest": "^0.26.2"
}
I have tried everything at this point, and I think the problem has something to do with v-dialog using teleport component. After struggling for several days trying to figure out I settled on using a stub to not use a real dialog when testing but I really don't like this approach.
any ideas would be greatly appreciated
I have the same issue and found the content of v-dialog was rendered in document.body when I called mount().
You can test the dialog content like below.
// expect(wrapper.find('.dialog-content').exists()).toBeTruthy();
expect(document.querySelector('.dialog-content')).not.toBeNull();
I recommend to call unmount() after each test.
afterEach(() => {
wrapper.unmount()
});
Hope this helps although I doubt it's a good approach because I don't want to care whether the component is using teleport or not.
I am trying to find a vue 3 component that is a code editor with a similar theme as vscode. It should have the tree structure and be able to execute the code.
Some of the things I found that sadly did not fit the bill are:
monaco-editor
vue3-ace-editor
ace
I would like to send the files from the backend and have them rendered in the embedded code editor.
Any advice would be greatly appreciated.
Update 1
I got it to work. There is not a file tree but there is a component for it. Just need to add a watcher to the file tress and have what is selected in the ManacoEditor. Here is a basic example to get the IDE to render in the browser.
<template>
<div>
<MonacoEditor
width="900"
height="750"
language="go"
#change="onChange"
:value="value"
></MonacoEditor>
</div>
</template>
<script lang="ts">
import {Options, Vue} from "vue-class-component";
import MonacoEditor from "monaco-editor-vue3";
#Options({
components: {
MonacoEditor,
},
props: {
editorInit: String,
content: String,
},
})
export default class Editor extends Vue {
value = `
package main
import "fmt"
func main() {
fmt.println("HelloWorld")
}`
onChange() {
console.log("value");
}
async mounted() {
// add parameters here
}
}
</script>
<style scoped>
</style>
package.json
"dependencies": {
"#codemirror/lang-html": "^6.1.1",
"#codemirror/lang-javascript": "^6.1.0",
"#codemirror/lang-json": "^6.0.0",
"#monaco-editor/loader": "^1.3.2",
"codemirror": "^6.0.1",
"core-js": "^3.8.3",
"monaco-editor": "^0.34.0",
"monaco-editor-vue3": "^0.1.6",
"monaco-editor-webpack-plugin": "^7.0.1",
"monaco-languageclient": "^4.0.0",
"vscode-ws-jsonrpc": "^2.0.0",
"vue": "^3.2.13",
"vue-class-component": "^8.0.0-0",
"vue-codemirror": "^6.1.1",
"vue-monaco": "^1.2.2",
"vue-router": "4"
},
Enjoy!
The editor powering VSCode is open source and Microsoft provides examples on how to use it.
Demo:
var editor = monaco.editor.create(document.getElementById("container"), {
value: ["function x() {", '\tconsole.log("Hello world!");', "}"].join("\n"),
language: "javascript",
});
monaco.editor.setTheme("vs-dark");
body {
margin: 0;
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<link
rel="stylesheet"
data-name="vs/editor/editor.main"
href="https://unpkg.com/monaco-editor#0.34.0/min/vs/editor/editor.main.css"
/>
</head>
<body>
<div
id="container"
style="width: 800px; height: 600px; border: 1px solid grey"
></div>
<script>
var require = {
paths: {
vs: "https://unpkg.com/monaco-editor#0.34.0/min/vs",
},
};
</script>
<script src="https://unpkg.com/monaco-editor#0.34.0/min/vs/loader.js"></script>
<script src="https://unpkg.com/monaco-editor#0.34.0/min/vs/editor/editor.main.nls.js"></script>
<script src="https://unpkg.com/monaco-editor#0.34.0/min/vs/editor/editor.main.js"></script>
</body>
</html>
How does it not fit the bill?
I got it to work. There is not a file tree but there is a component for it. Just need to add a watcher to the file tress and have what is selected in the ManacoEditor. Here is a basic example to get the IDE to render in the browser.
<template>
<div>
<MonacoEditor
width="900"
height="750"
language="go"
#change="onChange"
:value="value"
></MonacoEditor>
</div>
</template>
<script lang="ts">
import {Options, Vue} from "vue-class-component";
import MonacoEditor from "monaco-editor-vue3";
#Options({
components: {
MonacoEditor,
},
props: {
editorInit: String,
content: String,
},
})
export default class Editor extends Vue {
value = `
package main
import "fmt"
func main() {
fmt.println("HelloWorld")
}`
onChange() {
console.log("value");
}
async mounted() {
// add parameters here
}
}
</script>
<style scoped>
</style>
package.json
"dependencies": {
"#codemirror/lang-html": "^6.1.1",
"#codemirror/lang-javascript": "^6.1.0",
"#codemirror/lang-json": "^6.0.0",
"#monaco-editor/loader": "^1.3.2",
"codemirror": "^6.0.1",
"core-js": "^3.8.3",
"monaco-editor": "^0.34.0",
"monaco-editor-vue3": "^0.1.6",
"monaco-editor-webpack-plugin": "^7.0.1",
"monaco-languageclient": "^4.0.0",
"vscode-ws-jsonrpc": "^2.0.0",
"vue": "^3.2.13",
"vue-class-component": "^8.0.0-0",
"vue-codemirror": "^6.1.1",
"vue-monaco": "^1.2.2",
"vue-router": "4"
},
I have installed vuelidate 2 to validate forms in my NuxtJS project. I followed instructions for installation and setup step by step according to vuelidate documentation. This is how my setup files look until now:
package.json
"dependencies": {
"#nuxtjs/axios": "^5.13.6",
"#vue/composition-api": "^1.2.2",
"#vuelidate/core": "^2.0.0-alpha.26",
"#vuelidate/validators": "^2.0.0-alpha.22",
"cookie-universal-nuxt": "^2.1.5",
"core-js": "^3.15.1",
"nuxt": "^2.15.7",
"uikit": "^3.7.1"
}
plugins/composition-api.js
import Vue from 'vue'
import VueCompositionAPI from '#vue/composition-api'
Vue.use(VueCompositionAPI)
and nuxt.config.js for #vue/composition-api
plugins: [
{ src: '~/plugins/composition-api.js' }
]
and finally this is how I'm using vuelidate inside my component:
<script>
import useVuelidate from '#vuelidate/core'
import { required } from '#vuelidate/validators'
export default {
setup () {
return { v$: useVuelidate() }
},
data () {
return {
contact: {
name: ''
}
}
},
validations () {
return {
contact: {
name: { required }
}
}
},
methods: {
submitForm () {
this.v$.$validate()
.then((isFormValid) => {
if (isFormValid) {
console.log('valid!!!')
} else {
return false
}
})
},
}
}
</script>
<template>
<label>
<input v-model="contact.name">
<div v-if="v$.contact.name.$error">Name is required.</div>
</label>
</template>
These are a couple of problems that occur:
when I place v-if="v$.contact.name.$error" inside template I get the error Cannot read property 'name' of undefined.
When I call submitForm method, the value of isFormValid is always false. Even when I have filled the contact.name field. And validation properties like $dirty don't change.
I have no idea why these happen. What am I doing wrong?
Update: (In case it might be useful to solve the problen) My console errors filter was unchecked by accident and I hadn't seen this Nuxt warning: [vue-composition-api] already installed. Vue.use(VueCompositionAPI) should be called only once.. As I searched about this error I found out Nuxt uses a dependency called Nuxt composition api which depends on #vue/composition-api. But when I reomved #vue/composition-api from plugins even the code inside setup didn't work correctly.
Solution with vuelidate:
create plugin (plugins/vuelidate.js):
import Vue from 'vue'
import Vuelidate from 'vuelidate'
Vue.use(Vuelidate)
nuxt.config:
plugins: [
{ src: '~/plugins/vuelidate' }
],
import:
import { required } from 'vuelidate/lib/validators'
method:
formSubmit() {
this.$v.$touch();
if (!this.$v.$invalid) {
// if invalid datas
}
},
template:
<h3
:class="{
'is-invalid': $v.contact.name.$error,
}"
>
Something
</h3>
My package.json:
"dependencies": {
"bootstrap": "^3.3.7",
"mathjax": "^2.7.2",
"vue": "^2.5.2",
"vue-moment": "^3.1.0",
"vue-router": "^3.0.1"
},
I have a component:
<template>
<div class="post--body" v-html="previewText" id="post--body"></div>
</template>
<script>
import MathJax from 'mathjax'
export default {
name: 'blog-post',
data () {
return {
post: {body: ""}
}
},
mounted() {
fetch("/api/post/" + this.$route.params.id)
.then(response => response.json())
.then(data => {
this.post = data;
})
this.$nextTick(function () {
console.log("tick")
MathJax.Hub.Typeset()
})
},
computed: {
previewText () {
return this.post.body
}
}
}
</script>
But I got "Uncaught SyntaxError: Unexpected token <" on MathMenu.js?V=2.7.2:1
How to properly use mathjax?
I don't think you can import mathjax, because if I console log imported mathjax , it shows empty object. I have gone through the folder directory also that doesn't seem importable. So you need to manually put the script src pointing to Mathjax.js
The way I currently use Mathjax in vue is by making a custom global component.
<template>
<span ref="mathJaxEl" v-html="data" class="e-mathjax"></span>
</template>
<script type="text/javascript">
export default{
props:['data'],
watch:{
'window.MathJax'(val){
this.renderMathJax()
},
'data'(val){
this.renderMathJax()
}
},
mounted(){
this.renderMathJax()
},
methods:{
renderMathJax(){
if(window.MathJax){
window.MathJax.Hub.Queue(["Typeset", window.MathJax.Hub,this.$refs.mathJaxEl]);
}
}
}
}
</script>
It can be made a bit better by using a variable to save boolean whether, mathjax has been rendered or not, as rendering gets called for two watch values, which both may get triggered in case of browser refresh.
So for MathJax v3, just add the following to your vue component
mounted(){
MathJax.typeset();
},
Now when navigating to pages via Vue router the math will render on component mount.
I keep getting the following error for some global components that I have:
Failed to mount component: template or render function not defined.
found in
---> <Paginator>
<Root>
This is what I have in package.json:
{
"version": "1.0.0",
"scripts": {
"dev": "cross-env NODE_ENV=development webpack",
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
},
"dependencies": {
"vue": "^2.4.4"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.0.0",
"babel-preset-vue-app": "^1.2.0",
"babel-plugin-transform-runtime": "^6.0.0",
"cross-env": "^5.0.5",
"css-loader": "^0.28.7",
"file-loader": "^1.1.4",
"fs": "^0.0.1-security",
"node-sass": "^4.5.3",
"sass-loader": "^6.0.6",
"vue-loader": "^13.0.5",
"vue-template-compiler": "^2.4.4",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.9.1"
}
}
In webpack.config i have the following defined:
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
},
extensions: ['*', '.js', '.vue', '.json']
},
And this is my main js file:
import Vue from 'vue';
Vue.component('paginator', require('./components/Paginator.vue'));
var vm = new Vue({
el: '#root',
data: {}
});
html:
<div id="root>
<paginator v-bind:total-items="totalItems" v-bind:page-size="query.pageSize" v-bind:page="query.page" v-on:pagechanged="onPageChange"></paginator>
</div>
any ideas why im getting this error?
When I change the code as follows is seems to work however I want to register the paginator as a global component:
import Vue from 'vue';
import Paginator from './components/Paginator.vue';
var vm = new Vue({
el: '#root',
components: {
'paginator': Paginator
},
data: {}
});
This is the paginator component:
<template>
<div>
<div >
<nav>
<ul class="pagination">
...
</ul>
</nav>
</div>
</div>
</template>
<script>
export default {
props: ['totalItems', 'pageSize', 'page'],
data: function () {
return {
currentPage: 1,
}
},
computed: {
pages: function () {
this.currentPage = this.page;
var pageArray = [];
var pagesCount = Math.ceil(this.totalItems / this.pageSize);
for (var i = 1; i <= pagesCount; i++)
pageArray.push(i);
return pageArray;
}
},
methods: {
changePage: function (page){
this.currentPage = page;
this.$emit('pagechanged', page);
},
previous: function (){
if (this.currentPage == 1)
return;
this.currentPage--;
this.$emit('pagechanged', this.currentPage);
},
next: function () {
if (this.currentPage == this.pages.length)
return;
this.currentPage++;
this.$emit('pagechanged', this.currentPage);
}
},
}
I believe this line is the issue - require inside the component declaration hasn't ended well for me when using it (although I've not looked into why).
Edit: See #DecadeMoon answer for info on this.
Vue.component('paginator', require('./components/Paginator.vue'));
Recommended way:
import Paginator from './components/Paginator.vue'
Vue.component('paginator', Paginator);
You can also do one of the following which will make webpack split the module into a separate file download asynchronously (this can be useful for large components)
Option 1:
Vue.component('paginator', () => import('./components/Paginator.vue'));
Option 2:
I have a load function that wraps this as it allows me to pass a string and wrap the directory without having to type it multiple times but this is a simple version of that function I use:
function load (component) {
return () => import(component)
}
so it would become:
Vue.component('paginator', load('./components/Paginator.vue'));
This is a consequence of the way Webpack combines require (CommonJS) and import (ES6) module import styles.
I always recommend using ES6-style import syntax.
If you must use require, then you will need to select the default import as follows:
Vue.component('paginator', require('./components/Paginator.vue').default);
^^^^^^^^
I recommend using ES6 import syntax instead (it's a standardized, non-webpack-specific syntax):
import Paginator from './components/Paginator.vue';
Vue.component('paginator', Paginator);
The reason why import() syntax works is because of this issue. Only use import() if you want the module to be split into a separate file and downloaded asynchronously by Webpack.