Vue: How to supply the layout component with dynamic props depending on router-view component's data - vue.js

I'd like to have a layout component with a header component and a <router-view>, but the header component should receive different dynamic props depending on the data of the router-view component.
I'm agnostic to the way in which the layout architecture is being achieved: Either with nested routes or using a slot and passing the layout name via the route's meta field.
So, the header component should be passed in a dynamic prop from the view component - let's say Home.vue: If we go to route /home the router-view will be Home.vue and in Home.vue we have a reactive data field (i.e. scrolledDown) and the prop passed to the header component (i.e. theme) should depend on the value of this data field (scrolledDown).
The easiest way I can think of is using local state management (vuex or whatever), so that the view components (i.e. Home.vue) can store the reactive data and the layout component can grab it and pass it as prop to the header component. But maybe there are much better ways to accomplish this?
MainHeader.vue:
<template>
<div :class="`header--${theme}`">...</div>
</template>
<script>
export default {
props: ["theme"]
}
</script>
Layout.vue:
<template>
<div>
<main-header />
<router-view />
</div>
</template>
<script>
import MainHeader from "../MainHeader";
export default {
components: { MainHeader }
}
</script>
or alternatively:
App.vue:
<template>
<component :is="layout">
<router-view />
</component>
</template>
// script with `layout` data prop and layout component imports
Layout.vue:
<template>
<div>
<main-header />
<slot />
</div>
</template>
<script>
import MainHeader from "../MainHeader";
export default {
components: { MainHeader }
}
</script>
Home.vue:
<template>
<div>...</div>
</template>
<script>
export default {
computed: {
scrolledDown() { ... }
}
}
</script>

Related

Vue: Reuse loading spinner template and logic

I've multiple (20+) pages where I need the following code:
<template>
<template v-if="isLoading">
<Spinner full size="medium" />
</template>
<template v-else>
<p>Page data</p>
</template>
</template>
<script>
export default {
computed: {
isLoading() {
return this.$store.getters['loader/isLoading'];
},
},
};
</script>
I don't want to rewrite this part everytime I need it so is there a way to create something like a higher order component with access to the computed method and a way to add the hoc to the script tag of the different vue files? Or another way to archive this?
I could recommend extending the spinner component where you want to show the spinner. I've created a boilerplate setup that show a very simple implementation of this approach here.
The main idea is to expose a default slot for you spinner component, and wrap the page component in that slot.
<template>
<div>
<Spinner v-if="isLoading" full size="medium" />
<!-- Slot for component data -->
<slot v-else></slot>
</div>
</template>
<script>
export default {
computed: {
isLoading() {
return this.$store.getters['loader/isLoading'];
},
},
};
</script>
Then in any component that you want to show the spinner:
<template>
<spinner>
<!-- Pass here the component content to be shown after the spinner is hidden -->
</spinner>
</template>
<script>
import Spinner from "./spinner";
export default {
name: "Page1",
extends: Spinner,
components: { Spinner }
};
</script>

Why Vue 2 remove every thing when using :is prop override component?

Please see this minimum example, I have a simple component called HelloWorld.vue
HelloWorld.vue
<template>
<div class="foo">bar</div>
</template>
When I using this component like this
App.vue
<template>
<HelloWorld />
</template>
<script>
import HelloWorld from "./HelloWorld.vue";
export default {
components: {
HelloWorld,
},
};
</script>
This rendered HTML looks like this
Rendered HTML
<div class="foo">bar</div>
However, when I add :is prop, rendered HTML changed
App.vue
<template>
<HelloWorld is="h2" />
</template>
<script>
import HelloWorld from "./HelloWorld.vue";
export default {
components: {
HelloWorld,
},
};
</script>
Rendered HTML
<h2></h2>
Why is this happening?
Is it possible to overwrite only the outer HTML tag just like the class and style prop?
is should be used together with the component element:
<component is="h2"></component>
<HelloWorld is="h2" /> efficiently renders h2 instead of HelloWorld.
In order for root element to be configurable, the component should provide this:
<template>
<component :is="tag" class="foo">bar</div>
</template>
<script>
export default {
props: {
tag: {
type: String,
default: 'div'
}
}
}
</script>

How to import a component as prop from another component?

It is possible to import a component as prop from another component?
For example:
Q-Dialog
<template>
<q-dialog>
<q-layout>
<q-page-container>
<myCustomComponent />
</q-page-container>
</q-layout>
</q-dialog>
</template>
<script>
//Edited:This works, but I want to register dynamically from props
//import myCustomComponent from "components/MyCustomComponent.vue";
import myCustomComponent from this.myComponent;
export default {
props: ["myComponent"],
components: { myCustomComponent }
}
Another component:
this.$q.dialog({
component: CustomComponent, //dialog
myComponent: 'components/MyCustomComponent.vue'
})
Edited, for better clarify what I am trying to achieve in this case:
My dialog is an abstract component in which an unlimited number of different myCustomComponent can be rendered.
To achieve this, I need that the registration of each component (import) is not done in the q-dialog.
A solution to consider is to register each component in the file from which the q-dialog is loaded for rendering (different from the q-dialog, in my case the another component file) and then pass that path from the imported file to the q-dialog, possibly as props.
Is this possible?
Edited with solution:
Parent component
<script>
import registeredComponent from "components/MyCustomComponent.vue";
export default {
data() {
return {
myComponent: registeredComponent
}
}
methods: {
btnClickShowDialog(){
this.$q.dialog({
component: dialogComponent,
//pass registered component as prop to dialog
myCustomComponent: this.myComponent
})
}
}
</script>
Q-dialog
<template>
<q-dialog>
<q-layout>
<q-page-container>
<component :is="myCustomComponent" />
</q-page-container>
</q-layout>
</q-dialog>
</template>
<script>
export default {
props: ["myCustomComponent"]
}
</script>
In your q-dialog component you can use the component tag to dynamically render a passed in component prop. See this stackblitz.
// q-dialog html
<component :is="myComponent" />
In your parent component you'll want to import the desired component, assign it to a data property and pass it in
// parent component js
import SomeComponent from './SomeComponent.vue'
data () {
return {
passedInComponent: SomeComponent
}
}
// parent component html
<q-dialog :my-component="passedInComponent" />

render a Vue slot in a layout from component

I'm having problems with a named slot. This seems like it should work. In the code below I'm trying to use a named slot "sidebar". I would expect my sidebar slot content to show up between the Sidebar starts and Sidebar ends text but nothing shows up in that slot. Everything renders in the main slot.
Here's my code.
route...
{
path: "/test",
name: "test",
meta: {
layout: "test-layout"
},
component: () =>
import("#/pages/Test.vue")
},
and App.vue template...
<template>
<div id="app">
<component :is="layout">
<router-view />
</component>
</div>
</template>
and test-layout...
<template>
<div>
<div>
<h1>Sidebar starts</h1>
<slot name="sidebar"/>
<h1>Sidebar ends</h1>
</div>
<div class="container">
<h1>Content starts</h1>
<slot/>
<h1>Content ends</h1>
</div>
</div>
</template>
and page Test.vue...
<template>
<test-layout>
<span slot="sidebar">sidebar slot content {{forecast.todaySummary}}</span>
<div>main slot content {{forecast.currentSummary}}</div>
</test-layout>
</template>
<script>
import api from "#/js/web-services";
export default {
data() {
return {
forecast: null
};
},
created() {
api.getDailyForecast().then(response => {
this.forecast = response.data;
});
}
};
</script>
and the import in my main.js
import TestLayout from "./layouts/test-layout.vue";
Vue.component('test-layout', TestLayout);
Why isn't my sidebar slot working?
UPDATE
If I get rid of the two lines in main.js and add
import TestLayout from "#/layouts/test-layout.vue";
and
export default {
components: { TestLayout },
data() {...
to Test.vue then it works.
In your router file you are using layout: "test-layout" this means what ever comes in your vue component will be rendered in base test-layout.
There are two ways as far as I know to render the layouts.
Do not define layout in router file and on parent component define named slots like this<slot #header></slot><slot #body></slot> then every slot will be rendered within this (test-layout) layout, and then in your each component extend like this <test-layout><template header>Header content</template><template body>Body content</template></test-layout>.
Defining layout in router file like you did, you can not further use in slots in that layout, you can just import other components e.g <template><Component1><Component2> </template>

How to pass all props dynamically to child component

How to pass all props dynamically to child component? As an example consider a case:
// wrapper around a component my-wrapper.vue
<template>
<div>
<third-party-component />
</div>
</template>
third-party-component is a component which can accept number of attributes like value, disabled, clicked, etc. How I can use my-wrapper in way that whatever I passed as props to it it will be transferred to third-party-component like
<my-wrapper :value="myVal" :disabled="field.isDisabled" />
By default the attributes you add on to my-wrapper will be bound to the root element which is div. To avoid this set inheritAttrs option to false
Then you can bind all the attributes to using v-bind="$attrs" where $attrs contains parent-scope attribute bindings (except for class and style)
// wrapper around a component my-wrapper.vue
<template>
<div>
<third-party-component v-bind="$attrs"/>
</div>
</template>
<script>
export default{
inheritAttrs: false
}
</script>
I would set identical props to the wappers and use them in the template ...
But there is many ways, it easy to speak from parent to childs.
https://en.vuejs.org/v2/guide/components-props.html#Passing-Static-or-Dynamic-Props
https://v1.vuejs.org/guide/syntax.html#Shorthands
---- MyWrapper.vue
<template>
<div>
<third-party-component :value="value" :disabled="disabled" />
</div>
</template>
<script>
#import third-party-component from '../..? ./components/thirdPartyComponent.vue'
export default {
name: 'MyWrapper',
props: ['value', 'disabled'],
components: {
third-party-component // local component, you can use global
}
//... data, computed ...
}
</script>
----- MyAppli.vue
<template>
<my-wrapper :value="myVal" :disabled="field.isDisabled" />
</template>
<script>
import my-wrapper from '../..? ../components/MyWrapper.vue'
export default {
name: 'MyApp',
components: {
my-wrapper // local component, you can use global
} ....