Vue 3 - use props string to load component - vue.js

I have following component:
<script setup>
import {computed, onMounted, ref, watch} from "vue";
import {useDialogStore} from "#/store/dialog";
import TableSwitcher from "#/components/Dialogs/Components/TableSwitcher.vue"
let emit = defineEmits(['confirmDialogConfirmed', 'confirmDialogClose'])
let dialogStore = useDialogStore()
let question = computed(() => dialogStore.dialogQuestion)
let mainComponent = ref('')
let props = defineProps({
open: {
type: Boolean,
required: true
},
id: {
type: String,
default: 'main-dialog'
},
component: {
type: String,
required: true,
}
})
watch(props, (newValue, oldValue) => {
mainComponent.value = props.component
console.log(mainComponent);
if(newValue.open === true)
{
dialog.showModal()
}
},
{
deep:true
}
);
let dialog = ref();
let closeDialog = (confirmAction = false) =>
{
dialog.close()
dialogStore.close(confirmAction)
}
onMounted(() => {
dialog = document.getElementById(props.id);
});
</script>
<template>
<dialog :id="id">
<component :is="mainComponent" ></component>
</dialog>
</template>
For activating component I am using this:
<main-dialog
v-if="component"
:component="component"
:open="true">
</main-dialog>
component value is created on click and passed as a prop to the main component. When I click to activate this component I am getting following error:
Invalid vnode type when creating vnode
When I hard code the component name for the mainComponent var the component is loaded correctly. What am I doing wrong here?

There are different ways to solve that. I think in your case it would make sense to use slots. But if you want to keep your approach you can globally define your components in your Vue app.
without slots
const app = createApp({});
// define components globally as async components:
app.component('first-component', defineAsyncComponent(async () => import('path/to/your/FirstComponent.vue'));
app.component('second-component', defineAsyncComponent(async () => import('path/to/your/SecondComponent.vue'));
app.mount('#app');
Then you can use strings and fix some bugs in your component:
Don’t use ids, instead use a template ref to access the dialog element
Use const instead of let for non-changing values.
props are already reactive so you can use the props also directly inside your template and they will be updated automatically when changed from the outside.
// inside <script setup>
import {computed, onMounted, ref, watch} from "vue";
import {useDialogStore} from "#/store/dialog";
import TableSwitcher from "#/components/Dialogs/Components/TableSwitcher.vue"
// use const instead of let, as the values are not changing:
const emit = defineEmits(['confirmDialogConfirmed', 'confirmDialogClose'])
const props = defineProps({
open: {
type: Boolean,
required: true
},
id: {
type: String,
default: 'main-dialog'
},
component: {
type: String,
required: true,
}
});
const dialogStore = useDialogStore()
const question = computed(() => dialogStore.dialogQuestion);
const dialog = ref(null);
watchPostEffect(
() => {
if(props.open) {
dialog.value?.showModal()
}
},
// call watcher also on first lifecycle:
{ immediate: true }
);
let closeDialog = (confirmAction = false) => {
dialog.value?.close()
dialogStore.close(confirmAction)
}
<!-- the sfc template -->
<dialog ref="dialog">
<component :is="props.component" />
</dialog>
with slots
<!-- use your main-dialog -->
<main-dialog :open="open">
<first-component v-if="condition"/>
<second-component v-else />
</main-dialog>
<!-- template of MainDialog.vue -->
<dialog ref="dialog">
<slot />
</dialog>

Related

dynamically highlight block with highlight.js in vue app

I have a VueJS where I have created a component for rendering the contents from a WYSIWYG component (tiptap).
I have the following content being returned from the backend
let x = 0;
enum A {}
function Baa() {}
I'm using highlight.js to highlight this code snippet in the following manner:
import { defineComponent, h, nextTick, onMounted, onUpdated, ref, watch } from 'vue';
// No need to use a third-party component to highlight code
// since the `#tiptap/extension-code-block-lowlight` library has highlight as a dependency
import highlight from 'highlight.js'
export const WYSIWYG = defineComponent({
name: 'WYSIWYG',
props: {
content: { type: String, required: true },
},
setup(props) {
const root = ref<HTMLElement>(null);
const highlightClass = 'hljs';
const hightlightCodes = async () => {
console.log(root.value?.querySelectorAll('pre code')[0]);
setTimeout(() => {
root.value?.querySelectorAll('pre code').forEach((el: HTMLElement) => {
highlight.highlightElement(el as HTMLElement);
});
}, 2000);
}
onMounted(hightlightCodes);
watch(() => props.content, hightlightCodes);
return function render() {
return h('div', {
class: 'WYSIWYG',
ref: root,
innerHTML: props.content
});
};
},
});
Now, when I visit the page by typing the URL in the browser, it highlights the typescript code
Whenever I visit a different page and click on my browser's "Go back" button, it makes the code completely vanishes
What I have tried
I can see that the line root.value?.querySelectorAll('pre code') is returning the correct items and the correct code is present but the code vanishes after the 2 seconds passes - due to setTimeout.
How can I make highlight.js highlight the code parts whenever props.content changes?
Option 1
Use Highlight.js Vue integration (you need to setup the plugin first, check the link):
<script setup>
const props = defineProps({
content: { type: String, required: true },
})
</script>
<template>
<highlightjs :code="content" language="ts" />
</template>
Option 2
Use computed to reactively compute highlighted HTML of props.content
Use sync highlight(code, options) function to get the highlighted HTML
Use HTML as-is via innerHTML prop or v-html directive
<script setup>
import { computed } from 'vue'
import highlight from 'highlight.js'
const props = defineProps({
content: { type: String, required: true },
})
const html = computed(() => {
const { value } = highlight.highlight(props.content, { lang: 'ts' })
return value
})
</script>
<template>
<div v-html="html" />
</template>

How can I change the locale of a VueI18n instance in a Vue file, and have it apply to all other Vue files in the application?

I am currently trying to implement a feature where a user can select a language from a dropdown menu in a Settings page (SettingsDialog.vue), updating all of the text to match the new language. This application has multiple Vue files like a MenuBar.vue, HelpDialog.vue, each pulling from translation.ts for their English translations. However, I noticed that selecting a language from the dropdown menu only changed the elements inside my SettingsDialog.vue file, not all of the other Vue files I have.
I tried using the Vue-I18n documentation implementation of changing locale globally in the file. I was expecting for the locale of the entire application to change after selecting a language in SettingsDialog.vue, applying my English translations in translation.ts to the Menu Bar, Help Page, etc. What happened is that the translations from translation.ts only applied to the SettingsDialog.vue page, no where else.
I guess it would be helpful to add that this is an Electron application, and the Vue files in the project use Quasar. Each file does have the correct import statements.
main.ts:
// ...
window.datalayer = [];
const i18n = createI18n({
legacy: false,
locale: "",
messages,
});
createApp(App)
.use(store, storeKey)
.use(router)
.use(
createGtm({
id: process.env.VUE_APP_GTM_CONTAINER_ID ?? "GTM-DUMMY",
vueRouter: router,
enabled: false,
})
)
.use(Quasar, {
config: {
brand: {
primary: "#a5d4ad",
secondary: "#212121",
},
},
iconSet,
plugins: {
Dialog,
Loading,
},
})
.use(ipcMessageReceiver, { store })
.use(markdownItPlugin)
.use(i18n)
.mount("#app");
SettingsDialog.vue
// ...
<!-- Language Setting Card -->
<q-card flat class="setting-card">
<q-card-actions>
<div id="app" class="text-h5">{{ $t("言語") }}</div>
</q-card-actions>
<q-card-actions class="q-px-md q-py-sm bg-setting-item">
<div id="app">{{ $t("言語を選択する") }}</div>
<q-space />
<q-select
filled
v-model="locale"
dense
emit-value
map-options
options-dense
:options="[
{ value: 'ja', label: '日本語 (Japanese)' },
{ value: 'en', label: '英語 (English)' },
]"
label="Language"
>
<q-tooltip
:delay="500"
anchor="center left"
self="center right"
transition-show="jump-left"
transition-hide="jump-right"
>
Test
</q-tooltip>
</q-select>
</q-card-actions>
</q-card>
// ...
<script lang="ts">
import { useI18n } from "vue-i18n";
// ...
setup(props, { emit }) {
const { t, locale } = useI18n({ useScope: "global" });
// ...
return {
t,
locale,
// ...
};
MenuBar.vue
<template>
<q-bar class="bg-background q-pa-none relative-position">
<div
v-if="$q.platform.is.mac && !isFullscreen"
class="mac-traffic-light-space"
></div>
<img v-else src="icon.png" class="window-logo" alt="application logo" />
<menu-button
v-for="(root, index) of menudata"
:key="index"
:menudata="root"
v-model:selected="subMenuOpenFlags[index]"
:disable="menubarLocked"
#mouseover="reassignSubMenuOpen(index)"
#mouseleave="
root.type === 'button' ? (subMenuOpenFlags[index] = false) :
undefined
"
/>
// ...
<script lang="ts">
import { defineComponent, ref, computed, ComputedRef, watch } from "vue";
import { useStore } from "#/store";
import MenuButton from "#/components/MenuButton.vue";
import TitleBarButtons from "#/components/TitleBarButtons.vue";
import { useQuasar } from "quasar";
import { HotkeyAction, HotkeyReturnType } from "#/type/preload";
import { setHotkeyFunctions } from "#/store/setting";
import {
generateAndConnectAndSaveAudioWithDialog,
generateAndSaveAllAudioWithDialog,
generateAndSaveOneAudioWithDialog,
} from "#/components/Dialog";
import { useI18n } from "vue-i18n";
import messages from "../translation";
type MenuItemBase<T extends string> = {
type: T;
label?: string;
};
export type MenuItemSeparator = MenuItemBase<"separator">;
export type MenuItemRoot = MenuItemBase<"root"> & {
onClick: () => void;
subMenu: MenuItemData[];
};
export type MenuItemButton = MenuItemBase<"button"> & {
onClick: () => void;
};
export type MenuItemCheckbox = MenuItemBase<"checkbox"> & {
checked: ComputedRef<boolean>;
onClick: () => void;
};
export type MenuItemData =
| MenuItemSeparator
| MenuItemRoot
| MenuItemButton
| MenuItemCheckbox;
export type MenuItemType = MenuItemData["type"];
export default defineComponent({
name: "MenuBar",
components: {
MenuButton,
TitleBarButtons,
},
setup() {
const { t } = useI18n({
messages,
});
// ...
};
const menudata = ref<MenuItemData[]>([
{
type: "root",
label: t("ファイル"),
onClick: () => {
closeAllDialog();
},
// ...
]);
translation.ts
const messages = {
en: {
// MenuBar.vue
ファイル: "File",
エンジン: "Engine",
ヘルプ: "Help",
// SettingDialog.vue
言語: 'Language',
言語を選択する: 'Select Language',
オフ: 'OFF',
エンジンモード: 'Engine Mode',
// HelpDialog.vue
ソフトウェアの利用規約: 'test',
}
};
export default messages;
Maybe there are more problems but now I see two:
Your menudata should be computed instead of just ref. Right now you are creating a JS object and setting it label property to result of t() call. When global locale changes this object is NOT created again. It still holds same value the t() function returned the only time it was executed - when setup() was running
// correct
const menudata = computed<MenuItemData[]>(() => [
{
type: "root",
label: t("ファイル"),
onClick: () => {
closeAllDialog();
},
// ...
]);
This way whenever i18n.global.locale changes, your menudata is created again with new translation
As an alternative, set label to key and use t(label) inside the template. However computed is much more effective solution...
You don't need to pass messages to useI18n() in every component. Only to the global instance. By passing config object into a useI18n() in a component you are creating Local scope which makes no sense if you are storing all translations in a single global place anyway

Extract modelValue logic to composable

I'm transitioning from Vue 2 to Vue 3 and I'm having trouble with composables.
I have a bunch of components that inherits modelValue. So, for every component that uses modelValue I'm writing this code (example with a radio input component):
<script setup>
import { computed } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: [String, null],
required: true
}
})
const computedValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
</script>
<template>
<label class="radio">
<input
v-model="computedValue"
v-bind="$attrs"
type="radio"
>
<slot />
</label>
</template>
Is there a way to reuse the code for the modelValue?
I've just done this while I'm playing with Nuxt v3.
You can create a composable like this:
import { computed } from 'vue'
export function useModel(props, emit) {
return computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
}
<template>
<input type="text" v-model="value" />
</template>
<script setup lang="ts">
const props = defineProps({
modelValue: String,
})
const emit = defineEmits(['update:modelValue'])
const value = useModel(props, emit)
</script>
For completion of #BghinC's perfect answer here the fully typed version:
Composable
File: #/composables/useModelValue.ts
import {computed} from 'vue'
export default function useModelValue<T>(
props: {
modelValue: T
[key: string]: unknown
},
emit: (event: 'update:modelValue', ...args: unknown[]) => void
) {
return computed({
get: () => props.modelValue,
set: (value: T) => emit('update:modelValue', value),
})
}
Usage
<script setup lang="ts">
import useModelValue from '#/composables/useModelValue'
const props = defineProps<{
modelValue: Dog
}>()
const emit = defineEmits(['update:modelValue'])
const dog = useModelValue<Dog>(props, emit)
</script>

Fill vue multiselect dropdown from another component

I am currently working on a project and could use some help.
I have a backend with an endpoint which delivers an array of strings with approximately 13k entries. I created a component in DropdownSearch.vue which should be used on several different views with differing inputs. For this specific purpose I used vueform/multiselect. If I only try to add the dropdown without any information it works perfectly. Also if I access the endpoints and console.log() it it will work properly and deliver me an output. But if I try to initialize the output to the dropdown the whole page will stop working, the endpoint won't give me a response and the application freezes.
DropdownSearch.vue
<div>
<Multiselect
class="float-left"
v-model="valueDropdownOne"
mode="tags"
:placeholder="selectorOne"
:closeOnSelect="false"
:searchable="true"
:createTag="true"
:options="dropdownOne"
:groups="true"
/>
<Multiselect
class="float-left"
v-model="valueDropdownTwo"
mode="tags"
:placeholder="selectorTwo"
:closeOnSelect="false"
:searchable="true"
:createTag="true"
:options="dropdownTwo"
/>
<Multiselect
class="float-left"
v-model="valueDropdownThree"
mode="tags"
:placeholder="selectorThree"
:closeOnSelect="false"
:searchable="true"
:createTag="true"
:options="dropdownThree"
/>
</div>
</template>
<script>
import Multiselect from "#vueform/multiselect";
import { ref }from "vue"
export default {
name: "DropdownSearch",
components: { Multiselect },
props: {
selectorOne: {
type: String,
default: "<DEFAULT VALUE>",
required: true,
},
selectorTwo: {
type: String,
default: "<DEFAULT VALUE>",
required: true,
},
selectorThree: {
type: String,
default: "<DEFAULT VALUE>",
required: true,
},
dropdownOne: {
type: Array
}
,
dropdownTwo: {
type: Array
},
dropdownThree: {
type: Array
}
},
setup() {
const valueDropdownOne = ref()
const valueDropdownTwo = ref()
const valueDropdownThree = ref()
return {valueDropdownOne, valueDropdownTwo, valueDropdownThree}
}
};
</script>
<style src="#vueform/multiselect/themes/default.css"></style>
Datenbank.vue
<template>
<div>
<DropdownSearch
selectorOne="Merkmale auswählen"
:dropdownOne="dropdownOne"
selectorTwo="Monographien auswählen"
:dropdownTwo="dropdownTwo"
selectorThree="Orte auswählen"
:dropdownThree="dropdownThree"
></DropdownSearch>
</div>
</template>
<script>
import DropdownSearch from "../components/DropdownSearch.vue";
import { ref, onMounted } from "vue";
export default {
components: { DropdownSearch },
setup() {
const dropdownOne = ref([]);
const dropdownTwo = ref([]);
const dropdownThree = ref([]);
const getPlaces = async () => {
const response = await fetch("http://127.0.0.1:5000/project/get-places");
const places = await response.json();
return places;
};
onMounted(async () => {
const places = await getPlaces();
dropdownThree.value = places;
});
return {
dropdownOne,
dropdownTwo,
dropdownThree
};
},
};
</script>
<style lang="scss" scoped></style>
it is not the problem of vue
the library you used may not support virtual-list, when the amount of data becomes large, the actual dom element will also become large
you may need to find another library support virtual-list, only render dom in visual range or implement a custom component by a virtual-library
I found a solution to the given problem, as #math-chen already stated the problem is the amount of data which will resolve in the actual Dom becoming really large. Rather than using virtual-lists, you can limit the amount of entries displayed which can easily be done by adding
limit:"10"
to the multiselect component, filtering all items can easily be handled by javascript itself.

How to use vnode in vue template

I want to call the dialog like this:
import demo from './demo.vue';
methods: {
open() {
const dialog = this.$dialog({
content: demo
});
}
}
dialog.js
import Vue from 'vue';
import QfDialog from './qf-dialog';
import ElementQfUI from 'element-qf-ui';
Vue.use(ElementQfUI);
let DialogConstructor = Vue.extend(QfDialog);
export const dialog = (params) => {
const instance = new DialogConstructor({
propsData: {
visible: true,
...params
}
});
instance.$mount();
document.body.appendChild(instance.$el);
return instance;
}
Vue.prototype.$dialog = dialog;
I tried to generate a VNode from a vue object to use in the template, but it gives me following error:
Error in render: "TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property '_renderProxy' closes the circle"
<template>
<el-dialog :visible.sync="visible" v-bind="$attrs" v-on="$listeners">
{{ contentTpl }}
</el-dialog>
</template>
<script>
export default {
name: 'qf-dialog',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
contentTpl: null
};
},
created() {
// this.content is a vue obj
let content = JSON.parse(JSON.stringify(this.content));
let vnode = this.$createElement('demo-cc', content);
this.contentTpl = [vnode];
}
};
</script>
How do I make {{contentTpl}} work ?
You are passing a whole Vue component into your Dialog - not a VNodes
Just use Dynamic Components
<template>
<el-dialog :visible.sync="visible" v-bind="$attrs" v-on="$listeners">
<component :is="content" />
</el-dialog>
</template>
Note that using :visible.sync is problematic as Vue does not allow to modify props the component receives