HowTo: Toggle dark mode with TailwindCSS + Vue3 + Vite - vue.js

I'm a beginner regarding Vite/Vue3 and currently I am facing an issue where I need the combined knowledge of the community.
I've created a Vite/Vue3 app and installed TailwindCSS to it:
npm create vite#latest my-vite-vue-app -- --template vue
cd my-vite-vue-app
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Then I followed the instructions on Tailwind's homepage:
Add the paths to all of your template files in your tailwind.config.js file.
Import the newly-created ./src/index.css file in your ./src/main.js file. Create a ./src/index.css file and add the #tailwind directives for each of Tailwind’s layers.
Now I have a working Vite/Vue3/TailwindCSS app and want to add the feature to toggle dark mode to it.
The Tailwind documentation says this can be archived by adding darkMode: 'class' to tailwind.config.js and then toggle the class dark for the <html> tag.
I made this work by using this code:
Inside index.html
<html lang="en" id="html-root">
(...)
<body class="antialiased text-slate-500 dark:text-slate-400 bg-white dark:bg-slate-900">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Inside About.vue
<template>
<div>
<h1>This is an about page</h1>
<button #click="toggleDarkMode">Toggle</botton>
</div>
</template>
<script>
export default {
methods: {
toggleDarkMode() {
const element = document.getElementById('html-root')
if (element.classList.contains('dark')) {
element.classList.remove('dark')
} else {
element.classList.add('dark')
}
},
},
};
</script>
Yes, I know that this isn't Vue3-style code. And, yes, I know that one could do element.classList.toggle() instead of .remove() and .add(). But maybe some other beginners like me will look at this in the future and will be grateful for some low-sophisticated code to start with. So please have mercy...
Now I'll finally come to the question I want to ask the community:
I know that manipulating the DOM like this is not the Vue-way of doing things. And, of course, I want to archive my goal the correct way. But how do I do this?
Believe me I googled quite a few hours but I didn't find a solution that's working without installing this and this and this additional npm module.
But I want to have a minimalist approach. As few dependancies as possbile in order not to overwhelm me and others that want to start learning.
Having that as a background - do you guys and gals have a solution for me and other newbies? :-)

The target element of your event is outside of your application. This means there is no other way to interact with it other than by querying it via the DOM available methods.
In other words, you're doing it right.
If the element was within the application, than you'd simply link class to your property and let Vue handle the specifics of DOM manipulation:
:class="{ dark: darkMode }"
But it's not.
As a side note, it is really important your toggle method doesn't rely on whether the <body> element has the class or not, in order to decide if it should be applied/removed. You should keep the value saved in your app's state and that should be your only source of truth.
That's the Vue principle you don't want break: let data drive the DOM state, not the other way around.
It's ok to get the value (on mount) from current state of <body>, but from that point on, changes to your app's state will determine whether or not the class is present on the element.
vue2 example:
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
darkMode: document.body.classList.contains('dark')
}),
methods: {
applyDarkMode() {
document.body.classList[
this.darkMode ? 'add' : 'remove'
]('dark')
}
},
watch: {
darkMode: 'applyDarkMode'
}
})
body.dark {
background-color: #191919;
color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<div id="app">
<label>
<input type="checkbox" v-model="darkMode">
dark mode
</label>
</div>
vue3 example:
const {
createApp,
ref,
watchEffect
} = Vue;
createApp({
setup() {
const darkMode = ref(document.body.classList.contains('dark'));
const applyDarkMode = () => document.body.classList[
darkMode.value ? 'add' : 'remove'
]('dark');
watchEffect(applyDarkMode);
return { darkMode };
}
}).mount('#app')
body.dark {
background-color: #191919;
color: white;
}
<script src="https://unpkg.com/vue#next/dist/vue.global.prod.js"></script>
<div id="app">
<label>
<input type="checkbox" v-model="darkMode">
dark mode
</label>
</div>
Obviously, you might want to keep the state of darkMode in some external store, not locally, in data (and provide it in your component via computed), if you use it in more than one component.

What you're looking for is Binding Classes, but where you're getting stuck is trying to manipulate the <body> which is outside of the <div> your main Vue instance is mounted in.
Now your problem is your button is probably in a different file to your root <div id="app"> which starts in your App.vue from boilerplate code. Your two solutions are looking into state management (better for scalability), or doing some simple variable passing between parents and children. I'll show the latter:
Start with your switch component:
// DarkButton.vue
<template>
<div>
<h1>This is an about page</h1>
<button #click="toggleDarkMode">Toggle</button>
</div>
</template>
<script>
export default {
methods: {
toggleDarkMode() {
this.$emit('dark-switch');
},
},
};
</script>
This uses component events ($emit)
Then your parent/root App.vue will listen to that toggle event and update its class in a Vue way:
<template>
<div id="app" :class="{ dark: darkmode }">
<p>Darkmode: {{ darkmode }}</p>
<DarkButton #dark-switch="onDarkSwitch" />
</div>
</template>
<script>
import DarkButton from './components/DarkButton.vue';
export default {
name: 'App',
components: {
DarkButton,
},
data: () => ({
darkmode: false,
}),
methods: {
onDarkSwitch() {
this.darkmode = !this.darkmode;
},
},
};
</script>
While tailwind say for Vanilla JS to add it into your <body>, you generally shouldn't manipulate that from that point on. Instead, don't manipulate your <body>, only go as high as your <div id="app"> with things you want to be within reach of Vue.

Related

watching vueuse's useElementVisibility changes is not working

I have a sample project at https://github.com/eric-g-97477-vue/vue-project
This is a default vue project with vueuse installed.
I modified the script and template part of HelloWorld.vue to be:
<script setup>
import { watch, ref } from "vue";
import { useElementVisibility } from "#vueuse/core";
defineProps({
msg: {
type: String,
required: true,
},
});
const me = ref(null);
const isVisible = useElementVisibility(me);
watch(me, (newValue, oldValue) => {
console.log("visibilityUpdated", newValue, oldValue);
});
</script>
<template>
<div ref="me" class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
You’ve successfully created a project with
Vite +
Vue 3.
</h3>
</div>
</template>
and adjusted App.vue so the HelloWorld component could be easily scrolled on or off the screen.
This appears to match the Usage demo code. The primary difference being that I am using <script setup> and the demo code is not. But, perhaps, I need to do things differently as a result...?
The watcher will fire when the app loads and indicates that the HelloWorld component is visible, but it is not. Additionally, regardless if I scroll so the component is visible or not, the watcher does not fire again.
What am I doing wrong?
UPDATE: modified the question based on the discovery that I needed to add ref="me" to the div in the template. Without this, the watcher was never firing.

Simple Nuxt 3 Page Transition not working

I'm discovering Nuxt 3 and and simply want to make an animation between pages. The idea is to use javascript hooks to make page transitions using js library such as gsap or animeJs.
So in my app.vue file, I simply put <NuxtPage/> into <Transition> element like this :
<NuxtLayout>
<Transition>
<NuxtPage/>
</Transition>
</NuxtLayout>
My vue pages ('./pages/index.vue' and './pages/project/myproject.vue') look like this :
<template>
<div>
<h1>My Project</h1>
</div>
</template>
<script setup>
function onEnter(el, done) {
done()
}
function onLeave(el, done) {
done()
}
</script>
I have followed both Nuxt 3 and Vue 3 documentations :
https://v3.nuxtjs.org/guide/directory-structure/pages#layouttransition-and-pagetransition
https://vuejs.org/guide/built-ins/transition.html#javascript-hooks
I also read this thread on github, but I can't find answer :
https://github.com/nuxt/framework/discussions/851
When i was using Nuxt 2 I only need to put transition object into my page like this and it's working fine :
<script>
export default {
// ... (datas, methods)
transition: {
mode: "in-out",
css: false,
enter(el, done) {
console.log("enter");
done()
},
leave(el, done) {
console.log("leave");
done()
}
}
}
</script>
<template>
<div>
<h1 class="text-center text-5xl">Hello World</h1>
</div>
</template>
Do you have any idea how to achieve it ?
Nuxt 3 doesn't need a <Transition> wrapper around pages/layouts, by default it does that for you.
Take a look at this starter template: in assets/sass/app.scss, the last part of the style is page and layout transition.
You can tweak the default named animations (page- and layout-).
More infos here
Just follow the official documentation for Nuxt 3. You need to add the following code to your nuxt.config.ts file:
export default defineNuxtConfig({
app: {
pageTransition: { name: 'page', mode: 'out-in' }
},
})
And then apply the classes inside your app.vue file, like this:
<template>
<NuxtPage />
</template>
<style>
.page-enter-active,
.page-leave-active {
transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(1rem);
}
</style>
Nuxt 3 uses the Vue's <Transition> component under the hood, so you don't need to add it in the template.
Be careful with the css prefix.

Creating a vue.js component [duplicate]

This question already has answers here:
Creating a simple VUE.JS application
(2 answers)
Closed 2 years ago.
I have created a vue.js application by
vue init webpack myproject
I am then following a link, to create simple copper component, I have put that under src/components directory as follow:
<template>
<div id="app">
<polygon-crop :imageSource = "'src/assets/logo.png'"
ref="canvas"> </polygon-crop>
<button #click.prevent="crop"> Crop </button>
<button #click.prevent="undo"> Undo </button>
<button #click.prevent="redo"> Redo </button>
<button #click.prevent="reset"> Reset </button>
</div>
</template>
<script>
import Vue from 'vue';
import VuePolygonCropper from 'vue-polygon-cropper';
Vue.component(VuePolygonCropper);
export
default
{
name: 'App',
methods: {
crop: function() {
this.$refs.canvas.crop();
},
undo: function()
{
this.$refs.canvas.undo();
},
redo: function()
{
this.$refs.canvas.redo();
},
reset: function()
{
this.$refs.canvas.reset();
}
}
};
</script>
But actually, it doesn't render properly and my component doesn't show up properly. I am new to vue.js and any help would be appreciated!
You won't be able to get this component running with just this code snippet, there's a couple of things that you would need to do to fix this up.
Before we go any deeper, I would like you to make sure if you have installed this vue-polygon-cropper component. If you navigated to the package.json that is located in the same level as your "src" folder, you would see a mention of vue-polygon-cropper there, if not please install it by npm install vue-polygon-croper .
Let's take a look at your <template> section first:
1- In the template, you call a component <polygon-crop> but, there is no component registered by that name in your script (What you are attempting to register is 'VuePolygonCropper' so you should try using <VuePolygonCropper> component instead.
2-I see there you copied and pasted the logo image in assets, that's a great way to test it! However, Digging through the creator's example that they put up on github, It seems like this component requires a full path to your image file instead of the relative path. so instead of /src/assets/logo.png try doing :imageSource="require('../assets/logo.png')"
I'm assuming the assets logo is on a folder that is one level above your current component.
So your template should look like this:
<template>
<div id="app">
<VuePolygonCropper :imageSource = "require('../assets/logo.png')"
ref="canvas"> </VuePolygonCropper>
<button #click.prevent="crop"> Crop </button>
<button #click.prevent="undo"> Undo </button>
<button #click.prevent="redo"> Redo </button>
<button #click.prevent="reset"> Reset </button>
</div>
</template>
Now on to your script!
just import the VuePolygonCropper and mention it as a component in the components section.
You don't need to import vue and do Vue.component(VuePolygonCropper). The correct way to register this component would be like this
<script>
import VuePolygonCropper from 'vue-polygon-cropper';
export
default
{
name: 'App',
components:{VuePolygonCropper},
methods: {
crop: function() {
this.$refs.canvas.crop();
},
undo: function()
{
this.$refs.canvas.undo();
},
redo: function()
{
this.$refs.canvas.redo();
},
reset: function()
{
this.$refs.canvas.reset();
}
}
};
</script>
For the heck of it, I have created a codesandbox that you can play around with . You can try to play around with the App.vue file and see how it was created.
Happy coding!

Why v-if is not showing the heading when boolean value changes?

I am very new to vue js. I am just learning to use it from laracasts. What I want to do is communicate between root class and subclass. Here, user will put a coupon code and when he changes focus it will show a text.
My html code is like this
<body>
<div id="root">
<coupon #applied="couponApplied">
<h1 v-if="isCouponApplied">You have applied the coupon.</h1>
</div>
<script src="https://unpkg.com/vue#2.5.21/dist/vue.js"></script>
<script src="main.js"></script>
</body>
My main.js is like this,
Vue.component('coupon', {
template: '<input #blur="applied">',
methods: {
applied()
{
this.$emit('applied');
}
}
});
new Vue({
el: '#root',
data: {
isCouponApplied:false,
},
methods:{
couponApplied()
{
this.isCouponApplied = true;
}
}
});
I am checking using vue devtools extension in chrome. There is no error. The blur event is triggered. isCouponApplied also changes to true. But the h1 is not showing. Can anyone show me where I made the mistake?
The problem is that you are not closing your <coupon> tag
<div id="root">
<coupon #applied="couponApplied"></coupon>
<h1 v-if="isCouponApplied">You have applied the coupon.</h1>
</div>
Should fix your issue. If you don't close your tag, the parser will auto-close it, but it will do so at the close of its wrapping container (the root div), so the h1 content will be seen as inside the <coupon> element, and will be replaced by your component's template.

Vue js loading js file in mounted() hook

I have the following Vue component:
<template>
<div id="wrapper">
<div class="main-container">
<Header />
<router-view/>
<Footer/>
</div>
</div>
</template>
<script>
import './assets/js/popper.min.js';
// other imports
// ....
export default {
name: 'App',
components : {
Header,
Footer
},
mounted(){
// this is syntax error
import './assets/js/otherjsfile.js'
}
}
</script>
As is clear from the code snippet, I want to have the otherjsfile.js loaded in mounted() hook. That script file has certain IIFEs which expects the html of the web page to be fully loaded.
So how do I invoke that js file in a lifecycle hook?
This is the pattern I use. The example is importing a js file which contains an IIFY, which instantiates an object on window.
The only problem with this would occur if you want to use SSR, in which case you need Vue's <ClientOnly> component, see Browser API Access Restrictions
mounted() {
import('../public/myLibrary.js').then(m => {
// use my library here or call a method that uses it
});
},
Note it also works with npm installed libraries, with the same path conventions i.e non-relative path indicates the library is under node_modules.
I'm a little unsure of what your asking. But if you are just trying to include an external js file in your page, you can just use the script tag in your template and not have to put anything in your mounted function, like this:
<template>
<div id="wrapper">
<div class="main-container">
<Header />
<router-view/>
<Footer/>
</div>
<script src="./assets/js/otherjsfile.js"></script>
</div>
</template>
<script>
import './assets/js/popper.min.js';
// other imports
// ....
export default {
name: 'App',
components : {
Header,
Footer
},
}
</script>
Does this solve your issue?