How to render variable components in vue like jsx? - javascript

In jsx we can store components in variables like const comp=<p>Hello</p> and then we can put these variables anywhere, choosing witch component to render.
I was wondering if there is a similar thing in vue. If I had a template like:
<template>
<variable-comp />
</template>
I would like to change what variable-comp is dynamically. I'm aware of v-if and v-for but that's not the same thing.

vue uses this syntax for dynamic components
<component v-bind:is=”currentComponent”/>
where 'currentComponent' is the name (string) of component.
i.e.
<template>
<component v-bind:is=”currentComponent”/>
</template>
import CompA from './CompA.vue'
import CompB from './CompB.vue'
export default {
components: {
CompA,
CompB
},
data() {
isA: true
},
computed: {
currentComponent() {
return isA ? 'CompA' : 'CompB'
}
}
}

Related

Pass data attribute to vue 3 root instance [duplicate]

I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you're using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal Levý's answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')

Wrap received slots (vnodes) in anonymous component in Vue.js

I would like to receive two named slots slotOne and slotTwo. These are located in the child component at this.$scopedSlots.slotOne and this.$scopedSlots.slotTwo and contain vnodes. How can I wrap these slots (vnodes) in a new component so that I can conditionally render them like this:
Child Component:
<template>
<div>
<keep-alive>
<component :is="wrapperComponentContainingProperSlot"></component>
</keep-alive>
</div>
</template>
Parent Component:
<template>
<child>
<template v-slot:slotOne>
...
</template>
<template v-slot:slotTwo>
...
</template>
</child>
</template>
I'm guessing the core of this question is, how do I create a component from vnodes inside of another component?
I believe my motivation for desiring to implement this was based on the false premise that <keep-alive> is NOT destroyed when its parent is destroyed. This was not the case. Nevertheless, I did figure out how to wrap slots into their own components, both anonymous and named.
Child component:
<template>
<component :is="componentToRender"></component>
</template>
<script>
import Vue from 'vue';
export default {
computed: {
componentToRender() {
return this.showSlotOnesGlobally
? new this.SlotOneWrapperComponent(this)
: new this.SlotTwoWrapperComponent(this);
},
},
/* Assume a parent further up in the tree provided this data */
inject: {
showSlotOnesGlobally: 'showSlotOnesGlobally'
},
methods: {
SlotOneWrapperComponent(context) {
return Vue.component('SlotOneContentWrapper', {
render() {
return context.$scopedSlots.slotOne();
},
});
},
SlotTwoWrapperComponent(context) {
return Vue.component('SlotTwoContentWrapper', {
render() {
return context.$scopedSlots.slotTwo();
},
});
},
},
};
</script>
Parent component:
<template>
<child>
<template v-slot:slotOne>
...
</template>
<template v-slot:slotTwo>
...
</template>
</child>
</template>
To make them anonymous components, simply replace Vue.component('SlotOneContentWrapper', and Vue.component('SlotTwoContentWrapper', with Vue.extend(.
If anyone can offer a more concise solution, that would be wonderful.

How to throw data to main App.vue from views? [duplicate]

I have two components:
App.vue
Sidekick.vue
In my App.vue component, I have a property that I would like to access from Sidekick.vue
App.vue
<template>
<div id="app">
<p>{{ myData }}</p>
<div class="sidebar">
<router-view/> // our sidekick component is shown here
</div>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
myData: 'is just this string'
}
}
}
</script>
Sidekick.vue
<template>
<div class="sidekick">
{{ myData }}
</div>
</template>
<script>
export default {
name: 'Sidekick'
}
</script>
I would like access to myData (which is declared in App.vue) from Sidekick.vue
I have tried importing App.vue from within Sidekick.vue by doing something like:
Sidekick.vue (incorrect attempt)
<script>
import App from '#/App'
export default {
name: 'Sidekick',
data () {
return {
myData: App.myData
}
}
}
</script>
I have read about props - but have only seen references to child / parent components. In my case, Sidekick.vue is shown in a div inside App.vue (not sure if this makes it a "child"). Do I need to give access of myData to <router-view/> somehow?
UPDATE: (to show relationship between App.vue and Sidekick.vue
index.js (router file)
import Vue from 'vue'
import Router from 'vue-router'
import Sidekick from '#/components/Sidekick',
import FakeComponent from '#/components/FakeComponent'
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
redirect: '/fakecomponent'
},
{
path: '/sidekick',
name: 'Sidekick',
component: Sidekick
},
{
path: '/fakecomponent',
name: 'FakeComponent',
component: FakeComponent
}
]
})
export default router
Sidekick.vue gets rendered when we hit /sidekick
Just keep in mind, the rule of thumb is using props to pass data in a one-way flow
props down, events up.
https://v2.vuejs.org/v2/guide/components.html#Composing-Components
Quick solution:
Global event bus to post messages between your <App/> and <Sidekick/> components.
https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
Long term solution:
Use a state management library like vuex to better encapsulates data in one place (a global store) and subscribe it from your components tree using import { mapState, mapMutations } from 'vuex'
When you have parent-child communication, the best and recommended
option is to use props and events. Read more in Vue docs
When want to have shared state between many components the best and
recommended way is to use Vuex.
If you want to use simple data sharing you can use Vue observable.
Simple example: Say that you have a game and you want the errors to be accessible by many components. (components can access it and manipulate it).
errors.js
import Vue from "vue";
export const errors = Vue.observable({ count: 0 });
Component1.vue
import { errors } from 'path-of-errors.js'
export default {
computed: {
errors () {
get () { return errors.count },
set (val) { errors.count = val }
}
}
}
In Component1 the errors.count is reactive. So if as a template you have:
<template>
<div>
Errors: {{ errors }}
<button #click="errors++">Increase</button>
</div>
</template>
While you click the Increase button, you will see the errors increasing.
As you might expect, when you import the errors.js in another component, then both components can participate on manipulating the errors.count.
Note: Even though you might use the Vue.observable API for simple data sharing you should be aware that this is a very powerful API. For example read Using Vue Observables as a State Store
App.vue:
<router-view pass_data='myData'/>
Sidekick.vue:
export default {
name: "Sidekick",
props: ["pass_data"],
created() {
alert("pass_data: "+this.pass_data)
}
}
If App.js(Parent) and Sidekick(Child)
App.js
in Template
In script
import Sidekick from './Sidekick.vue:
Sidekick.vue
props: ['myData']
now you can access myData anywhere in sidekick.
In template myData and
in scripts this.myData

Vuejs Share Data Between Components

I have two components:
App.vue
Sidekick.vue
In my App.vue component, I have a property that I would like to access from Sidekick.vue
App.vue
<template>
<div id="app">
<p>{{ myData }}</p>
<div class="sidebar">
<router-view/> // our sidekick component is shown here
</div>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
myData: 'is just this string'
}
}
}
</script>
Sidekick.vue
<template>
<div class="sidekick">
{{ myData }}
</div>
</template>
<script>
export default {
name: 'Sidekick'
}
</script>
I would like access to myData (which is declared in App.vue) from Sidekick.vue
I have tried importing App.vue from within Sidekick.vue by doing something like:
Sidekick.vue (incorrect attempt)
<script>
import App from '#/App'
export default {
name: 'Sidekick',
data () {
return {
myData: App.myData
}
}
}
</script>
I have read about props - but have only seen references to child / parent components. In my case, Sidekick.vue is shown in a div inside App.vue (not sure if this makes it a "child"). Do I need to give access of myData to <router-view/> somehow?
UPDATE: (to show relationship between App.vue and Sidekick.vue
index.js (router file)
import Vue from 'vue'
import Router from 'vue-router'
import Sidekick from '#/components/Sidekick',
import FakeComponent from '#/components/FakeComponent'
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
redirect: '/fakecomponent'
},
{
path: '/sidekick',
name: 'Sidekick',
component: Sidekick
},
{
path: '/fakecomponent',
name: 'FakeComponent',
component: FakeComponent
}
]
})
export default router
Sidekick.vue gets rendered when we hit /sidekick
Just keep in mind, the rule of thumb is using props to pass data in a one-way flow
props down, events up.
https://v2.vuejs.org/v2/guide/components.html#Composing-Components
Quick solution:
Global event bus to post messages between your <App/> and <Sidekick/> components.
https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
Long term solution:
Use a state management library like vuex to better encapsulates data in one place (a global store) and subscribe it from your components tree using import { mapState, mapMutations } from 'vuex'
When you have parent-child communication, the best and recommended
option is to use props and events. Read more in Vue docs
When want to have shared state between many components the best and
recommended way is to use Vuex.
If you want to use simple data sharing you can use Vue observable.
Simple example: Say that you have a game and you want the errors to be accessible by many components. (components can access it and manipulate it).
errors.js
import Vue from "vue";
export const errors = Vue.observable({ count: 0 });
Component1.vue
import { errors } from 'path-of-errors.js'
export default {
computed: {
errors () {
get () { return errors.count },
set (val) { errors.count = val }
}
}
}
In Component1 the errors.count is reactive. So if as a template you have:
<template>
<div>
Errors: {{ errors }}
<button #click="errors++">Increase</button>
</div>
</template>
While you click the Increase button, you will see the errors increasing.
As you might expect, when you import the errors.js in another component, then both components can participate on manipulating the errors.count.
Note: Even though you might use the Vue.observable API for simple data sharing you should be aware that this is a very powerful API. For example read Using Vue Observables as a State Store
App.vue:
<router-view pass_data='myData'/>
Sidekick.vue:
export default {
name: "Sidekick",
props: ["pass_data"],
created() {
alert("pass_data: "+this.pass_data)
}
}
If App.js(Parent) and Sidekick(Child)
App.js
in Template
In script
import Sidekick from './Sidekick.vue:
Sidekick.vue
props: ['myData']
now you can access myData anywhere in sidekick.
In template myData and
in scripts this.myData

Is it possible to pass a component as props and use it in a child Component in Vue?

In a Vue 2.0 app, let's say we have components A, B and C.
A declares, registers and uses B
Is it possible to pass C from A to B?
Something like this:
<template>
<div class="A">
<B :child_component="C" />
</div>
</template>
And use C in B somehow.
<template>
<div class="B">
<C>Something else</C>
</div>
</template>
The motivation: I want to create a generic component B that is used in A but receives from A its child C. Actually A will use B several times passing different 'C's to it.
If this approach is not correct, what is the proper way of doing it in Vue?
Answering #Saurabh
Instead of passing as props, I tried the suggestion inside B.
<!-- this is where I Call the dynamic component in B -->
<component :is="child_component"></component>
//this is what I did in B js
components: {
equip: Equipment
},
data () {
return {
child_component: 'equip',
_list: []
}
}
Basically I'm trying to render Equipment, but the dynamic way
I get 3 errors in console and a blank page
[Vue warn]: Error when rendering component at /home/victor/projetos/tokaai/public/src/components/EquipmentFormItem.vue:
Uncaught TypeError: Cannot read property 'name' of undefined
TypeError: Cannot read property 'setAttribute' of undefined
Apparently I'm doing something wrong
Summing up:
<!-- Component A -->
<template>
<div class="A">
<B>
<component :is="child_component"></component>
</B>
</div>
</template>
<script>
import B from './B.vue';
import Equipment from './Equipment.vue';
export default {
name: 'A',
components: { B, Equipment },
data() {
return { child_component: 'equipment' };
}
};
</script>
<!-- Component B -->
<template>
<div class="B">
<h1>Some content</h1>
<slot></slot> <!-- Component C will appear here -->
</div>
</template>
You can use special attribute is for doing this kind of thing. Example of dynamic component and its usage can be found here.
You can use the same mount point and dynamically switch between multiple components using the reserved element and dynamically bind to its is attribute.
Here's how is can be used with either an imported component or one passed as a prop:
<template>
<div class="B">
<component :is="myImportedComponent">Something</component>
--- or ---
<component :is="myPassedComponent">Something else</component>
</div>
</template>
<script>
import myImportedComponent from "#/components/SomeComponent.vue"
export default {
props: {
myPassedComponent: Object
},
components: {
myImportedComponent
},
}
</script>
Here's solution to forward custom component through props of another component
:is is special attribute and it will be used to replace your actual component and it will be ignored if you try to use it as a prop in your component. Luckily you can use something else like el and then forward this to component like so:
<template>
<div>
<component :is="el">
<slot />
</component>
</div>
</template>
<script>
export default {
name: 'RenderDynamicChild',
props: {
el: {
type: [String, Object],
default: 'div',
},
},
}
</script>
Any valid element you use in el attribute will be used as a child component. It can be html or reference to your custom component or div by default as specified in component declaration.
Passing custom component to prop is little bit tricky. One would assume you declare in a components property of parent component and then use it for el attribute but this doesn't work. Instead you need to have your dynamic component in data or computed property so you can use it in a template as a prop. Also note AnotherComponent doesn't need to be declared in components property.
<template>
<RenderDynamicChild :el="DynamicComponent">
Hello Vue!
</RenderDynamicChild>
</template>
<script>
import RenderDynamicChild from './DynamicChild';
import AnotherComponent from './AnotherComponent';
export default {
name: "ParentComponent",
components: { DynamicChild },
data() {
return {
DynamicComponent: AnotherComponent,
};
},
};
</script>
Using computed property for your dynamic component allows you to switch between components easily:
<script>
import DynamicChild from './DynamicChild';
import AnotherComponent from './AnotherComponent';
export default {
name: "ParentComponent",
components: { DynamicChild },
data() { return { count: 0 } },
computed: {
DynamicComponent() {
return this.count % 2 > 1 ? AnotherComponent : 'article';
},
},
};
</script>
Increase this.count to alternate between AnotherComponent and simple article html element.
Maybe it's too late to answer this question. But I think it could help others with this same issue.
I've been looking for a way to pass components throw others in vue, but it looks that VUE3 have a approach for that using named slots:
Here it's the documentation about that:
https://v3.vuejs.org/guide/component-slots.html#named-slots
Basically you can have:
<template>
<div class="A">
<slot name="ComponentC"></slot> <!-- Here will be rendered your ComponentC -->
</div>
<div class="A">
<slot name="ComponentD"></slot> <!-- Here will be rendered your ComponentD -->
</div>
<div class="A">
<slot></slot> <!-- This is going to be children components -->
</div>
</template>
And from your B component
<template>
<div class="B">
<A>
<template v-slot:ComponentC>
<h1>Title of ComponentC </h1>
</template>
<template v-slot:ComponentD>
<h1>Title of ComponentD </h1>
</template>
<template v-slot:default>
<h1>Title of child component </h1>
</template>
</A>
</div>
</template>
If you would like to use another component within your functional component you can do the following:
<script>
import Vue from 'vue'
import childComponent from './childComponent'
Vue.component('child-component')
export default {}
</script>
<template functional>
<div>
<child-component/>
</div>
</template>
Reference:
https://github.com/vuejs/vue/issues/7492#issue-290242300
If you mean Dynamically importing a component in a parent component, so yes, you can do that in Vue3 using:
<component :is="child_component" />
but to render "child_component" itself dynamically, you can use
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
Let me give you an example:
let's say you have several multiple child components (ChildA, ChildB, ChildC) that you want to load dynamically based on what you pass to the parent component (Parent), so the Parent component will be something like this:
Parent
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
const props = defineProps<{
childComponent?: string;
}>();
const AsyncComp = defineAsyncComponent(() =>
import(`./${props.childComponent}.vue`)
)
</script>
<template>
<component :is="AsyncComp"/>
</template>
and then you can call the Parent component dynamically wherever you want like this:
<Parent :childComponent="child-a"/>
<Parent :childComponent="child-b"/>
<Parent :childComponent="child-c"/>
For a better explanation, you can check this article:
https://medium.com/#pratikpatel_60309/dynamic-importing-component-templates-with-vue-js-78d2167db1e7

Categories

Resources