Nuxt vue component doesn't re-render after ssr - vue.js

I have a component in nuxt, ssr enabled
<template>
<div class="img">
<img class="max-h-full" :src="imageSrc" />
</div>
</template>
<script setup lang="ts">
import { useDarkModeMonitor } from "../composables/darkMode";
const isDarkMode = useDarkModeMonitor();
const imageSrc = computed(() => {
if (isDarkMode.value)
return "URL_TO_SOME_IMAGE";
return "URL_TO_ANOTHER_IMAGE";
});
</script>
The useDarkModeMonitor checks if the user's preferred theme is dark mode. Since it uses windows to check, it is not available while being rendered on the server side. In that case isDarkMode gives hardcoded value true.
The page shows up in the correct theme but the images are hardcoded to show the dark version. On the Vue dev tools it shows isDarkMode and imageSrc computed properly, but the image src is still the initial value from ssr.
How can I force the image part to rerender with the new imageSrc?
PS: If you have a better way of handling this kind of situation, please do share :)

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.

Hydration problem with client-side authentication and computed property in Vue/Nuxt layout

My app (vue/nuxt 3) stores the user authentication state in localStorage. As a consequence it is only available on the client and prerendered pages always show the unauthenticated content. Client will render the authenticated content as soon as it is aware of it. That's ok and accepted.
However, this does not seem to apply for computed properties. My whole layout depends on the authentication state, e.g. like this:
<template>
<div :class="computedClasses">
<slot />
</div>
</template>
<script setup>
const computedClasses = computed(() => ({
if ($someReferenceToStore.user.logged.in) {
return 'loggedin'
} else {
return 'anonymous'
}
}))
</script>
The problem is, that even though the user is logged in, the computedClasses is not updated to loggedin but the server generated anonymous is shown. How to solve this? How can I make the client update the computed property and overwrite the server rendered classes?
I know, I can wrap parts of my template that depend on the authentication state with <ClientOnly> to avoid hydration mismatches. Wrapping my layout with <ClientOnly> would basically disable any server rendering. Can I set a property of an element (the :class="...") to client-only?
My current solution is to use a ref that's being updated.
<template>
<div :class="computedClasses">
<slot />
</div>
</template>
<script setup>
const computedClasses = ref('');
onMounted(() => {
computedClasses.value = $someReferenceToStore.user.logged.in ? 'loggedin' : 'anonymous';
});
</script>
Depending on the use case, one might want to add a watcher to update the ref as well.

HowTo: Toggle dark mode with TailwindCSS + Vue3 + Vite

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.

Vue3 Composition API Reusable reactive values unique to calling component

Running Vue 3.2.6 and Vite 2.5.1
I've been experimenting a bit with the new Composition API and trying to figure out some common usecases where it makes sense to use it in favor of the OptionsAPI. One good usecase I immediately identified would be in Modals, the little popups that occur with a warning message or dialogue or whatever else.
In my old Apps, I'd have to create the modal opening logic in every single component where the modal is being called, which lead to a lot of repetition. With the CompAPI, I tried extracting the logic into a simple modal.ts file that exports 2 things, a reactive openModal boolean, and a toggleModal function. It works great! Until I have more than one modal in my app, that is, in which case it'll open every single Modal at once, on top of one another.
As an example setup
modal.ts
import { ref } from "vue";
const openModal = ref(false);
const toggleModal = () => {
openModal.value = !openModal.value;
};
export { openModal, toggleModal };
App.vue
<template>
<Example1 />
<Example2 />
<Example3 />
</template>
Modal.vue
<template>
<div class="modal" #click.self.stop="sendClose">
<slot />
</div>
</template>
<script setup>
const emit = defineEmits(["closeModal"]);
const sendClose = () => {
emit("closeModal");
};
</script>
Example#.vue
Note that each of these are separate components that have the same layout, the only difference being the number
<template>
<h1>Example 1 <span #click="toggleModal">Toggle</span></h1>
<teleport to="body">
<Modal v-if="openModal" #closeModal="toggleModal">
<h1>Modal 1</h1>
</Modal>
</teleport>
</template>
<script setup>
import { openModal, toggleModal } from "#/shared/modal";
import Modal from "#/components/Modal.vue";
</script>
What happens when clicking the toggle span is obvious (in hindsight). It toggles the value of openModal, which will open all 3 modals at once, one on top of the other. The issue is even worse if you try to implement nested Modals, aka logic in one modal that will open up another modal on top of that one.
Am I misunderstanding how to use ref here? Is it even possible for each component to have and keep track of its own version of openModal? Cause the way I've set it up here, it's acting more like a global store, which isn't great for this particular usecase.
The way I imagined this working is that each component would import the reactive openModal value, and keep track of it independently. That way, when one component calls toggleModal, it would only toggle the value inside of the component calling the function.
Is there a way of doing what I originally intended via the Composition API? I feel like the answer is simple but I can't really figure it out.
That is because you are not exporting your composition correctly, resulting in a shared state, since you are exporting the same function and ref to all components. To fix your issue, you should wrap whatever you're exporting in modal.ts in a function, say:
// Wrap in an exported function (you can also do a default export if you want)
export function modalComposition() {
const openModal = ref(false);
const toggleModal = () => {
openModal.value = !openModal.value;
};
return { openModal, toggleModal };
}
And in each component that you plan to use the composition, simply import it, e.g.:
import { modalComposition } from "#/shared/modal";
import Modal from "#/components/Modal.vue";
// By invoking `modalComposition()`, you are no longer passing by reference
// And therefore there is no "shared state"
const { openModal, toggleModal } = modalComposition();
Why does this work?
When you export a function and then invoke it in the setup of every single component, you are ensuring that each component is setup by executing the function, which returns a new ref for every single instance.

How can I add vuetify dialog into existing application?

I created a vue dialog app/component using vue cli. It consist of a sample button to be clicked on to imitate how the dialog (What I need) will be loaded when a link on the existing application is clicked. I have a couple of issues.
When using v-app it adds the application wrapper I dont need seeing as its only the dialog I want. It creates a huge whitespace not needed. If I remove it, it errors [Vuetify] Unable to locate target [data-app] and the dialog wont load when <div #click='getInformation('USA')'></div> in the existing application is used.
Tried removing v-app and just using template but continues to error. Seems I need to still specify v-app in some way. Lost here
An example on how Im trying to pull it off but not working in App.vue
<template>
<div v-resize="onResize">
<v-dialog>
<v-card>
{{ information }}
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
data() {
return {
isMobile: false,
information: []
};
},
methods: {
onResize() {
if (window.innerWidth < 425) this.isMobile = true;
else this.isMobile = false;
},
getInformatiom(country) {
axios
.get(`${api}/${country}/info`, {
headers: {
Authorization: `token`
}
})
.then(response => {
this.information = response.data.info;
});
}
}
};
main.js
import Vue from "vue";
import App from "./App.vue";
import Vuetify from "vuetify";
import "vuetify/dist/vuetify.min.css";
Vue.use(Vuetify);
Vue.config.productionTip = false;
new Vue({
render: h => h(App)
}).$mount("#app");
Dialog component is ready to go, just having so much trouble getting it to show when its being called from the existing application. Just a note, the existing application does not use Vue, its only classic asp, Im only updating the dialog on the page to look/work better using vue/vuetify. Any help would be GREATLY APPRECIATED
You NEED the v-app element with vuetify.
Try this to only use the app when showing the dialog. Then use CSS to customise the v-app.
<v-app v-if='this.information && this.information.length'>
<v-dialog>...</v-dialog>
</v-app>
I would use the max-width prop of v-dialog, make it dynamic by adding :max-width and then have that bound to a computed property which subscribes to your screen size. I would not try to control it from an external div. See here for full list of sizing options
https://vuetifyjs.com/en/components/dialogs