How to get _value properties in Vue 3 ref - javascript

Hi I'm trying to get values from the _value property in Vue to no avail. How am I able to grab these values like accessKey, align, ariaAtomic etc. I'm trying to get the clientWidth.
<div id="hello" ref="theRange" class="border-red-200 border-2 w-[80px]">
<h1>Test</h1>
</div>
const theRange = ref(null);
console.log(theRange.value.clientWidth); // Throws error

This is how you can get it
<script setup>
import { ref, onMounted } from 'vue'
const cube = ref(null)
onMounted(() => {
console.log(cube.value.clientWidth)
})
</script>
<template>
<div ref="cube">nice</div>
</template>
<style scoped>
div {
width: 200px;
height: 300px;
background-color: orange;
}
</style>
Here is an example.
Regarding Vue's lifecycle hooks, setup() doesn't have the element attached to the DOM yet, hence why you may not have anything regarding window-related properties of your object.

Related

How to refer to Vue when using provide

I've been trying to pass down a value via a parent component to a child component without using props. I'm using provide for this on a parent component (not the highest parent). The value im passing down will be dynamically updated, so after reading the vue docs I have to do something like: Vue.computed(() => this.todos.length) but it throws an error because Vue is undefined. I seem to be unable to import Vue like this import Vue from 'vue' (or something similar). How can I make this work? To be honest, even when I try to pass down a static variable I get undefined in the (direct) child component, even when I use the exact same code as in the vue docs.
So I have 2 questions:
how to refer/import the Vue instance?
Is it possible to use provide on a direct parent component (which is not the root)?
I'm using Vue3
You can get the instance using getCurrentInstance but it's not needed for what you want to do:
import { getCurrentInstance } from 'vue';
setup() {
const internalInstance = getCurrentInstance();
}
You can provide a computed from any component. Import provide and computed in the parent and use them like:
Parent
import { provide, computed } from 'vue';
setup() {
...
const total = computed(() => x.value + y.value); // Some computed
provide('total', total);
}
Import inject in the child:
Child
import { inject } from 'vue';
setup() {
...
const total = inject('total');
}
Here's a demo (with just slightly different import syntax because the CDN doesn't use actual imports):
const { createApp, ref, computed, provide, inject } = Vue;
const app = createApp({});
// PARENT
app.component('parent', {
template: `
<div class="parent">
<child></child>
<button #click="x++">Increment</button> (from Parent)
</div>
`,
setup() {
const x = ref(5);
const y = ref(10);
const total = computed(() => x.value + y.value);
provide('total', total);
return {
x
}
}
});
// CHILD
app.component('child', {
template: `
<div class="child">
Total: {{ total }}
</div>
`,
setup() {
const total = inject('total');
return {
total
}
}
});
app.mount("#app");
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
.parent, .child { padding: 24px; }
.parent {
background: #dddddd;
}
.child {
margin: 6px 0;
background: #ddeeff;
}
<div id="app">
<parent></parent>
</div>
<script src="https://unpkg.com/vue#next"></script>

TypeScript VueJS: Using watcher with computed property

I'm building a custom component that will allow me to use arbitrary <div>s as radio buttons. Currently my code looks like so:
<template>
<div
class="radio-div"
:class="selected ? 'selected' : ''"
#click="handleClick"
>
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
.radio-div {
display: inline-block;
border-radius: 5px;
border: 1px solid #000;
padding: 5px;
margin: 5px;
}
.radio-div.selected {
box-shadow: 0 0 10px rgba(255, 255, 0, 0.5);
border: 2px solid #000;
}
</style>
<script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-property-decorator";
#Component
export default class RadioDiv extends Vue {
#Prop()
val!: string;
#Prop({
required: true
})
value!: string;
selected = false;
mounted() {
this.selected = this.value === this.val;
}
#Watch("value")
onChange() {
this.selected = this.value === this.val;
}
handleClick(e: MouseEvent) {
this.$emit("input", this.val);
}
}
</script>
To utilize this I can put it in a template like so:
<template>
<div class="q-pa-md">
<q-card>
<q-card-section class="bg-secondary">
<div class="text-h6">Political Affiliation</div>
</q-card-section>
<q-separator />
<q-card-section>
<radio-div v-model="politicalParty" val="Republican">
<div class="text-h6">Republican</div>
<p>Wrong answer</p>
</radio-div>
<radio-div v-model="politicalParty" val="Democrat">
<div class="text-h6">Democrat</div>
<p>Wrong answer</p>
</radio-div>
<radio-div v-model="politicalParty" val="Independent">
<div class="text-h6">Independent</div>
<p>These people clearly know what's up</p>
</radio-div>
</q-card-section>
</q-card>
</div>
</template>
<script lang="ts">
import { Vue, Component, Watch } from "vue-property-decorator";
import RadioDiv from "../components/RadioDiv.vue";
#Component({
components: { RadioDiv }
})
export default class Profile extends Vue {
politicalParty = "Independent";
}
</script>
This works as expected. I can click on the <div>s and it switches which one is selected and updates the variable appropriately.
But now I want to tie this into a global state manager. So instead of a local politicalParty variable, I have a computed property like so:
<script lang="ts">
import { Vue, Component, Watch } from "vue-property-decorator";
import RadioDiv from "../components/RadioDiv.vue";
import globalState from "../globalState";
#Component({
components: { RadioDiv }
})
export default class Profile extends Vue {
get politicalParty() {
return globalState.politicalParty;
}
set politicalParty(val) {
globalState.politicalParty = val;
}
}
</script>
Putting a console.log statement in the setter I can see that it is getting called, and the variable is being updated. But putting a console.log statement in my value watcher (in the RadioDiv component) shows it's no longer being called now that I'm using computed properties.
What's the secret to get my RadioDiv reactive again, now that I'm using global state?
Update
The issue doesn't seem to be specific to my custom components, or to watchers. I decided to ignore this and move on while waiting for an answer from StackOverflow and ran into the issue again with Quasar's components:
<template>
...
<q-card-section>
<div class="row">
<div class="col">
<q-slider v-model="age" :min="0" :max="100" />
</div>
<div class="col">
{{ ageText }}
</div>
</div>
</q-card-section>
...
</template>
<script lang="ts">
...
get age() {
return globalState.age;
}
set age(val) {
globalState.age = val;
this.ageText = "You are " + val + " years old";
}
...
</script>
This led me to try using no custom components whatsoever:
<template>
...
<input type="text" v-model="test" />
<p>Text: {{ test }}</p>
...
</template>
<script lang="ts">
let testVal = "";
...
get test() { return testVal; }
set test(val) { testVal = val; }
...
</script>
Once again: No reactivity. When I use a computed property with v-model nothing seems to change after the call to set
If globalState were just an Object, then it would not be reactive, so computed is only going to read its value once. Same for testVal, which is just a String (also not reactive itself).
To make the test computed prop reactive to testVal, create testVal with Vue.observable():
const testVal = Vue.observable({ x: '' })
#Component
export default class Profile extends Vue {
get test() { return testVal.x }
set test(val) { testVal.x = val }
}
Similarly for globalState, exporting a Vue.observable() would allow your computed props to be reactive:
// globalState.js
export default Vue.observable({
politicalParty: ''
})

Vue Typescript Konva. Canvas onClick throws "Uncaught SyntaxError: Function statements require a function name"

I doubt this is a Konva specific question but since I am using the library as the basis of my example I wanted to include the bare minimum of my code to replicate this.
Whenever the onMouseDownHandler (and in my full code onMouseUpHandler as well) is fired by clicking on the canvas the error "Uncaught SyntaxError: Function statements require a function name" is thrown in my google dev tools console as shown below.
From reading the docs I have written this using the correct syntax. Any help would be greatly appreciated as I have spent many an hour trying to resolve this.
<template>
<v-stage
ref="stage"
class="konva-stage"
:config="stageSize"
:onMouseDown="onMouseDownHandler"
/>
</template>
<script lang="ts">
import Vue from 'vue'
import { Prop } from 'vue-property-decorator'
import Component, { mixins } from 'vue-class-component'
import Konva from 'konva'
#Component({
name: 'MapCanvas'
})
export default class MapButtons extends Vue {
stageSize = {
width: window.innerWidth,
height: window.innerHeight
}
onMouseDownHandler (e: any) : void {
console.log('mouse down')
}
}
</script>
<style scoped lang="scss">
.konva-stage {
background-color: white;
width: 100%;
height: 100%;
position: absolute;
}
</style>
Fixed it myself!
<v-stage
ref="stage"
class="konva-stage"
:config="stageSize"
:onMouseDown="onMouseDownHandler" <-- NOT THIS
#mousedown="onMouseDownHandler" <-- THIS
/>

Why wont the component respond to the custom event?

I am emitting an event called emit-event-main2-counter in Main2.vue
Why will the data cntr in Bottom.vue not update?
App.vue
<template>
<div class="app">
<Main2 />
<Bottom/>
</div>
</template>
<script>
import Main2 from "./components/Main2";
import Bottom from "./components/Bottom";
export default {
components: {
Main2,
Bottom
},
}
</script>
<style scoped>
h1 {
color: red;
}
</style>
Main2.vue
<template>
<div>
main2 template <span class="text1">{{message}}</span>
<button type="button" v-on:click="btnClickButton">my click</button>
<div>{{counter}}</div>
</div>
</template>
<script>
import appInput from "./appInput.vue";
export default {
data: () => {
return {
message: "theText",
counter: 0,
}
},
components: {
appInput,
},
methods: {
btnClickButton(e) {
this.$root.$emit('emit-event-main2-counter', this.counter)
console.log('button');
this.counter +=1;
}
}
}
</script>
<style scoped>
.text1 {
color:red;
}
.text2 {
color:blue;
}
</style>
Bottom.vue
<template>
<div class="Bottom" v-on:emit-event-main2-counter="cntr = $event">
bottom text and cntr ({{cntr}})
</div>
</template>
<script>
export default {
data: () => {
return {
cntr: 0
}
},
}
</script>
<style scoped>
</style>
You could emit an event to parent from Main2 having as parameters this.counter and in the parent one receive that and pass it through props to Bottom
In Main2 :
this.$emit("emit-event-main2-counter",this.counter);
in the parent component :
<template>
<Main2 v-on:emit-event-main2-counter="sendToBottom"/>
<Bottom :cntr="pcounter"/>
....
</template>
data:{
pcounter:0
},
methods:{
sendToBottom(c){
this.pcounter=c
}
}
Bottom should have property called cntr
props:["cntr"]
Bottom.vue
<template>
<div class="Bottom" >
bottom text and cntr ({{cntr}})
</div>
</template>
<script>
export default {
props:["cntr"],
data: () => {
return {
}
},
}
</script>
If you want to use root events, you need to emit the event with this.$root.$emit() and also listen to the event on the root like this: this.$root.$on().
You should use it directly in the script part. Listen to the root event e.g. in the created() hook and then disable it with $off in the beforeDestroy() hook.
However, I wouldn't encourage you to use $root events. It is usually better to communicate between the components like #BoussadjraBrahim proposed in his answer.
If you have a more complex application, it makes sense to take a look at Vuex and store the complete state in the Vuex store. By doing this, you can watch the global application state in the components and react if it changes. In this scenario, you would use the Vuex store instead of a root EventBus.

Changing body styles in vue router

I'm using Vue router with two pages:
let routes = [
{
path: '/',
component: require('./components/HomeView.vue')
},
{
path: '/intro',
component: require('./components/IntroView.vue')
}
]
This works fine, except that each of my components has different body styling:
HomeView.vue:
<template>
<p>This is the home page!</p>
</template>
<script>
export default {
}
</script>
<style>
body {
background: red;
}
</style>
IntroView.vue:
<template>
<div>
<h1>Introduction</h1>
</div>
</template>
<script>
export default {
}
</script>
<style>
body {
background: pink;
}
</style>
My goal is to have these two pages have different background styles (eventually with a transition between them). But at the moment when I go to the home route (with the red background), then click the intro route, the background colour stays red (I want it to change to pink).
Edit:
index.html:
<body>
<div id="app">
<router-link to="/" exact>Home</router-link>
<router-link to="/intro">Introduction</router-link>
<router-view></router-view>
</div>
<script src="/dist/build.js"></script>
</body>
I got it working with the lifecycle hook beforeCreate and a global stylesheet. In global.css:
body.home {
background: red;
}
body.intro {
background: pink;
}
In the <script> section of HomeView.vue:
export default {
beforeCreate: function() {
document.body.className = 'home';
}
}
And similar in IntroView.vue.
watch: {
$route: {
handler (to, from) {
const body = document.getElementsByTagName('body')[0];
if (from !== undefined) {
body.classList.remove('page--' + from.name.toLowerCase());
}
body.classList.add('page--' + to.name.toLowerCase());
},
immediate: true,
}
},
Another fairly simple solution, add it to your base App.vue file. The to.name can be replaced with to.meta.class or similar for something more specific. This is a nice do it once and it works forever type solution though.
If the class is view specific, may be this will help
methods: {
toggleBodyClass(addRemoveClass, className) {
const el = document.body;
if (addRemoveClass === 'addClass') {
el.classList.add(className);
} else {
el.classList.remove(className);
}
},
},
mounted() {
this.toggleBodyClass('addClass', 'mb-0');
},
destroyed() {
this.toggleBodyClass('removeClass', 'mb-0');
},
Move the methods section to a mixin and then the code can be DRY.
Alternatively you can use this
vue-body-class NPM
vue-body-class GitHub
It allows to control your page body classes with vue-router.
Wrote this when faced the similar issue.
It also refers to Add a class to body when component is clicked?
I ran into an issue when I wanted to modify the styles of the html and body tags along with the #app container on specific routes and what I found out is that for various reasons, this can be quite complicated.
After reading through:
#Saurabh's answer on another relative question: https://stackoverflow.com/a/42336509/2110294
#Mteuahasan's comment above regarding Evan You's suggestion
#GluePear's / OP's answer to this question: https://stackoverflow.com/a/44544595/2110294
Sass style inclusion headaches: https://github.com/vuejs/vue-loader/issues/110#issuecomment-167376086
In your App.vue (could be considered as the centralised state):
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'my-app',
methods: {
handleStyles () {
// Red style to the body tag for the home page
if (['/'].includes(this.$route.path)) document.body.className = 'bg-red'
// Pink style to the body tag for all other pages
else if (document.body.classList.contains('bg-red')) document.body.className = 'bg-pink'
}
},
// Handle styles when the app is initially loaded
mounted () {
this.handleStyles()
},
// Handle styles when the route changes
watch: {
'$route' () {
this.handleStyles()
}
}
}
</script>
<style>
.bg-red {
background: red;
}
.bg-pink {
background: pink;
}
</style>
So for the route / you get the red style and for all other routes the pink style is applied.
The handleStyles logic could have been dealt with by the beforeCreated hook however in my case, this would only affect the html and body styles but the #app element where the router view is rendered into would only available when the dom has been mounted so I think that it is a slightly more extensible solution.
Top answer is right, but need some optimization.
Because that answer doesn't work when one refreshes that page. Reason is that dom is not loaded done when set the style you want.
So, better solution is this:
beforeCreate() {
this.$nextTick(() => {
document.querySelector('body').style.backgroundColor = '#f2f2f2'
})
},
beforeDestroy() {
document.querySelector('body').style.backgroundColor = ''
},
By wrapping style setting handler in this.$nextTick, the style will be set when dom is loaded. So you can get correct styles when refresh page
You can also do it directly in the router file using the afterEach hook:
mainRouter.afterEach((to) => {
if (["dialogs", "snippets"].includes(to.name)) {
document.body.style.backgroundColor = "#F7F7F7";
// or document.body.classList.add(className);
} else {
document.body.style.backgroundColor = "#FFFFFF";
// or document.body.classList.remove(className);
}
});
afterEach hook documentation
to is a route object which contains the route name (if named), path, etc. Documentation for all the props
You can use scoped attribute in the style element. Then the style will be limited only to that vue file.
HomeView.vue:
<template>
<p>This is the home page!</p>
</template>
<script>
export default {
}
</script>
<style scoped>
body {
background: red;
}
</style>
IntroView.vue:
<template>
<div>
<h1>Introduction</h1>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
body {
background: pink;
}
</style>

Categories

Resources