Vue 3: How to use the render function to render different components that themselves render different components with slots, events, etc? - vue.js

The title is basically my question. But the following code should make clearer what my goal is.
I have a version of this:
// AppItem.vue
<script>
import { h } from 'vue'
import { AppItem1 } from './item-1';
import { AppItem2 } from './item-2';
import { AppItem3 } from './item-3';
const components = {
AppItem1,
AppItem2,
AppItem3,
};
export default {
props: {
level: Number
},
render() {
return h(
components[`AppItem${this.level}`],
{
...this.$attrs,
...this.$props,
class: this.$attrs.class + ` AppItem--${this.level}`,
},
this.$slots
)
}
}
</script>
// AppItem1.vue
<template>
<AppBlock class="AppItem--1">
<slot name="header">
#1 - <slot name="header"></slot>
</slot>
<slot></slot>
</AppBlock>
</template>
// AppItem2.vue
<template>
<AppBlock class="AppItem--2">
<template #header>
#2 - <slot name="header"></slot>
</template>
<slot></slot>
</AppBlock>
</template>
// AppBlock.vue
<template>
<div class="AppBlock">
<div class="AppBlock__header">
<slot name="header"></slot>
</div>
<div class="AppBlock__body">
<slot></slot>
</div>
</div>
</template>
And my goal would be to use <AppItem> like...
<AppItem level="1">
<template #header>
Animal
</template>
<AppItem level="2">
<template #header>
Gorilla
</template>
<p>The gorilla is an animal...</p>
</AppItem>
<AppItem level="2">
<template #header>
Chimpanzee
</template>
<p>The Chimpanzee is an animal...</p>
</AppItem>
</AppItem>
...and have it render like...
<div class="AppBlock AppItem AppItem--1">
<div class="AppBlock__header">
Animal
</div>
<div class="AppBlock__body">
<div class="AppBlock AppItem AppItem--2">
<div class="AppBlock__header">
Gorilla
</div>
<div class="AppBlock__body">
<p>The gorilla is an animal...</p>
</div>
</div>
<div class="AppBlock AppItem AppItem--2">
<div class="AppBlock__header">
Chimpanzee
</div>
<div class="AppBlock__body">
<p>The Chimpanzee is an animal...</p>
</div>
</div>
</div>
</div>
Why does it not work? What I'm I misunderstaning?

The right way to get the AppItem--N component by name is to use the resolveComponent() function:
const appItem = resolveComponent(`AppItem${props.level}`);
I also fixed a couple of other small problems and had to rewrite the <setup script> to the setup() function. Now it works.
Playground
const { createApp, h, resolveComponent } = Vue;
const AppBlock = { template: '#appblock' }
const AppItem1 = { components: { AppBlock }, template: '#appitem1' }
const AppItem2 = { components: { AppBlock }, template: '#appitem2' }
const AppItem = {
components: {
AppItem1, AppItem2, AppBlock
},
props: {
level: Number
},
setup(props, { attrs, slots, emit, expose } ) {
const appItem = resolveComponent(`AppItem${props.level}`);
return () =>
h(appItem, {
...attrs,
...props,
class: (attrs.class ? attrs.class : '') + " AppItem--" + props.level,
}, slots);
}
}
const App = { components: { AppItem } }
const app = createApp(App)
app.mount('#app')
#app { line-height: 1; }
[v-cloak] { display: none; }
<div id="app">
<app-item :level="1">
<template #header>
<h4>Animal</h4>
</template>
<app-item :level="2">
<template #header>
Gorilla
</template>
<p>The gorilla is an animal...</p>
</app-item>
<app-item :level="2">
<template #header>
Chimpanzee
</template>
<p>The Chimpanzee is an animal...</p>
</app-item>
</app-item>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script type="text/x-template" id="appitem1">
<app-block class="AppItem">
<slot name="header">
#1 - <slot name="header"></slot>
</slot>
<slot></slot>
</app-block>
</script>
<script type="text/x-template" id="appitem2">
<app-block class="AppItem">
<template #header>
#2 - <slot name="header"></slot>
</template>
<slot></slot>
</app-block>
</script>
<script type="text/x-template" id="appblock">
<div class="AppBlock">
<div class="AppBlock__header">
<slot name="header"></slot>
</div>
<div class="AppBlock__body">
<slot></slot>
</div>
</div>
</script>

Related

How can I use a sidenavbar toggle function in another header component in Vue3

Im using Vue3 in Laravel 9 with Inertia.js and I´m trying to create a Sidenavbar with a headerbar.
I would like to toggle the Sidenavbar with a "Menu" Button in the Header Component.
But I have no idea how can i use the toggle function for my Sidenavbar in my headerbar.
The Toggle function in the Sidenavbar is working fine.
Screenshot with Header and Sidebar
Layout/App.vue
<template>
<Header />
<div class="app">
<Nav />
<slot />
</div>
</template>
<script>
import Nav from "./Nav";
import Header from "./header.vue";
export default {
components: { Nav, Header },
};
</script>
Sidenavbar Nav.vue
<template>
<aside :class="`${is_expanded ? 'is-expanded' : ''}`">
<h3>Menu</h3>
<div class="menu">
<router-link to="/" class="button">
<span class="material-symbols-rounded">home</span>
<span class="text">Home</span>
</router-link>
<router-link to="/about" class="button">
<span class="material-symbols-rounded">description</span>
<span class="text">About</span>
</router-link>
<router-link to="/team" class="button">
<span class="material-symbols-rounded">group</span>
<span class="text">Team</span>
</router-link>
<router-link to="/contact" class="button">
<span class="material-symbols-outlined">admin_panel_settings</span>
<span class="text">Administration</span>
</router-link>
</div>
<div class="flex"></div>
<div class="menu">
<router-link to="/settings" class="button ">
<span class="material-symbols-rounded">settings</span>
<span class="text">Settings</span>
</router-link>
</div>
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="ToggleMenu">
<span class="material-symbols-outlined menu-icon">menu</span>
<span class="material-symbols-outlined arrow-back">arrow_back</span>
</button>
</div>
</aside>
</template>
<script >
import {ref} from 'vue'
export default {
data() {
return {
is_expanded: ref(localStorage.getItem("is_expanded") === "true"),
visible: false
};
},
methods: {
ToggleMenu() {
this.is_expanded = !this.is_expanded;
localStorage.setItem("is_expanded", this.is_expanded);
}
},
mounted() {
console.log(`The initial count is ${this.is_expanded}.`);
}
}
</script>
Header Header.vue
<template>
<div class=" header border-2">
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="">
<span class="material-symbols-outlined menu-icon">menu</span>
</button>
</div>
</div>
</template>
<script>
export default {
}
</script>
Here is my app.js file
import { createApp, h } from 'vue'
import { createInertiaApp } from '#inertiajs/inertia-vue3'
export const toggleMenu = new Vue();
createInertiaApp({
resolve: name => require(`./pages/${name}`),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el)
},
})

Vue.JS: Communication with component

I have two component Header and AddUser component. Inside Header component I have a button that has function to show modal. And the modal is inside AddUser component like below.
Header.vue
<template>
<header>
<h2>{{title}}</h2>
<p>{{description}}</p>
<div class="text-center btn-class">
<button #click="showModal = true">Add Vehicle</button>
</div>
</header>
</template>
<script>
export default{
name: 'Header',
props: {
title: {
type: String,
default: 'Title!!',
},
description: {
type: String,
default: 'Description!!',
}
}
}
</script>
AddUser.vue
<template>
<div>
<transition name="fade" appear>
<div class="modal-overlay" v-if="showModal" #click="showModal = false"></div>
</transition>
<transition name="slide" appear>
<div class="modal" v-if="showModal">
<h3>Enter your vehicle detail</h3>
<div class="body">
<form #submit="onSubmit">
<div class="form-group">
<input type="text" v-model="name" name="name" class="form-control" placeholder="Enter your name">
</div>
<br><br>
<div class="form-btn form-group text-center">
<Button type="submit" text="Add" color="#2BA0A3" />
</div>
</form>
</div>
</div>
</transition>
</div>
</template>
<script>
import Button from './Button'
export default {
name: 'AddUser',
data(){
return {
showModal: false,
name: ''
}
},
components: {
Button
},
methods: {
onSubmit(e){
e.preventDefault()
if(!this.name){
alert('Please enter name.')
}
},
}
}
</script>
And this is my App.vue
<template>
<div class="container">
<Header />
<AddUser />
</div>
</template>
<script>
import Header from './components/Header'
import AddUser from './components/AddUser'
export default {
name: 'App',
components: {
Header,
AddUser,
}
}
</script>
The code works perfectly if I put button inside AddUser.vue but I want to keep the button on Header.vue and want it work also. How can I make it both component to communicate?
Move your showModal variable to your App.vue and pass it to your AddUser component as a prop. Inside your Header and AddUser components you will need to update the showModal value by emitting a show-modal-event with the value true or false depending on if you want to show or hide the modal. Listen for the event in App.vue and update it's local data showModal value with the $events value.
Here is an example. ** FYI this code isn't tested so there might be typos.
Header.vue
<template>
<header>
<h2>{{title}}</h2>
<p>{{description}}</p>
<div class="text-center btn-class">
<button #click="$emit('show-modal-event', true)">Add Vehicle</button>
</div>
</header>
</template>
<script>
export default{
name: 'Header',
props: {
title: {
type: String,
default: 'Title!!',
},
description: {
type: String,
default: 'Description!!',
}
}
}
</script>
AddUser.vue
<template>
<div>
<transition name="fade" appear>
<div class="modal-overlay" v-if="showModal" #click="$emit('show-modal-event', false)"></div>
</transition>
<transition name="slide" appear>
<div class="modal" v-if="showModal">
<h3>Enter your vehicle detail</h3>
<div class="body">
<form #submit="onSubmit">
<div class="form-group">
<input type="text" v-model="name" name="name" class="form-control" placeholder="Enter your name">
</div>
<br><br>
<div class="form-btn form-group text-center">
<Button type="submit" text="Add" color="#2BA0A3" />
</div>
</form>
</div>
</div>
</transition>
</div>
</template>
<script>
import Button from './Button'
export default {
name: 'AddUser',
data(){
return {
name: ''
}
},
props: {
showModal: {
type: Boolean,
default: false
}
},
components: {
Button
},
methods: {
onSubmit(e){
e.preventDefault()
if(!this.name){
alert('Please enter name.')
}
},
}
}
</script>
App.vue
<template>
<div class="container">
<Header #show-modal-event="showModal=$event" />
<AddUser #show-modal-event="showModal=$event" :showModal="showModal" />
</div>
</template>
<script>
import Header from './components/Header'
import AddUser from './components/AddUser'
export default {
name: 'App',
components: {
Header,
AddUser,
},
data(){
return {
showModal: false,
}
},
}
</script>

VueJS: Fetching image path from an API and displaying dynamic data and pictures

I am basically trying to fetching image paths and other data from an API and render data and pictures dynamically. I tried to nest the banner and the sidebar as a components in the Home.vue file which all worked fine after taking the html, css, js from an html template. Which all works fine. But, when I after fetching the data from my API and populate the it in the DOM it does populate but is not rendered. Am I missing something here?
Home.vue
<template>
<div>
<div class="body-content outer-top-xs" id="top-banner-and-menu">
<div class="container">
<div class="row">
<template>
<SideBar />
</template>
<div class="col-xs-12 col-sm-12 col-md-9 homebanner-holder">
<Banner />
<Scroller
:newPcts=newProducts
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import SideBar from '#/components/SideBar.vue'
import Banner from '#/components/Banner.vue'
import Scroller from '#/components/Scroller.vue'
// require('#/assets/images/sliders/01.jpg'),
// require('#/assets/images/sliders/02.jpg')
export default {
name: 'home',
components: {
SideBar,
Banner,
Scroller
},
data() {
return {
newProducts: this.getNewProducts(),
bannerImages: null,
}
},
methods:{
getNewProducts(){
// get all products
axios.get("http://localhost:4000/products")
.then((response) => {
this.newProducts = response.data
})
},
}
}
</script>
Banner.vue
<template>
<div id="hero">
<div id="owl-main" class="owl-carousel owl-inner-nav owl-ui-sm">
<div
class="item"
v-for="(image, index) in bannerImgs"
:key="index"
style="background-image: url(assets/images/sliders/02.jpg);"
>
<div class="container-fluid">
<div class="caption bg-color vertical-center text-left">
<div class="slider-header fadeInDown-1">Spring 2016</div>
<div class="big-text fadeInDown-1">
Women
<span class="highlight">Fashion</span>
</div>
<div class="excerpt fadeInDown-2 hidden-xs">
<span>
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut
odit aut fugit
</span>
</div>
<div class="button-holder fadeInDown-3"></div>
</div>
<!-- /.caption -->
</div>
<!-- /.container-fluid -->
</div>
</div>
<!-- /.owl-carousel -->
</div>
</template>
<script>
//require('#/assets/images/sliders/01.jpg')
export default {
data() {
return {
publicPath: process.env.BASE_URL,
bgimg: require('#/assets/images/sliders/01.jpg'),
bannerImgs: null
}
},
methods:{
getImgUrl(imgUrl) {
return require('#/'+imgUrl)
},
getBannerImages(){
// get active banners
fetch("http://localhost:4000/banners", {
method: "get"
})
.then((response) => {
return response.json()
})
.then((jsonData) => {
console.log(jsonData[0])
this.bannerImgs = jsonData[0]
// return jsonData[0]
})
}
},
created(){
// console.log("mounted ", this.publicPath)
// console.log(this.bannerImgs)
this.getBannerImages()
}
}
</script>
Scroller.vue
<template>
<div id="product-tabs-slider" class="scroll-tabs outer-top-vs wow fadeInUp">
<div class="more-info-tab clearfix">
<h3 class="new-product-title pull-left">New Products</h3>
<ul class="nav nav-tabs nav-tab-line pull-right" id="new-products-1">
<!-- <li class="active"> -->
<!-- <a data-transition-type="backSlide" href="#all" data-toggle="tab">All</a> -->
<!-- </li> -->
<!-- <li> -->
<!-- <a data-transition-type="backSlide" href="#smartphone" data-toggle="tab">Clothing</a> -->
<!-- </li> -->
</ul>
<!-- /.nav-tabs -->
</div>
<div class="tab-content outer-top-xs">
<!-- Tabs -->
<div class="tab-pane in active" id="all">
<div class="product-slider">
<div class="owl-carousel home-owl-carousel custom-carousel owl-theme" data-item="4">
<div
class="item item-carousel"
v-for="(item, index) in newPcts"
:key="index"
>
{{item.title}}
{{item.price}}
<!-- /.products -->
</div>
<!-- /.item -->
</div>
<!-- /.home-owl-carousel -->
</div>
<!-- /.product-slider -->
</div>
<!-- END TAB -->
</div>
<!-- /.tab-content -->
</div>
</template>
<script>
import axios from 'axios';
export default {
props:{
newPcts: Array
},
data(){
return {
// newProducts: []
}
},
methods:{
getProductImages(id){
let imgurl = "http://localhost:4000/mainpicture/"+id
axios.get(imgurl)
.then((response) => {
console.log(response.data)
return require('#/' + response.data)
})
}
},
computed:{
newProducts(){
axios.get("http://localhost:4000/products")
.then((response) => {
// this.newProducts = response.data
// console.log(response.data[0])
return response.data
})
}
},
async created() {
console.log('Scroller mounted')
console.log(this.newPcts)
},
mounted() {
console.log('Scroller mounted')
console.log(this.newPcts)
}
}
</script>

Passing events between Vue parent and child

I am trying to emit an event from a child and listen for it on the parent, but I am always getting errors like Invalid handler for event "new-tab": got undefined.
My app:
<div class="ibox" id="app">
<div class="ibox-content">
<h2>Table</h2>
<div class="details">
<table-tabs
v-on:new-tab="newTab"
v-on:close-tab="closeTab"
ref="tableTabs"
name="table-tabs"
></table-tabs>
</div>
</div>
</div>
TableTabs component:
<template>
<div>
<b-card no-body>
<b-tabs card>
<b-tab active>
<template slot="title">
<i class="fa fa-tablet-alt"></i> Orders
</template>
<orders-table
name="orders-table"
></orders-table>
</b-tab>
<b-tab v-for="order in tabs" :key="i">
<template slot="title">
<div>{{ order.name }}</div>
<b-button type="button" class="close float-right" aria-label="Close" #click="closeTab(order.id)">
<span aria-hidden="true">×</span>
</b-button>
</template>
<items-table
name="items-table"
:api-url="'/api/items/' + order.id"
></items-table>
</b-tab>
</b-tabs>
</b-card>
</div>
</template>
<script>
export default {
name: 'table-tabs',
data() {
return {
tabs: [],
}
},
methods: {
closeTab(id) {
for (let i = 0; i < this.tabs.length; i++) {
if (this.tabs[i].id === id) {
this.tabs.splice(i, 1);
}
}
},
newTab(item) {
this.tabs.push(item);
}
}
}
</script>
OrdersTable component:
<template>
<div>
<vuetable ref="vuetable"
:api-url="apiUrl"
:fields="fields"
pagination-path="pagination"
#vuetable:pagination-data="onPaginationData"
>
<div slot="name-slot" slot-scope="{ rowData }">
<a href="#" class="float-left" #click="newTab(rowData.order)">
{{ rowData.order.name }}
<span class="fa fa-search-plus"></span>
</a>
</div>
</vuetable>
<div class="row">
<div class="col-md-6">
<vuetable-pagination-info
ref="paginationInfo"
></vuetable-pagination-info>
</div>
<div class="col-md-6">
<vuetable-pagination-bootstrap
ref="pagination"
class="pull-right"
#vuetable-pagination:change-page="onChangePage"
></vuetable-pagination-bootstrap>
</div>
</div>
</div>
</template>
<script>
import Vuetable from 'vuetable-2/src/components/Vuetable';
import VuetablePaginationBootstrap from '../VuetablePaginationBootstrap';
import VuetablePaginationInfo from 'vuetable-2/src/components/VuetablePaginationInfo';
import TableFieldDef from './table-field-def';
export default {
name: 'orders-table',
components: {
Vuetable,
VuetablePaginationBootstrap,
VuetablePaginationInfo
},
data() {
return {
apiUrl: '/api/orders?include=items',
fields: TableFieldDef,
}
},
methods: {
newTab(order) {
this.$emit('new-tab', order);
}
}
}
</script>
Why does this.$emit('new-tab', order) always result in the handler newTab being undefined.
Vue 2.5.21
The parent in this case is the TableTabs component. If the parent needs to listen to events emitted by a child component then it needs to add the event listener to the child component, which is the OrdersTable component in this case. So instead of this ..
<table-tabs
v-on:new-tab="newTab"
v-on:close-tab="closeTab"
ref="tableTabs"
name="table-tabs"
></table-tabs>
You should do this (inside the TableTabs component) ..
<orders-table
name="orders-table"
v-on:new-tab="newTab"
></orders-table>

Vue: Data 'undefind' when component render

I would like to set a class to one of my elements when current language I equal to element id. But when method is fired to do this check then I have log "undefined".
Why? How to set that class properly?
Is data object loaded latter then component render?
<template>
<div>
<div class="star-box">
<div class="head">
<img :src="'images/flags/en.png'"
id="en"
class="student-img"
v-bind:class="{'activeLanguage': checkActiveLanguage('en')}"
alt=""
>
</div>
<div class="body">
<h5 class="heading">English</h5>
</div>
</div>
<div class="star-box">
<div class="head">
<img :src="'images/flags/de.png'"
id="de"
class="student-img"
v-bind:class="{'activeLanguage': checkActiveLanguage('de')}"
alt=""
>
</div>
<div class="body">
<h5 class="heading">Deutsch</h5>
</div>
</div>
</div>
</template>
JS
<script>
import {mapActions,mapGetters} from 'vuex';
export default {
name: 'Language',
data() {
return {
language: ''
}
},
methods: {
checkActiveLanguage: (lang)=> {
console.log(this.language);
if(lang==this.language) return true;
},
...mapGetters(['getCurrentLanguage']),
},
beforeMount(){
this.language = this.getCurrentLanguage();
}
}
</script>