How to compute styles on <body> or <html> using vue.js? - vue.js

I am using vuejs style bindings to render changes dynamically as the styles are computed.
This works great for everything within the scope of my Vue instance but how can I compute styles for body or html tags?
This used to be possible when you could bind the vue instance to but vue no longer lets you do it.
I want to dynamically update the background color of using my computed variables in vue.
edit: added code snippet to demonstrate
var app = new Vue({
el: '#app',
data: {
color: '#666666'
},
computed: {
backgroundColor: function() {
return {
'background-color': this.color
}
}
},
methods: {
toggleBackground: function() {
if(this.color=='#666666'){
this.color = '#BDBDBD'
} else {
this.color = '#666666'
}
}
}
})
<script src="https://vuejs.org/js/vue.min.js"></script>
<html>
<body>
<div id="app" :style="backgroundColor">
<div>
lots of content...
</div>
<button #click="toggleBackground"> Click to toggle </button>
</div>
</body>
</html>

If you really need to style body itself, you'll need to do it with plain JavaScript in a watcher. A simple example is below.
You should (not something I've tried, but I'm hypothesizing) be able to defeat overscrolling effects by making body and your outer container non-scrolling. Put a scrollable container inside that. When it overscrolls, it will show your outer container, right?
The reasons for not binding to body are here (for React, but applies to Vue).
What’s the problem with ? Everybody updates it! Some people have
non-[Vue] code that attaches modals to it. Google Font Loader will
happily put elements into body for a fraction of second, and
your app will break horribly and inexplicably if it tries to update
something on the top level during that time. Do you really know what
all your third party scripts are doing? What about ads or that social
network SDK?
new Vue({
el: '#app',
data: {
isRed: false
},
watch: {
isRed() {
document.querySelector('body').style.backgroundColor = this.isRed ? 'red' : null;
}
}
});
#app {
background-color: white;
margin: 3rem;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<input type="checkbox" v-model="isRed">
</div>

I think I found better solution than using jQuery/querySelector
You can add tag style right in your Vue template.
And add v-if on this, smth like that:
<style v-if="true">
body {
background: green;
}
</style>
Thus you can use computed/methods in this v-if and DOM always will update when you need.
Hope this will help someone ;)
UPD:
Using tag "style" in templates is not best idea, but you can create v-style component, then everything will be fine:
Use style tags inside vuejs template and update from data model
My snippet:
Vue.component('v-style', {
render: function (createElement) {
return createElement('style', this.$slots.default)
}
});
new Vue({
el: '#app',
data: {
isRed: false,
color: 'yellow',
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="checkbox" v-model="isRed">
<v-style v-if="isRed">
body {
background: red; /*one more benefit - you can write "background: {{color}};" (computed)*/
}
</v-style>
</div>

Related

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.

How to load an External SVG into Vue as an Object

I need to load an SVG file into a Vue template. It must be loaded in such a way that I can access the internal classes with js and css, so presumably I'm looking for an <object> tag and not an <img> tag.
The SVG is located on an external server, not a part of my project. Vue-Svg-Loader works just fine if I have the svg as part of my project, but doesn't seem to work when the SVG isn't available until runtime.
I've tried the following
<template>
<div ref="floorplan" id="floorplan-frame">
<object
type="image/svg+xml"
id="floorplan"
:data="svgPath"
></object>
</div>
</template>
<script>
export default {
data() {
return {
svgPath: 'http://test-mc4/floorplan.svg',
};
},
};
</script>
Unfortunately it doesn't work. If I replace the <object> tag with <img :src="svgPath" /> it does show the SVG, but as a static image where the internal css classes are not available. It does show me that my path is correct and the file is actually available, but it doesn't explain why the object tag is just empty when I use it.
I've searched extensively and I can figure out how to load it as an Object if it's internal, or how to load it External as long as it's an image. I just can't seem to figure out how to do both.
In order to access the elements within an <object>, you'd need to wait until it was loaded (load event) and then access the child SVG document by its contentDocument property.
Unfortunately, this won't work in your case because the SVG files are coming from a different origin. The same-origin policy will block access to the contentDocument. Here is an example, which also fails (logs null) because a data: URL is a different origin:
const svgPath = 'data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDIwIDIwIj4NCiAgICA8Y2lyY2xlIGN4PSIxMCIgY3k9IjEwIiByPSIxMCIgZmlsbD0icmVkIi8+DQo8L3N2Zz4=';
const app = new Vue({
el: '#app',
data: {
svgPath,
},
methods: {
svgLoaded() {
setTimeout(() => {
console.log(this.$refs.object.contentDocument);
}, 1000);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<object
ref="object"
:data="svgPath"
width="100"
height="100"
v-on:load="svgLoaded"
></object>
</div>
The only way to load an SVG from a different origin and then access its internal structure with your JS and CSS would be to fetch it and then load it into your component as v-html. Note that this opens significant XSS vulnerabilities; you should be wary of this option and only use it with external servers you trust. In any case, here's a working example:
const svgPath = 'data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDIwIDIwIj4NCiAgICA8Y2lyY2xlIGN4PSIxMCIgY3k9IjEwIiByPSIxMCIgZmlsbD0icmVkIi8+DQo8L3N2Zz4=';
const app = new Vue({
el: '#app',
data: {
svgData: '',
},
async mounted() {
const svgResponse = await fetch(svgPath);
this.svgData = await svgResponse.text();
await Vue.nextTick();
// SVG is present in the DOM at this point
const svg = this.$refs.drawing.firstElementChild;
console.log(svg.outerHTML);
// DOM manipulations can be performed
const circle = svg.querySelector('circle');
circle.setAttribute('fill', 'blue');
circle.setAttribute('r', '6');
}
});
.drawing {
width: 100px;
height: 100px;
}
/* SVG can be styled as part of the main document */
.drawing circle {
stroke: cyan;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div
class="drawing"
ref="drawing"
v-html="svgData"
></div>
</div>

VueJs: bind `v-on` on a custom component to replace an existing one

In order to ease the styling of my page, I'd like to create a bunch of mini components like, and exploit how attributes are merged in VueJs. So for example, here is a minimal js file also hosted on this JSFiddle:
Vue.component('my-button', {
template: '<button style="font-size:20pt;"><slot></slot></button>'
})
var app = new Vue({
el: "#app",
data: {
message: "world",
},
methods: {
sayHello: function () {
alert("Hello");
}
}
})
and then in my html I just want to use <my-button> instead of button:
<div id="app">
Hello {{message}} <my-button #click="sayHello" style="color:red;">Style works, but not click</my-button> <button v-on:click="sayHello" style="color:red;">Both works</button>
</div>
Unfortunately, it seems that attributes are merged, but not listeners, so it means that I can't do v-on:click on my new button... Any way to make it possible?
Thanks!
-- EDIT --
I saw the proposition of Boussadjra Brahim of using .native, and it works, but then I found this link that explains why it's not a great practice and how to use v-on="$listeners" to map all listeners to a specific sub-button. However, I tried, to just change my template with:
template: `<button style="font-size:20pt;" v-on="$listeners"><slot></slot></button>`,
but I get an error:
Vue warn: Property or method "$listeners" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option."
Here is the JSFiddle.
Your fiddle didn't work because you were using an old version of Vue, $listeners was added in Vue 2.4.0.
Here's a demo:
Vue.component('my-button', {
template: '<button style="color: red" v-on="$listeners"><slot/></button>'
})
new Vue({
el: '#app',
methods: {
sayHello() {
alert('Hello')
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-button #click="sayHello">Custom Button</my-button>
<button #click="sayHello">Ordinary Button</button>
</div>

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.

Pass object data to styles in Vue.js

I want to be able to pass data from a object to the <styles> in a single file component. However, it doesn't seem like this is possible.
What I'm trying to achieve:
<template></template>
<script>
export default {
data: function() {
return { color: "#f00" }
}
}
</script>
<style>
body {
background-color: this.color
}
</style>
As far as I'm aware, you are not able to pass data from the component to its stylesheets.
The best practice as far as dynamic styling is to use v-bind:class or v-bind:style if needed. The <style> section should be used for the CSS templating language only.
<template>
<p :style="{ backgroundColor: bgColor }">Lorem ipsum</p>
</template>
<script>
export default {
data() {
return {
bgColor: '#000'
}
}
}
</script>
Let me know if you have any other questions!
Update
Since the goal is to use it for Sass functions like darken, I would recommend managing the various colors needed through utility classes instead.