Vue how to customize global navbar at view level - vue.js

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>

Related

How to pass props to sibling and child component in vue

The structure of my code is like this:
So in the Product component, I am making an API call:
<template>
<button class="btn button col-2" #click="addToCart()">
Add to cart
</button>
</template>
<script>
methods:{
addToCart: function () {
let amount = this.itemsCount !== "" ? this.itemsCount : 1;
if(this.variationId != null) {
this.warningMessage = false;
cartHelper.addToCart(this.product.id, this.variationId, amount, (response) => {
this.cartItems = response.data.attributes.items;
});
} else {
this.warningMessage = true;
}
console.log(this.cartItems)
},
}
</script>
And what I am trying to do is the response (this.cartItems) should be shown in Cart component. And my Navbar component:
<template>
<nav class="navbar navbar-expand-lg shadow">
<div class="container navbar-container">
<div class="navbar navbar-profile">
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" id="dropdownCart" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fa fa-fw fa-cart-arrow-down"></i>
<span></span>
</button>
<div #click="$event.stopPropagation()">
<CartBox :cartItems="cartItems"/>
</div>
</div>
</div>
</div>
</nav>
</template>
<script>
export default {
props: {
cartItems:Object
},
components: {CartBox},
}
And CartBox:
<template>
<Cart/>
</template>
<script>
import Cart from '../components/Cart'
export default {
components: {
Cart
}
}
</script>
And my Cart component:
<template>
<div
class="dropdown-menu cart"
aria-labelledby="triggerId"
>
<div class="inner-cart">
<div>
<div class="cart-items">
<div>
<a class="remove">Remove</a>
</div>
</div>
</div>
<hr/>
<div class="cart-items-total">
<span>Total:</span>
Clear Cart
</div>
<hr/>
<router-link :to="{name: 'order'}" class="btn button-secondary">Go To Cart</router-link>
</div>
</div>
</template>
<script>
export default {
computed: {
},
methods: {
}
};
</script>
I am really confused how to pass the props to sibling component and then the child component but if you could pass it to Cart component, that would really help me.
There are two approaches for your request:
1. Using props, provide and inject
This could be accomplished with Provide / inject, after passing your response to a parent. Basically, you will emit your response from your Product component to a parent, maybe like your App.vue as the prop myData, then you provide it for every child, no matter where it is nested, like this:
provide: {
providedData: this.myData
}
In any child you can now use:
inject: ['providedData']
Please note, that this data will only be available if your Product component received it. The second approach is recommended.
2. Using a store
Using a store like vuex is a bit more complex than approach 1, but it will save a lot of time in the future. You would recieve your response in your Product component, dispatch it to the store and could call the state of information from this store anywhere in your app. See further information in this documentation: Vuex | Getting Started

Cant Display img when using props to store src on vue js

so on this project i was trying to make an image component to display an image from a string props.
here is my component code
this is the component
<template>
<div class="Img-grid">
<div class="container">
<div class="col">
<img :v-bind:src="recipeImage" alt="image-photo">
<p>{{recipeName}}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ImgGrd'
props: {
recipeImage: String,
recipeName: String
}
}
</script>
this is my where the component display
<template>
<div class="RecipeByYou">
<div class="container">
<ImgGrid recipeName="a" v-bind:recipeImage="imgUrl" />
</div>
</div>
</template>
<script>
import ImgGrid from '../components/Image_Grid.vue'
export default {
name: 'RecipeImage',
components: {
Header,
ImgGrid
},
data () {
return {
imgUrl: 'https://media.sproutsocial.com/uploads/2017/02/10x-featured-social-media-image-size.png'
}
}
}
am i doing anything wrong? because when i inspect the web element it shows this thing, so i was confuse where did i do wrong, is this the correct method?
<img data-v-366ed4fa="" v-bind:src="https://media.sproutsocial.com/uploads/2017/02/10x-featured-social-media-image-size.png" alt="image-photo">
change this code <img :v-bind:src="recipeImage" alt="image-photo"> to <img v-bind:src="recipeImage" alt="image-photo">.
or you can change <img :v-bind:src="recipeImage" alt="image-photo"> to <img :src="recipeImage" alt="image-photo">.
: is shorthand of v-bind, your code :v-bind:src="recipeImage" means v-bind:v-bind:src="recipeImage"

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' ]

Only show slot if it has content

Is there a way to only display a slot if it has any content?
For example, I'm building a simple Card.vue component, and I only want the footer displayed if the footer slot has content:
Template
<template>
<div class="panel" :class="panelType">
<div class="panel-heading">
<h3 class="panel-title">
<slot name="title">
Default Title
</slot>
</h3>
</div>
<div class="panel-body">
<slot name="body"></slot>
<p class="category">
<slot name="category"></slot>
</p>
</div>
<div class="panel-footer" v-if="hasFooterSlot">
<slot name="footer"></slot>
</div>
</div>
</template>
Script
<script>
export default {
props: {
active: true,
type: {
type: String,
default: 'default',
},
},
computed: {
panelType() {
return `panel-${this.type}`;
},
hasFooterSlot() {
return this.$slots['footer']
}
}
}
</script>
In in View:
<card type="success"></card>
Since the above component doesn't contain a footer, it should not be rendered, but it is.
I've tried using this.$slots['footer'], but this returns undefined.
Does anyone have any tips?
It should be available at
this.$slots.footer
So, this should work.
hasFooterSlot() {
return !!this.$slots.footer;
}
Example.
You should check vm.$slots and also vm.$scopedSlots for it.
hasSlot (name = 'default') {
return !!this.$slots[ name ] || !!this.$scopedSlots[ name ];
}
CSS simplifies this a lot. Just use the following code and voila!
.panel-footer:empty {
display: none;
}
This is the solution for Vue 3 composition API:
<template>
<div class="md:grid md:grid-cols-5 md:gap-6">
<!-- Here, you hide the wrapper if there is no used slot or empty -->
<div class="md:col-span-2" v-if="hasTitle">
<slot name="title"></slot>
</div>
<div class="mt-5 md:mt-0"
:class="{'md:col-span-3': hasTitle, 'md:col-span-5': !hasTitle}">
<div class="bg-white rounded-md shadow">
<div class="py-7">
<slot></slot>
</div>
</div>
</div>
</div>
</template>
<script>
import {ref} from "vue";
export default {
setup(props, {slots}) {
const hasTitle = ref(false)
// Check if the slot exists by name and has content.
// It returns an empty array if it's empty.
if (slots.title && slots.title().length) {
hasTitle.value = true
}
return {
hasTitle
}
}
}
</script>
Now, in Vue3 composition API , you can use useSlots.
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
</script>
<template>
<div v-if="slots.content" class="classname">
<slot name="content"></slot>
</div>
</template>
In short do this in inline:
<template lang="pug">
div
h2(v-if="$slots.title")
slot(name="title")
h3(v-if="$slots['sub-title']")
slot(name="sub-title")
</template>
I have ran into a similiar issue but across a wide code base and when creating atomic design structured components it can be tiring writing hasSlot() methods all the time and when it comes to TDD - its one more method to test... Saying that, you can always put the raw logic in a v-if but i have found that the template end up cluttered and harder to read on occasions especially for a new dev checking out the code structure.
I was tasked to find out a way of removing parent divs of slots when the slot isnt provided.
Issue:
<template>
<div>
<div class="hello">
<slot name="foo" />
</div>
<div class="world">
<slot name="bar" />
</div>
</div>
</template>
//instantiation
<my-component>
<span slot="foo">show me</span>
</my-component>
//renders
<div>
<div class="hello">
<span slot="foo">show me</span>
</div>
<div class="world"></div>
</div>
as you can see, the issue is that i have an almost 'trailing' div, that could provide styling issues when the component author decides there is no need for a bar slot.
ofcourse we could go <div v-if="$slots.bar">...</div> or <div v-if="hasBar()">...</div> etc but like i said - that can get tiresome and eventually end up harder to read.
Solution
My solution was to make a generic slot component that just rendered out a slot with a surrounding div...see below.
//slot component
<template>
<div v-if="!!$slots.default">
<slot />
</div>
</template>
//usage within <my-component/>
<template>
<div>
<slot-component class="hello">
<slot name="foo"/>
</slot-component>
<slot-component class="world">
<slot name="bar"/>
</slot-component>
</div>
</template>
//instantiation
<my-component>
<span slot="foo">show me</span>
</my-component>
//renders
<div>
<div class="hello">
<span>show me</span>
</div>
</div>
I came into use-case issues when trying this idea and sometimes it was my markup structure that needed to change for the benefit of this approach.
This approach reduces the need for small slot checks within each component template. i suppose you could see the component as a <conditional-div /> component...
It is also worth noting that applying attributes to the slot-component instantiation (<slot-component class="myClass" data-random="randomshjhsa" />) is fine as the attributes trickle into the containing div of the slot-component template.
Hope this helps.
UPDATE
I wrote a plugin for this so the need for importing the custom-slot component in each consumer component is not needed anymore and you will only have to write Vue.use(SlotPlugin) in your main.js instantiation. (see below)
const SLOT_COMPONENT = {
name: 'custom-slot',
template: `
<div v-if="$slots.default">
<slot />
</div>
`
}
const SLOT_PLUGIN = {
install (Vue) {
Vue.component(SLOT_COMPONENT.name, SLOT_COMPONENT)
}
}
export default SLOT_PLUGIN
//main.js
import SlotPlugin from 'path/to/plugin'
Vue.use(SlotPlugin)
//...rest of code
Initially I thought https://stackoverflow.com/a/50096300/752916 was working, but I had to expand on it a bit since $scopeSlots returns a function which is always truthy regardless of its return value. This is my solution, though I've come to the conclusion that the real answer to this question is "doing this is an antipattern and you should avoid it if possible". E.g. just make a separate footer component that could be slotted in.
Hacky solution
hasFooterSlot() {
const ss = this.$scopedSlots;
const footerNodes = ss && ss.footer && ss.footer();
return footerNodes && footerNodes.length;
}
Best Practice (helper component for footer)
const panelComponent = {
template: `
<div class="nice-panel">
<div class="nice-panel-content">
<!-- Slot for main content -->
<slot />
</div>
<!-- Slot for optional footer -->
<slot name="footer"></slot>
</div>
`
}
const footerComponent = {
template: `
<div class="nice-panel-footer">
<slot />
</div>
`
}
var app = new Vue({
el: '#app',
components: {
panelComponent,
footerComponent
},
data() {
return {
name: 'Vue'
}
}
})
.nice-panel {
max-width: 200px;
border: 1px solid lightgray;
}
.nice-panel-content {
padding: 30px;
}
.nice-panel-footer {
background-color: lightgray;
padding: 5px 30px;
text-align: center;
}
<script src="https://unpkg.com/vue#2.6.11/dist/vue.min.js"></script>
<div id="app">
<h1>Panel with footer</h1>
<panel-component>
lorem ipsum
<template #footer>
<footer-component> Some Footer Content</footer-component>
</template>
</panel-component>
<h1>Panel without footer</h1>
<panel-component>
lorem ipsum
</panel-component>
</div>
Hope I understand this right. Why not using a <template> tag, which is not rendered, if the slot is empty.
<slot name="foo"></slot>
Use it like this:
<template slot="foo">
...
</template>
For Vue 3:
Create an utility function
//utils.js
function isSlotHasContent(slotName, slots) {
return Boolean(!!slots[slotName] && slots[slotName]()[0].children.length > 0);
}
In your component:
<script setup>
import { isSlotHasContent } from 'path/to/utils.js';
const slots = useSlots();
// "computed" props has a better performance
const isFooSlotHasContent = computed(() => isSlotHasContent('foo', slots));
</script>
<template>
<div>
<div v-if="isFooSlotHasContent">
<slot name="foo" />
</div>
<div v-if="!isFooSlotHasContent">
Some placeholder
</div>
</div>
</template>
TESTED
So this work for me in vue 3:
I use onMounted to first get the value, and then onUpdate so the value can update.
<template>
<div v-if="content" class="w-1/2">
<slot name="content"></slot>
</div>
</template>
<script>
import { ref, onMounted, defineComponent, onUpdated } from "vue";
export default defineComponent({
setup(props, { slots }) {
const content = ref()
onMounted(() => {
if (slots.content && slots.content().length) {
content.value = true
}
})
onUpdated(() => {
content.value = slots.content().length
console.log('CHECK VALUE', content.value)
})
})
</script>
#Bert answer does not seem to work for dynamic templates like <template v-slot:foo="{data}"> ... </template>.
i ended up using:
return (
Boolean(this.$slots.foo) ||
Boolean(typeof this.$scopedSlots.foo == 'function')
);
I like the Solution of #AlexMA however in my case I needed to pass props to the function in order to get the nodes to show up.
Here is an example of how I am passing the "row" to the scoped slot, in my case the row contains a type param that I want to test against in the calling component.
<other-component>
<template v-slot:expand="{ row }" v-if="!survey.editable">
<div v-if="row.type != 1" class="flex">
{{ row }}
</div>
</template>
</other-component>
In "other-component" I have the template defined as
<template>
<div>
<div v-for="(row, index) in rows">
{{ hasSlotContent(row) }}
<slot name="expand" :row="row"> </slot>
</div>
</div>
</template>
Because the v-slot requires "row" to be passed to it I created a a method
methods:{
hasSlotContent(row){
const ss = this.$scopedSlots
const nodes = ss && ss.expand && ss.expand({ row: row })
return !!(nodes && nodes.length)
}
}
I call this on each iteration so that it can evaluate itself and give back the appropriate response.
you can use the "hasSlotContent(row)" method where-ever you need it, in my example I'm just outputting the truthy value to the DOM.
I hope this helps someone come to a quicker solution.
Reposting a Vue 3 solution from Github, which also works with Options API, since there was a fairly upvoted method from an Issue there:
The comment itself: https://github.com/vuejs/core/issues/4733#issuecomment-1024816095
The function (remove types if you're not writing TypeScript):
import {
Comment,
Text,
Slot,
VNode,
} from 'vue';
export function hasSlotContent(slot: Slot|undefined, slotProps = {}): boolean {
if (!slot) return false;
return slot(slotProps).some((vnode: VNode) => {
if (vnode.type === Comment) return false;
if (Array.isArray(vnode.children) && !vnode.children.length) return false;
return (
vnode.type !== Text
|| (typeof vnode.children === 'string' && vnode.children.trim() !== '')
);
});
}
This works just as fine, if you delete the slotProps argument (unless you need it).

Vue JS Component reattach or Cache

VueJS component doesn't get cached or atleast reattached after navigation. On refresh or launch everything gets attached and rendered well but after navigating to another page then back. The First Component - Carousel - component in my case doesn't get rendered but the API call is made.
<template>
<div class="rel">
<div id="homeCarousel" class="owl-carousel owl-slider">
<div class="item" v-for="product in featured">
<div class="bg-holder top-area-half" >
<div class="bg-mask-lighten"></div>
<img class="bg-img" v-bind:src="product.feature_image_url">
<div class="hero-caption">
<div class="container">
<h3 class="hero-title">{{product.feature_title}}</h3>
<p class="hero-subtitle">{{product.feature_subtitle}}</p>
<a class="btn btn-white btn-ghost btn-lg hero-btn" href="#">Shop now</a>
</div>
</div>
</div>
</div>
</div>
<div id="hero-slider-nav" class="hero-slider-nav">
<div class="container">
<div class="pull-right"></div>
</div>
</div>
</div>
</template>
<style>
</style>
<script>
export default{
data(){
return{
featured:[]
}
},
ready(){
},
mounted(){
this.getFeaturedProducts();
},
components:{
},
methods: {
getFeaturedProducts: function () {
Vue.http.get('/api/product/filter/featured=1').then(
(response) => {
this.featured = response.body;
}
)
}
}
}
</script>
`
<template>
<div class="global-wrapper clearfix ">
<keep-alive>
<Carousel></Carousel>
</keep-alive>
//The rest of the code which is just importing the Component
I found out what i was doing wrong. I had a separate JS/JQuery file and on the document ready i was initializing an owl carousel by id #('homeCarousel').owlCarousel({}) . What worked was, since i had already bootstrapped owl carousel -> on the mounted lifecycle callback i was now targeting the element and making it an owl carousel.