Executing js on slot - vue.js

I'm a beginner in web development and I'm trying to help out friends restarting an old game. I'm in charge of the tooltip component but I hit a wall...
There are many Vue components and in a lot of them I want to call a child component named Tooltip, I'm using vue-tippy for easy configuration. This is the component:
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<slot name='tooltip-content'>
</slot>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods:{
formatContent(value) {
if (! value) return '';
return formatText(value.toString());
}
},
}
</script>
In one of the other components I try to use the tooltip:
<template>
<a class="action-button" href="#">
<Tooltip>
<template #tooltip-trigger>
<span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="#/assets/images/pm.png" alt="mp"></span>
<span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="#/assets/images/pa.png" alt="ap"></span>
<span v-if="action.canExecute">{{ action.name }}</span>
<span v-else><s>{{ action.name }}</s></span>
<span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
</template>
<template #tooltip-content>
<h1>{{action.name}}</h1>
<p>{{action.description}}</p>
</template>
</Tooltip>
</a>
</template>
<script>
import Tooltip from "#/components/Utils/ToolTip";
export default {
props: {
action: Object
},
components: {Tooltip}
};
</script>
From here everything is fine, the tooltip is correctly displayed with the proper content.
The thing is, the text in the {{ named.description }} needs to be formatted with the formatContent content. I know I can use the props, the components would look like that:
Tooltip.vue:
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<h1 v-html="formatContent(title)" />
<p v-html="formatContent(content)"/>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods:{
formatContent(value) {
if (! value) return '';
return formatText(value.toString());
}
},
props: {
title: {
type: String,
required: true
},
content: {
type: Array,
required: true
}
}
}
</script>
Parent.vue:
<template>
<a class="action-button" href="#">
<Tooltip :title="action.name" :content="action.description">
<template v-slot:tooltip-trigger>
<span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="#/assets/images/pm.png" alt="mp"></span>
<span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="#/assets/images/pa.png" alt="ap"></span>
<span v-if="action.canExecute">{{ action.name }}</span>
<span v-else><s>{{ action.name }}</s></span>
<span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
</template>
</Tooltip>
</a>
</template>
<script>
import Tooltip from "#/components/Utils/ToolTip";
export default {
props: {
action: Object
},
components: {Tooltip}
};
</script>
But I need to use a slot in the tooltip component because we'll have some "extensive" lists with v-for.
Is there a way to pass the data from a slot into a JS function?

If I understand you correctly, you're looking for scoped slots here.
These will allow you to pass information (including methods) from child components (the components with <slot> elements) back to the parents (the component(s) filling those slots), allowing parents to use chosen information directly in the slotted-in content.
In this case, we can give parents access to formatContent(), which will allow them to pass in content that uses it directly. This allows us to keep the flexibility of slots, with the data passing of props.
To add this to your example, we add some "scope" to your content slot in Tooltip.vue. This just means we one or more attributes to your <slot> element, in this case, formatContent:
<!-- Tooltip.vue -->
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<!-- Attributes we add or bind to this slot (eg. formatContent) -->
<!-- become available to components using the slot -->
<slot name='tooltip-content' :formatContent="formatContent"></slot>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods: {
formatContent(value) {
// Rewrote as a ternary, but keep what you're comfortable with
return !value ? '' : formatText(value.toString());
}
},
}
</script>
Now that we've added some scope to the slot, parents filling the slot with content can use it by invoking a slot's "scope":
<!-- Parent.vue -->
<template>
<a class="action-button" href="#">
<Tooltip>
. . .
<template #tooltip-content="{ formatContent }">
<!-- Elements in this slot now have access to 'formatContent' -->
<h1>{{ formatContent(action.name) }}</h1>
<p>{{ formatContent(action.description) }}</p>
</template>
</Tooltip>
</a>
</template>
. . .
Sidenote: I prefer to use the destructured syntax for slot scope, because I feel it's clearer, and you only have to expose what you're actually using:
<template #tooltip-content="{ formatContent }">
But you can also use a variable name here if your prefer, which will become an object which has all your slot content as properties. Eg.:
<template #tooltip-content="slotProps">
<!-- 'formatContent' is now a property of 'slotProps' -->
<h1>{{ slotProps.formatContent(action.name) }}</h1>
<p>{{ slotProps.formatContent(action.description) }}</p>
</template>
If you still need the v-html rendering, you can still do that in the slot:
<template #tooltip-content="{ formatContent }">
<h1 v-html="formatContent(title)" />
<p v-html="formatContent(content)"/>
</template>

Related

Vue3, Render parent div only if slot inside of it has content

I'm trying to render a header only if there's a text or a populated slot inside of it.
I tried:
<div
class="flex py-sm px-md w-full align-middle rounded-t-xl"
v-if="props.title || $slots.header"
:class="[`bg-${props.headerColor}`]"
>
<p class="text-bo-xl font-bold" :class="`text-${props.titleTextColor}`">
{{ props.title }}
</p>
<slot name="header"></slot>
</div>
But the div renders anyway, even if the slot is empty. I think it considers the slot present even if it's not populated.
Any ideas?
sorry for the late response. If I utilize computed() it seems to work:
<template>
<section>
<h4>lorem</h4>
<div v-if="hasHeaderSlot">
<h2>ipsum</h2>
<slot name="header"></slot>
</div>
</section>
</template>
<script>
import { computed } from 'vue';
export default {
setup(_, { slots }) {
const hasHeaderSlot = computed(() => slots.header && slots.header());
return {
hasHeaderSlot,
};
},
};
</script>
I hope this helps. With best regards

VUE - load component only if slot exist

I would like to load a component into a slot only if the slot exists (because I don't want to waste resources creating and mounting the component).
So I did it like that:
Test.vue
<slot :load="true" name="slotNo1"/
Wrapper.vue
<template slot="slotNo1" slot-scope="{ load }"> <Test v-if="load" /> </template>
But I wonder if there is an easier way without having to create the "load" variable.
Refer this
Vue.component('Custom', {
template: `
<div>
<span>always displayed</span>
<strong v-if="hasSlotData">
displayed only when slot passed: <slot></slot>
</strong>
</div>
`,
computed: {
hasSlotData() {
return this.$slots.default;
}
}
});
new Vue({
el: '#root'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="root">
<custom></custom>
<custom>content</custom>
</div>
If you wanna control parent, this may help you https://michaelnthiessen.com/advanced-vue-controlling-parent-slots

Implementing a reusable tabbed component using Vuejs

I'm trying to implement a tabbed reusable component in vueJs but I'm getting an error that a particular component is not defined. Below are both components
//TabComponent
<template>
<div>
<div class="row">
<div class="col-lg-12 col-xl-12">
<div class="card-box">
<ul class="nav nav-tabs nav-bordered">
<li v-for="tab in tabs" :key="tab" class="nav-item">
{{tab}}
</li>
</ul>
<div class="tab-content">
<component :is="selectedComponent"></component>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TabComponent',
props: [ selectedComponent, tabs ] //The error is coming from this line
}
</script>
I have imported it to this component and currently it shows the error
Uncaught ReferenceError: selectedComponent is not defined
//Entitlements component
<template>
<div>
<tab-component :tabs="tabs" :selectedComponent="selectedComponent" />
</div>
</template>
<script>
import TabComponent from "../../../components/TabComponent";
import List from "./Entitlements/List";
import MyEntitlements from "./Entitlements/MyEntitlements";
export default {
name: 'Entitlements',
components: {List, MyEntitlements, TabComponent},
data(){
return{
tabs: ['List', 'MyEntitlements'],
selectedComponent: 'List',
}
}
}
</script>
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 (source)
Try with:
<tab-component :tabs="tabs" :selected-component="selectedComponent" />
Edit:
If you define props as an array, change the list with strings (see "Prop types" here):
props: [ 'selectedComponent', 'tabs' ]

Vue how to customize global navbar at view level

Im super new to Vue.
i have a Vue-CLI app, which have a navbar and content.
Navbar is common to all pages, but i want to customize in each page whit some additional content.
Example:
Common-> home | about
View home -> home | about | your are in view home
View about -> home | about | your are in view about
router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import NavBar from '#/components/NavBar.vue';
Vue.use(VueRouter);
Vue.component('nav-bar', NavBar);
//...
components/navbar.vue
<template>
<div>
<b-nav-item to="/">home</b-nav-item>
<b-nav-item to="/about">about</b-nav-item>
{{customContent}}
</div>
</template>
<script>
export default {
name: 'NavBar',
props: {
customContent: {
type: String,
default: 'default Content',
},
},
};
</script>
App.vue
<template>
<div id="app">
<nav-bar />
<div class="container-fluid">
<router-view />
</div>
</div>
</template>
views/home.vue
<template>
<div class="row">
<div class="col-12">
<image-card :images="images"/>
</div>
</div>
</template>
<script>
//how can i customize here the navbar by adding for example 'your are in view home'???
</script>
Thanks so much!
There are a few ways in which you can solve this problem. I'll list two of them.
1. Update NavBar by $route
In this approach, the NavBar component already contains all of the possible combinations, and will display the relevant portion(s) depending on what $route contains.
Here's some pseudo code:
navbar.vue
<template>
<div class="navbar">
<div class="navbar-left>
APPNAME
</div>
<div v-if="name === 'landing'">
...
</div>
<div v-else-if="name === 'room'">
...
</div>
</div>
</template>
App.vue
<template>
<div id="app">
<NavBar :name="$route.name"/>
<main>
<router-view/>
</main>
</div>
</template>
In this example, the NavBar component is very rigid, and doesn't really lend itself to much reuse. However, it does encapsulate all the relevant code relating to the nav bar.
2. Extensible NavBar with slots
In this approach, the NavBar only provides the bare-minimum to create a nav bar. The rest of the route-specific elements are to be filled in by the views.
navbar.vue
<template>
<div class="navbar">
<div class="navbar-left">
<div class="navbar-brand">
APPNAME
</div>
<slot name="left"></slot>
</div>
<div class="navbar-right">
<slot name="right"></slot>
</div>
</div>
</template>
App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
landing.vue
<template>
<div>
<header>
<NavBar>
<template slot="right">
<span>
<div class="navbar-item">
<div class="buttons">
<button class="button" #click="...">Start Watching</button>
</div>
</div>
</span>
</template>
</NavBar>
</header>
<main>
...
</main>
</div>
</template>
This approach has a bit of repetition in terms of DOM elements, but gives you an extremely flexible NavBar that can be customized by each view.
The approach you want to use depends on what is important to you.
If strict encapsulation is what you want, then you may want to use approach 1, as all of the NavBar-related code is contained within a single file.
However, if you believe that there is a potential for reuse, or if you would like all view-related code to live in one place, then it makes sense to use slots instead and extend the NavBar as required by each view.
I use a breadcrumb to achieve a similar thing. Just an idea but Vue router allows you to add meta data to the current route which you always have access to
router.js
path: '/add',
name: 'add',
component: () => import(/* webpackChunkName: "add" */ '../../views/Add.vue'),
meta: {
breadCrumb: [
{ name: 'Add New' }
]
},
Notice the meta object attached to the route.. this will be used to describe the current view.
Breadcrumb.vue component
<template>
<div class="breadcrumb">
<ul class="d-flex m-0 p-0"
<li
v-for="(breadcrumb, idx) in breadcrumbList"
:key="idx">
{{ breadcrumb.name }}
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Breadcrumb',
data () {
return {
breadcrumbList: []
}
},
mounted () { this.updateList() },
watch: { '$route' () { this.updateList() } },
methods: {
routeTo (pRouteTo) {
if (this.breadcrumbList[pRouteTo].link) this.$router.push(this.breadcrumbList[pRouteTo].link)
},
updateList () { this.breadcrumbList = this.$route.meta.breadCrumb },
formatPath(path) {
const newPath = path.replace(/\//g, " > ")
return newPath
}
}
}
</script>
And then you can import the breadcrumb into your navbar or where ever you would like to place it
<Breadcrumb class="breadcrumb" />
import Breadcrumb from '#/components/Breadcrumb.vue'
components: {Breadcrumb}
So basically the breadcrumb will always watch your current route and change the data based on the meta data you provide in your router.js file
You can access to router name like this:
<div v-if="this.$route.name == 'home'">
<HeaderTransparent />
</div>
<div v-else>
<HeaderWhite />
</div>

How can I get value in select vue.js? vue.js 2

My case is like this
I have a component like this :
<template>
<div class="panel panel-default panel-filter">
...
<div id="collapse-location" class="collapse in">
<!-- province -->
<div style="margin-bottom: 10px">
<location-bs-select element-name="province_id" level="provinceList" type="1" module="searchByLocation"/>
</div>
<!-- city -->
<location-bs-select element-name="city_id" level="cityList" type="2" module="searchByLocation"/>
</div>
<!-- button search -->
<div class="panel-item">
<br>
<a href="javascript:;" class="btn btn-block btn-success" v-on:click="searchData">
Search
</a>
</div>
...
</div>
</template>
<script>
export default{
...
data() {
return{
...
province_id:'',
}
},
...
methods: {
...
searchData: function() {
console.log(this.province_id)
console.log(document.getElementsByName("province_id")[0].value)
console.log('testtt')
}
}
}
</script>
The component have child component, that is location-bs-select. The component used to display provincy and city
The component child like this :
<template>
<select class="form-control" v-model="selected" :name="elementName" #change="changeLocation">
<template v-for="option in options">
<template>
<option v-bind:value="option.id" >{{ option.name }}</option>
</template>
</template>
</select>
</template>
<script>
export default{
props: ['elementName', 'level','type','module'],
...
};
</script>
If I do inspect element, the result like this :
When click button search, I want to get the value of province and city
I try javascript like this :
console.log(document.getElementsByName("province_id")[0].value)
it works
But I want to use vue step. I try like this :
console.log(this.province_id)
It does not work
How can I solve it?
I hope I got you right. You want to propagate the value of the select back to the parent. The Child component COULD be like this.
removed template nesting
added change event listener and emit method
added data
And all together:
<template>
<select #change="emitChange" class="form-control" v-model="selected" :name="elementName">
<option v-for="option in options" v-bind:value="option.id" >{{ option.name }}</option>
</select>
</template>
<script>
export default{
props: ['elementName', 'level','type','module'],
data: function() {
return { selected: null }
},
methods: {
emitChange: function() {
this.$emit('changeval', this.selected);
}
}
};
</script>
Now your parent needs to listen to this emit. Here just the relevant parts of your parent
...
<location-bs-select element-name="city_id"
level="cityList"
type="2"
#changeval="changeval"
module="searchByLocation"/>
...
methods: {
changeval: function(sValue) {
this.province_id = sValue;
console.log(this.province_id);
}
}
Quickly summed up
the select value is bound to the selected prop of your data
the select has an attached change event which will emit changes
the parent will listen to this emit and will update it's relevant data prop