Dynamic component tag in tsx - vue.js

How can I use a dynamic component name inside a tsx render function? This is what I'm after (not transpiling but just so you get the idea):
<script lang="tsx">
export default defineComponent({
props: {
hLevel: {type: String as PropType<'1'|'2'|'3'>,required: false,default: '3'}
},
setup(props) {
const tag=computed(() => `h${props.hLevel}`)
return () => {
<div>
{ <[tag.value]>title</[tag.value]> }
</div>
}
}
</script>
It works fine with a static string, e.g.
const HeadTag = 'h1'

Import your component and use it directly without [] :
return () => (
<div>
<tag.value>title</tag.value>
</div>)

Related

How to use pinia store with v-for directive whilst keeping it reactive?

I have a pinia data store similar to the following code snippet that stores user information and a list of individual orders he is placing:
order.js
import { defineStore } from 'pinia'
import { reactive } from 'vue'
export const useOrderStore = defineStore('order', {
state: () => ({
username: '',
orders: reactive([
{
id: '',
item: '',
price: ''
}
])
}),
})
Also I am using the v-for directive to render the components that should display the individual orders
OrdersComp.vue
<template>
<div class="orders_container">
<div v-for="(order, index) in orders" :key="order.id">
<OrderComp />
</div>
</div>
</template>
<script>
import { storeToRefs } from 'pinia'
import { useOrderStore } from "#/store/order";
setup() {
const { orders } = storeToRefs(useOrderStore())
return { orders };
},
</script>
How can I access the store data for the individual orders in the child component OrderComp
Basically I want something like this:
OrderComp.vue
<div>
<p>{{ orders.id }}</p>
<input v-model="orders.item" />
<input v-model="orders.price" />
</div>
<script>
import { storeToRefs } from 'pinia'
import { useOrderStore } from "#/store/order";
setup() {
const { orders } = storeToRefs(useOrderStore())
return { orders };
},
</script>
and still keep its reactive state? How does the child component know which order of the orders array to modify? Can/Should I combine the pinia data store with props that pass the data from parent to child? (Though this seems somewhat wrong for me, as pinia is probably able to replace all data passing between components) And furthermore as item and price are bound to input fields, they should of course dynamically change based on a user input.
Based on Estus Flasks comments I got it working by emitting events from the child OrderComp to the parent OrdersComp and on each change it invoked a function that modified my orders array at the correct index in the datastore.
So following the example above I did something like this:
order.js
import { defineStore } from 'pinia'
import { reactive } from 'vue'
export const useOrderStore = defineStore('order', {
state: () => ({
username: '',
orders: reactive([
{
id: '',
item: '',
price: ''
}
])
}),
actions: {
modifyOrder (id, order) {
var foundIndex = this.orders.findIndex(elem => elem.id == order.id)
this.orders[foundIndex] = order
}
}
})
OrdersComp.vue
<template>
<div class="orders_container">
<div v-for="(order, index) in order_store.orders" :key="order.id">
<OrderComp #change="order_store.modifyOrder(index, order)"
v-model:itemProp="order.item"
v-model:priceProp="order.price"
/>
</div>
</div>
</template>
<script>
import { useOrderStore } from "#/store/order";
export default {
setup() {
const order_store = useOrderStore()
return { order_store };
},
}
</script>
Note: I use a wrapper function here to emit the inputs, however you can of course emit it directly e.g. via #input/#change
OrderComp.vue
<div>
<input v-model="item" />
<input v-model="price" />
</div>
<script>
import { useModelWrapper } from "#/modelWrapper";
export default {
name: "OrderComp",´
props: {
itemProp: { type: String, default: "" },
priceProp: { type: String, default: "" },
},
emits: [
"update:itemProp",
"update:priceProp",
],
setup(props, { emit }) {
return {
item: useModelWrapper(props, emit, "itemProp"),
price: useModelWrapper(props, emit, "priceProp"),
};
},
}
modelWrapper.js
import { computed } from "vue";
export function useModelWrapper(props, emit, name = "modelValue") {
return computed({
get: () => props[name],
set: (value) => emit(`update:${name}`, value),
});
}

vue can not get component ref when using dynamic components and dynamic import

I have a Vue page with some components.
I want load components by dynamic configuration, but I always get component ref as undefined:
<template>
...
<template v-for="com in components">
<component :is="com.name" :ref="el => setComponents(com.key,el)"/>
</template>
</template>
<script>
...
components: {
Com1: () => import('/#/views/common/com1.vue'),
Com2: () => import('/#/views/common/com2.vue'),
},
setup() {
const components = [
{name: 'Com1', key: "Com1"},
{name: 'Com2', key: "Com2"},
];
const comRefs = {};
const setComponents = (key, el) => {
// el always get undefined
console.log('setComponents', key, el);
comRefs[key] = el;
}
return {
components,
setComponents
}
}
</script>
If I import components without dynamic, it works fine:
<script>
import Com1 from '/#/views/common/com1.vue';
import Com2 from '/#/views/common/com2.vue';
...
components: {
Com1,
Com2,
},
setup() {
const components = [
{name: 'Com1', key: "Com1"},
{name: 'Com2', key: "Com2"},
];
const comRefs = {};
const setComponents = (key, el) => {
// right now I can get the components refs
console.log('setComponents', key, el);
comRefs[key] = el;
}
return {
components,
setComponents
}
}
</script>
Is it possible to get dom refs with dynamic component and dynamic import ?
The ref is always undefined because the specified component is unknown (because of invalid declarations in the components option).
In Vue 3, async components must be declared with defineAsyncComponent():
import { defineAsyncComponent } from 'vue'
export default {
components: {
Com1: defineAsyncComponent(() => import('/#/views/common/com1.vue')),
Com2: defineAsyncComponent(() => import('/#/views/common/com2.vue')),
},
}
demo

How to use composition API to create a new component in vue3?

When we use vue2 to create API, we just follow options API like below:
data are in data
methods are in methods
<script>
export default {
name: 'demo',
components: {},
filter:{},
mixins:{},
props: {},
data(){
return{
}
},
computed:{},
watch:{},
methods: {},
}
</script>
But the vue3 changed, how should I build a component with vue3 composition API?
Some example say that I should import reactive etc. From vue first and put all codes in setup(){}?
Some example show that I can add setup to <script>?
Please give me an example.
ok bro , Composition Api works like that:
<script>
import { fetchTodoRepo } from '#/api/repos'
import {ref,onMounted} from 'vue'
export default {
setup(props){
const arr = ref([]) // Reactive Reference `arr`
const getTodoRepo = async () => {
arr.value = await fetchTodoRepo(props.todo)
}
onMounted(getUserRepo) // on `mounted` call `getUserRepo`
return{
arr,
getTodoRepo
}
}
}
</script>
There are two ways to create a component in vue3.
One:<script> + setup(){},such as this:
<script>
import { reactive, onMounted, computed } from 'vue'
export default {
props: {
title: String
},
setup (props, { emit }) {
const state = reactive({
username: '',
password: '',
lowerCaseUsername: computed(() => state.username.toLowerCase())
})
onMounted(() => {
console.log('title: ' + props.title)
})
const login = () => {
emit('login', {
username: state.username,
password: state.password
})
}
return {
login,
state
}
}
}
</script>
Two:use <script setup="props">
loading....

vue.js 2 single file component with dynamic template

I need a single file component to load its template via AJAX.
I search a while for a solution and found some hints about dynamic components.
I crafted a combination of a parent component which imports a child component and renders the child with a dynamic template.
Child component is this:
<template>
<div>placeholder</div>
</template>
<script>
import SomeOtherComponent from './some-other-component.vue';
export default {
name: 'child-component',
components: {
'some-other-component': SomeOtherComponent,
},
};
</script>
Parent component is this
<template>
<component v-if='componentTemplate' :is="dynamicComponent && {template: componentTemplate}"></component>
</template>
<script>
import Axios from 'axios';
import ChildComponent from './child-component.vue';
export default {
name: 'parent-component',
components: {
'child-component': ChildComponent,
},
data() {
return {
dynamicComponent: 'child-component',
componentTemplate: null,
};
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>
The dynamic template code fetched is this:
<div>
<h1>My dynamic template</h1>
<some-other-component></some-other-component>
</div>
In general the component gets its template as expected and binds to it.
But when there are other components used in this dynamic template (some-other-component) they are not recognized, even if they are correctly registered inside the child component and of course correctly named as 'some-other-component'.
I get this error: [Vue warn]: Unknown custom element: some-other-component - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Do I miss something or is it some kind of issue/bug?
I answer my question myself, because I found an alternative solution after reading a little bit further here https://forum.vuejs.org/t/load-html-code-that-uses-some-vue-js-code-in-it-via-ajax-request/25006/3.
The problem in my code seems to be this logical expression :is="dynamicComponent && {template: componentTemplate}". I found this approach somewhere in the internet.
The original poster propably assumed that this causes the component "dynamicComponent" to be merged with {template: componentTemplate} which should override the template option only, leaving other component options as defined in the imported child-component.vue.
But it seems not to work as expected since && is a boolean operator and not a "object merge" operator. Please somebody prove me wrong, I am not a JavaScript expert after all.
Anyway the following approach works fine:
<template>
<component v-if='componentTemplate' :is="childComponent"></component>
</template>
<script>
import Axios from 'axios';
import SomeOtherComponent from "./some-other-component.vue";
export default {
name: 'parent-component',
components: {
'some-other-component': SomeOtherComponent,
},
data() {
return {
componentTemplate: null,
};
},
computed: {
childComponent() {
return {
template: this.componentTemplate,
components: this.$options.components,
};
},
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>

Why dynamic component is not working in vue3?

Here is a working Vue2 example:
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Redone under Vue3 option does not work. No errors occur, but the component does not appear.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
Has anyone studied the vue2 beta version? Help me please. Sorry for the clumsy language, I use Google translator.
Leave everything in the template as in Vue2
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
Change only in "setup" using defineAsyncComponent
You can learn more about defineAsyncComponent here
https://labs.thisdot.co/blog/async-components-in-vue-3
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")) : '')
const onClick = () => {
isShow.value = true;
}
Try this
import DynamicComponent from './DynamicComponent.vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? DynamicComponent : '')
});
...
return {
state,
...
}
}
}
The issue with this seems to be to do with the way we register components when we use the setup script - see the official docs for more info. I've found that you need to register the component globally in order to reference it by string in the template.
For example, for the below Vue component:
<template>
<component :is="item.type" :item="item"></component>
</template>
<script setup lang="ts">
// Where item.type contains the string 'MyComponent'
const props = defineProps<{
item: object
}>()
</script>
We need to register the component in the main.ts, as such:
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent.vue'
var app = createApp(App);
app.component('MyComponent', MyComponent)
app.mount('#app')
Using 'watch' everything works.
<template>
<component :is="componentPath"/>
</template>
<script lang="ts">
import {defineComponent, ref, watch, SetupContext} from "vue";
export default defineComponent({
props: {
path: {type: String, required: true}
},
setup(props: { path: string }, context: SetupContext) {
const componentPath = ref("");
watch(
() => props.path,
newPath => {
if (newPath !== "")
import("#/" + newPath + ".vue").then(val => {
componentPath.value = val.default;
context.emit("loaded", true);
});
else {
componentPath.value = "";
context.emit("loaded", false);
}
}
);
return {componentPath};
}
});
</script>