Vue 3: how to change body background color only in specific page - vue.js

with vue 3 i need to change the body background color only when i visit a signin page.
My index.html is:
<body class="body">
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
How can i change the body background color from a component or from vue router?

In the App component you can put a conditional class on the top level element if the route matches your desired route. As long as your top level element is full height and width of the body.
<template>
<div class="background" :class="{ otherColor: isSignIn }">
<div id="app"></div>
</div>
</template>
<script>
export default {
computed: {
isSignIn() {
return this.$route.path === '/signIn';
}
}
}
</script>
<style>
.background {
height: 100vh;
width: 100vw;
background-color: #ffffff;
}
.otherColor {
background-color: #d4d4d4; // whatever background color you want.
}
</style>

Related

Rendering nested components

I am starting to learn VueJS by building a simple website.
Right now I have made three components:
Header
Navigation
Topbar
I want to render navigation and topbar inside header, so I can call the header component inside every page (I haven't found a way to make a "layout" or something, that every page uses).
Header.vue
<template>
<header id="topNav">
<topbar />
<navigation />
</header>
</template>
<script lang="ts">
import Vue from 'vue'
import navigation from '../navigation/navigation.vue'
import Topbar from '../topbar/topbar.vue'
export default Vue.extend({
components: { Topbar, navigation },
})
</script>
<style scoped>
#topNav {
background-color: #fff;
-webkit-box-shadow: 0 0 35px 0 rgb(154 161 171 / 15%);
box-shadow: 0 0 35px 0 rgb(154 161 171 / 15%);
position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 1001;
padding: 0 12px;
}
</style>
Topbar
<template>
<div id="topBar">
<h2>this is the topbar</h2>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({})
</script>
<style scoped>
#topBar {
border-bottom: 1px solid #dee2e6;
height: 70px;
padding: 0 10px;
z-index: 100;
position: fixed;
left: 0;
right: 0;
}
</style>
And when I call it on for example my home page (index.vue) nothing renders.
<template>
<main>
<header />
<div id="page-content">
<h1>Home</h1>
</div>
</main>
</template>
<script lang="ts">
import Vue from 'vue'
import Header from '../components/header/header.vue'
export default Vue.extend({
name: 'Home',
components: { Header },
})
</script>
I've tried reading the documentation and search around, but haven't been able to figure out what I'm doing wrong.
Since the question is tagged with a Nuxt tag, I'll recommend looking into Nuxt layouts: https://nuxtjs.org/docs/concepts/views#layouts
There is a default named default that you could use by creating the file /layouts/default.vue and passing the components inside of it.
You can of course change that with a layout: 'yolo' if you want another 'yolo` layout.
Pro tip: you don't need to import the components yourself in Nuxt.
You have named your component header, which is a standard html element. Therefore the browser will probably just try to render a standard <header> element instead of your component.
Therefore it is advised to always use multi word component names. See docs here. You can use eslint in your code editor to help you spot these mistakes.
PS: if you are learning vue from the start, I would advise you to use the composition api with the script setup approach, as it makes things easier and provide the opportunity to write clearer code as components grow.

List Transitions work only for "enter" not for "leave"

Following the example in the docs, I'm using transition-group for a list of items. Strangely it works when items appear (enter), not when they disappear (leave), meaning they slide down in an animated fashion when appearing, but disappear instantly without animation: the leave animation failed. Why?
<template>
<div v-if="notifications.length">
<transition-group name="notifications">
<span
v-for="notification in notifications"
:key="notification.id"
>
<!-- content -->
</span>
</transition-group>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState({
notifications: state => state.notifications.notifications
})
}
}
</script>
<style lang="scss" scoped>
.notifications-enter-active,
.notifications-leave-active {
transition: all 0.5s;
}
.notifications-enter {
transform: translateY(-100%);
}
.notifications-leave-to {
opacity: 0;
}
</style>
Store
export const mutations = {
DELETE_NOTIFICATION (state, id) {
state.notifications.splice(
state.notifications.findIndex(item => item.id === id),
1
)
}
}
I couldn't reproduce the exact symptom with that code (demo 1), which only transitions on leave instead of enter in your scenario. The reason for that is because the span is display: inline, which prevents the transition.
The Vue docs provide a tip for this:
One important note is that these FLIP transitions do not work with elements set to display: inline. As an alternative, you can use display: inline-block or place elements in a flex context.
So, you can apply display: flex on the transition-group:
<template>
<transition-group class="container">
...
</transition-group>
</template>
<style>
.container {
display: flex;
}
</style>
demo 2
Or display: inline-block on the span to be transitioned:
<template>
<span class="notification-item">
...
</span>
</template>
<style>
.notification-item {
display: inline-block;
}
</style>
demo 3
Turns out by replacing <div v-if="notifications.length"> with <div v-if="notifications"> transitions now work. Even though this doesn't make any sense to me.
If anyone can explain in a comment that'd be nice :)

How to create a Pre-Loading/ Splash Screen in Nuxt.js before the app starts?

I have tried to add a loader as shown in the nuxt.js documentation in between the routes but its not work. But I'm not able to add a splash screen before the app starts.
Code snippet in my components/loading.vue
<template>
<div v-if="loading" class="loading-page">
<p>Loading...</p>
</div>
</template>
<script>
export default {
data: () => ({
loading: false
}),
methods: {
start(){
this.loading = true
},
finish(){
this.loading = false
}
}
}
</script>
In nuxt.js.config
export default {
...
loading: '~/components/loading.vue'
...
}
As far as I know, you can't use a Vue component as a loading indicator for your your Nuxt app.
You will have to create an HTML document instead. This HTML document does not have to have an <html>, <head> or <body>. It just has to be the splash screen you want to show.
Here's how I did it:
Create an html document ~/assets/loading.html
Add the following to nuxt.config.js file.
loadingIndicator: {
name: '~/assets/loading.html'
}
Save and reload your page, you should now have a custom loading indicator / splash screen.
Example HTML file:
Here's a very simple file to show a splash screen image, when loading a nuxt app.
<div style="height: 100vh; width: 100vw; display: flex; align-items: center; justify-content: center; flex-direction: column; background-color: #004066; margin-left: -8px; margin-top: -8px; overflow: hidden;">
<img width="90%" src="<%= options.img %>">
</div>
NOTE:
Pay attention to <%= options.img %>. I'm making use of options, which can be defined in the nuxt.config.js simply by adding more keys to loadingIndicator, an example can be seen below.
loadingIndicator: {
name: '~/assets/loading.html',
img: '/loading.gif'
}
NOTE 2:
When accessing assets such as images in the loading indicator, you will have to put them in the /static folder.
Documentation: https://nuxtjs.org/docs/2.x/features/loading#custom-indicators
Official examples: https://github.com/nuxt/nuxt.js/tree/dev/packages/vue-app/template/views/loading

How to design a reusable dialog box within children components in Vue?

I've been struggling with implementing a dialog box / modal design and behavior from inside of children components in Vue.
So here's the set up, I have a Vue component called "WorkersComponent". This component is just a list of workers assigned to some case fetched from the backend (Laravel). This component is reusable an can be in any place/case/ticket/lookup where a user would want to add workers to.
The component has an "add" button in it. Once clicked, I want a new component to appear at that location (at the click location), which could be a dropdown, modal, dialogue - doesn't really mater. This subcomponent has a search bar and some controls to fetch workers info and add them to the parent component.
My problem is that I can't figure out how to get the nesting / positioning to work. Because it is a child component, its position is always against the parent component, so I can only control it's position within that parent component, but I want it to be displaying on top of other DOM elements and components if necessary - whatever makes sense. Worst case scenario - I want it to be in the middle of the page at least.
Now how do I implement this? I probably want it to be a unique subcomponent, not a global generic modal. On top of it, if it were a global generic, then I have an idea of how to populate the modal with relevant options but how to pass them back to the component that called the modal - no idea. So I'm struggling with the approach. It seems like such a simple thing and yet, I can't find a viable solution.
<workers-component name="Assigned Workers">
<button <!-- Vue controls in here to invoke a modal/dialogue/dropdown --> >Add Worker</button>
<!-- The subcomponent itself -->
<workers-select-component />
</workers-component>
Here's an example from Gmail: wherever this search bar is (let's say it's a parent component), if I click on a triangle, it will expand this other pane, which will (1) appear wherever the search bar is and (2) cover other elements to display it and (3) not dismiss the pane until manually dismissed (which is easy but normal Bootstrap dropdowns don't support this).
Here's a solution:
Vue.component('ToggleDialog', {
props: ['state'],
template: `
<button
#click="$emit('toggle', state)"
class="dialog-button"
>
TOGGLE MODAL
</button>
`
})
Vue.component('DialogModal', {
props: ['state'],
template: `
<div
class="dialog-backdrop"
>
<div
class="dialog-button"
>
<toggle-dialog
:state="state"
#toggle="toggleModal"
/>
</div>
</div>
`,
methods: {
toggleModal(state) {
this.$emit('toggle', state)
}
}
})
new Vue({
el: "#app",
data() {
return {
isModalOpen: false
}
},
methods: {
toggleModal(state) {
this.isModalOpen = !state
}
}
})
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-button {
padding: 10px 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<toggle-dialog :state="isModalOpen" #toggle="toggleModal">
OPEN MODAL
</toggle-dialog>
<dialog-modal v-if="isModalOpen" :state="isModalOpen" #toggle="toggleModal" />
</div>
As you can see the modal is not the child of the button, but the child of the main app. toggle events are emitted (and the modal re-emits it) to the app that controls the state of the modal dialog.
For more complex apps it might not be the best. You could use an event bus (deprecated in Vue3) or Vuex (state management) to overcome this multiple emit-re-emit stuff.
EDIT: NEW SOLUTION
Vue.component('ToggleDialog', {
data() {
return {
isModalOpen: false
}
},
template: `
<div
class="toggle-modal-wrapper"
>
<button
#click="isModalOpen = !isModalOpen"
class="dialog-button"
>
TOGGLE MODAL
</button>
<dialog-modal
v-if="isModalOpen"
#toggle="isModalOpen = !isModalOpen"
>
<slot></slot>
</dialog-modal>
</div>
`
})
Vue.component('DialogModal', {
props: {
innerComponent: {
type: String
}
},
template: `
<div
class="dialog-backdrop"
>
<div>
<slot></slot>
<br />
<button
#click="$emit('toggle')"
class="dialog-button"
>
TOGGLE MODAL
</button>
</div>
</div>
`
})
new Vue({
el: "#app",
})
.toggle-modal-wrapper {
z-index: 10000;
}
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-button {
padding: 10px 15px;
}
.other-part {
z-index: 1000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<toggle-dialog>
<template>
This is the first.
</template>
</toggle-dialog>
<toggle-dialog>
<template>
This is the other.
</template>
</toggle-dialog>
<div class="other-part">
OTHER PART OF THE UI
</div>
</div>
You could try playing with slots if you want a reusable component - or even better: the render function.

VueJs: How to change CSS background image for each page route?

I am probably making this more complicated than it needs to be, but does anyone have any ideas on how I can swap out custom banner images for each of my pages (when route changes to any thing other than index page)? I have the slots working , but need way to change background image in the non-index banners. Here's my code:
App.vue and shows only the main page banner:
<template>
<div id="app">
<BannerHome v-if="isHomeView" />
<main id="routerView">
<RouterView :key="$route.fullPath" />
</main>
<Footer />
</div>
</template>
<script>
import BannerHome from '#/components/BannerHome.vue'
import Footer from '#/components/Footer.vue'
export default {
name: 'App',
components: {
BannerHome,
Footer
},
computed: {
isHomeView() {
return this.$route.fullPath === '/'
}
}
}
</script>
Example of non-index page (About.vue):
<template>
<article>
<banner>
<template v-slot:titleTop>Mission Focused</template>
<template v-slot:titleBottom>
Dedicated To Service
</template>
</banner>
<section class="container-md pb-5">
<h1>About Page</h1>
</section>
</article>
</template>
<script>
import Banner from '#/components/Banner.vue'
export default {
name: 'About',
components: {
Banner
}
}
</script>
Banner component for non-index pages (Banner.vue). Note the background image. How can I swap this out for each non-index page? Right now the background image is shown for ALL non-index pages.
<template>
<header>
<MainNav />
<div class="container">
<h1><em><slot name="titleTop"></slot><br />
<span class="pl-3"><slot name="titleBottom"></slot></span></em>
</h1>
</div>
</header>
</template>
<script>
import MainNav from '#/components/MainNav.vue'
export default {
name: 'Banner',
components: {
MainNav
}
}
</script>
<style lang="scss" scoped>
header {
position: relative;
width: 100%;
// set this page height here
height: 20em;
text-transform: uppercase;
}
header::before {
content: "";
display: block;
position: absolute;
left: 0;
bottom: 6em;
width: 100%;
// set this page height here
height: calc(20em + 10em);
z-index: -1;
transform: skewY(-3.5deg);
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)),
url('../assets/mockphoto.jpg') < -----how to swap out this image?
no-repeat left top,
linear-gradient(#4e4376, #2b5876);
background-size: cover;
border-bottom: 0.2em solid #fff;
}
h1 {
font-size: 1.75rem!important;
font-weight: 700!important;
letter-spacing: 0.01em;
padding: 2.5rem 0 0 0;
// text-shadow: 0.022em 0.022em 0.022em #111;
color: #fff;
}
</style>
Btw, I am also using Gridsome...so if there is also an easy way to access page data via GraphQL for this purpose I can also use that. Thanks for any help !
So, I actually just need to add the /deep/ keyword to the parent CSS like so:
/deep/ header::before {
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)),
url('../assets/mockphoto.jpg')
no-repeat left top,
linear-gradient(#4e4376, #2b5876);
}
Docs: https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors