how do I save assigned values from inside onMounted hook in Vue 3? My intention of saving the width and height values is so that can use to manipulate the values inside a custom-directive outside of the setup function later on.
I realised that it is only possible manipulating inside the onMounted and using watch see if there is a change to the value. But even so, after assigning the values, it is still undefined.
Is using Vuex the way to go for my current solution?
Because I can only access DOM properties inside onMounted hook and not anywhere else.
<template>
<div class="outer">
<div class="container">
<div>
<div class="border">
<img
id="image"
ref="image"
src="#/assets/1.jpg"
class="image"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, onMounted, watch } from "vue";
const widthVal = ref<number>()
const heightVal = ref<number>()
export default defineComponent({
setup() {
const image = ref<HTMLElement | null>(null)
onMounted(() => {
if (image.value) {
widthVal.value = image.value.offsetWidth;
heightVal.value = image.value.offsetHeight;
console.log('width: ', widthVal.value)
console.log('height: ', heightVal.value)
}
})
watch([widthVal, heightVal], (newVal, oldVal) => {
widthVal.value = newVal[0];
heightVal.value = newVal[1];
console.log(widthVal.value)
console.log(heightVal.value)
})
// becomes undedefined
console.log('width: ', widthVal.value)
return { image }
}
});
</script>
<style>
p {
color: yellow;
}
.outer {
margin: 1em;
display: flex;
justify-content: center;
height: 100vh;
}
.container {
background: rgb(98, 98, 98);
border-radius: 5px;
width: 950px;
height: 650px;
padding: 1em;
overflow: hidden;
font-family: "Trebuchet Ms", helvetica, sans-serif;
}
img {
width: 950px;
height: 650px;
/* remove margins */
margin-left: -18px;
margin-top: -18px;
}
</style>
If you inspect widthVal inside setup() and not inside the watch or onMounted function it gets called BEFORE the values are assigned cause assignments inside setup happen even before the beforeCreate hook.
See: lifecycle hooks
EDIT:
If you really want to use widthVal/heightVal inside setup I'd recommend using it within a function (or a watcher, whatever you need) and calling that inside onMounted after you initialized widthVal/heightVal. E.g.:
const doSomethingElse = () => {
// Use widthVal and heightVal here...
}
onMounted(() => {
widthVal.value = newVal[0];
heightVal.value = newVal[1];
doSomethingElse();
})
...
Related
How can i change the body{overflow:hidden} when my modal it will be open?
for example it will be my modal, when its open, i would like to apply this style body{overflow:hidden}
<div v-if="dialogFoundation">
i am using vuejs3, i am using setup(){...}
The best performance would be to use javascript plain. You can add Eventlistener top the modal trigger Element. In my example i use a button. If it triggered then you can use classList and assign the body a class. In my example .dark.
Vue version
<!-- Use preprocessors via the lang attribute! e.g. <template lang="pug"> -->
<template>
<div id="app">
<h1>{{message}}</h1>
<p></p>
<button #click="doSomething">Modal</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Welcome to Vue!'
};
},
methods: {
doSomething() {
const b = document.querySelector('body');
b.classList.toggle('dark');
}
}
};
</script>
<!-- Use preprocessors via the lang attribute! e.g. <style lang="scss"> -->
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
a,
button {
color: #4fc08d;
}
button {
background: none;
border: solid 1px;
border-radius: 2em;
font: inherit;
padding: 0.75em 2em;
}
.dark {
background: black;
opacity: 0.4;
}
</style>
Vanilla JS
const btn = document.querySelector('button');
btn.addEventListener('click', () => {
const b = document.querySelector('body');
b.classList.toggle('dark');
})
.dark {
background: black;
opacity: 0.4;
}
<body>
<div></div>
<button>click</button>
</body>
You can use watchers in Vue.js for solving this problem.
When variables changes you can check whether it is true or not, and if true change overflow of body to hidden.
{
watch: {
dialogFoundation(dialogFoundation) {
document.body.style.overflow = dialogFoundation ? "hidden" : "auto"
}
}
}
But I think this is not good solution. You can set this styles to your app element
#app {
width: 100%;
height: 100%;
overflow: auto;
}
and you can change style of app element using Vue directives.
<template>
<div id="app" :class="{ hidden: dialogFoundation }">
Long text....
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const dialogFoundation = ref(true);
return { dialogFoundation };
},
};
</script>
<style>
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
overflow: auto;
}
#app.hidden {
overflow: hidden;
}
</style>
Code in codesandbox - https://codesandbox.io/s/immutable-glitter-rwc2iy?file=/src/App.vue
How to access name of the component of the slot?
I want to create a copy of the component provided in the slot:
const child1 = slot
const child2 = h(???, slot.props)
So that child1 renders exactly as child2.
I need this, so that I can change properties of that VNode, for examples classes.
Context
import { h } from 'vue';
export default {
setup(props, { slots }) {
const children = [];
for (const slot of slots.default()) {
const child = h(???, slot.props)
children.push(h('div', [child]));
}
return () =>
h('div', children);
},
};
Background
I want to make a component similar to q-button-group:
I need 2 components TButton and TButtonGroup so that I can style TButton independently and create groups just by putting those buttons inside the TButtonGroup.
Example
<TButtonGroup>
<TButton label="Two" />
<TButton label="Three" />
</TButtonGroup>
TButton should have a different list of classes:
when it's inside TButtonGroup: px-4 py-2
when it's not: border rounded-lg px-4 py-2
See full html
Playground
https://stackblitz.com/edit/vue3-button-group-razbakov?file=src%2Fcomponents%2FGroupRender.js
Component name of vnode won't tell much, components are already resolved at this point. VNode's element or component is stored in type property.
The problem with your approach is that render function is an alternative to component template, not a way to access the entire DOM element hierarchy. There will be no TButton child elements like div in render function, only TButton vnode itself. It needs to be rendered in order to access its children.
If TButton were someone else's component which initial behaviour needs to be modified, this could be done by adding some directive to it and accessing component's children elements.
But since TButton is your own component that can be modified to your needs, the most straightforward way is to make it change classes depending on a prop and provide this prop when it's inside TGroup, i.e.:
const child = h(slot.type, {...slot.props, group: true}, slot.children);
children.push(child);
Use the component type you created:
const { h } = Vue
const TBtn = {
props: {
staticClass: {
type: Array,
default: () => [],
},
},
template: `
<div
class="t-btn px-4 py-2"
>
<slot></slot>
</div>
`
}
const TBtnGroup = {
setup(props, {
slots
}) {
const children = [...slots.default()]
.map(slot => h(slot.type, {
class: ['border', 'rounded-lg']
}, slot))
return () => h('div', {
class: ['d-flex']
}, children)
},
}
const App = {
template: `
<t-btn>OUTSIDE 1</t-btn>
<t-btn>OUTSIDE 2</t-btn>
<br />
<t-btn-group>
<t-btn>INSIDE 1</t-btn>
<t-btn>INSIDE 2</t-btn>
<t-btn>INSIDE 3</t-btn>
</t-btn-group>
`
}
const app = Vue.createApp(App)
app.component('TBtn', TBtn)
app.component('TBtnGroup', TBtnGroup)
app.mount('#app')
.px-4 {
padding-left: 16px;
padding-right: 16px;
}
.py-2 {
padding-top: 8px;
padding-bottom: 8px;
}
.border {
border: 1px solid black;
}
.rounded-lg {
border-radius: 8px;
}
.d-flex {
display: flex;
gap: 8px;
}
.t-btn:hover {
cursor: pointer;
background-color: black;
color: white;
}
<script src="https://unpkg.com/vue#next"></script>
<div id="app"></div>
I'm using a computed object. I want to expose the properties of that object in the setup function, but I haven't found how.
export default {
setup() {
const counter = Vue.ref(0)
const data = Vue.computed(() => ({
plus1: counter.value + 1,
plus2: counter.value + 2,
}))
const increment = () => {
counter.value++
}
return {
counter,
plus1: data.value.plus1, // This is bad, no reactivity
plus2: data.value.plus2,
increment
}
},
};
Full code pen here : https://codepen.io/philfontaine/pen/KKmzJrK
EDIT
Alternatives I have considered:
Using the object data directly in the template
Using a computed property of each of the properties
But these are alternatives, not how I really wished it could work.
You could use a reactive object with toRef instead.
Vue.createApp({
setup() {
const counter = Vue.ref(0);
const data = Vue.reactive({
get plus1() { return counter.value + 1; },
get plus2() { return counter.value + 2; },
});
const increment = () => counter.value++;
return {
counter,
plus1: Vue.toRef(data, 'plus1'),
plus2: Vue.toRef(data, 'plus2'),
increment
}
}
}).mount('#app');
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
a, button {
color: #4fc08d;
}
button {
background: none;
border: solid 1px;
border-radius: 2em;
font: inherit;
padding: 0.75em 2em;
margin: 0.85em;
}
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<div>{{ counter }}</div>
<div>{{ plus1 }}</div>
<div>{{ plus2 }}</div>
<button #click="increment">Increment</button>
</div>
Although, this is a little awkward to me. I would use some other method if possible; why do plus1 and plus2 need to be a part of the same object? What's wrong with
const counter = ref(0);
const plus1 = computed(() => counter.value + 1);
const plus2 = computed(() => counter.value + 2);
this? I'm sure your real example is much more complex, but it may be worth thinking about: computed is the intuitive thing to use here, and on individual variables.
You cannot directly flatten a computed object (no API is currently provided to do that).
I ended up resorting to using the computed object directly in the template.
I make a spinner component in my project and I pass some props to load the spinner and it's like this
Vue.component("Spinner", require("./components/Loading/Loading.vue").default, {
props: ["loading"]
});
.spinner {
width: 100%;
height: 100%;
background-color: white;
position: fixed;
transition: 0.5s;
z-index: 999;
}
.ring {
position: absolute;
top: 50%;
left: 50%;
margin: 0 auto;
height: 100%;
z-index: 1000;
}
<template>
<div class="spinner">
<div class="ring">
<half-circle-spinner :size="60" color="#1ABC9C"/>
</div>
</div>
</template>
and I call it in the other components like this:
<template>
<Spinner key="list-key" :loading="true"/>
</template>
The thing I need is to make this.spin = false when all the DOM elements are loaded on my page. please let me know your ideas. :)
We'll initialize a isLoading variable to true when the component is created and then set it to false in the mounted hook - by using nextTick in the mounted hook it will execute after all dom children have been loaded: https://v2.vuejs.org/v2/api/#mounted
You can try using v-if & v-else to show/hide the content/spinner based on a data attribute (such as isLoading) you can change once the content has loaded:
<template>
<div>
<Spinner v-if="isLoading"/>
<div v-else>
... actual dom content
</div>
</div>
</template>
<script>
export default {
data () {
return {
isLoading: true
}
},
mounted () {
this.$nextTick(() => {
this.isLoading = false
})
}
}
</script>
I have come across an issue where the implementation of slots in a webcomponent is not functioning as expected. My understanding of Web Components, Custom Elements and Slots is that elements rendered in a slot should inherit their style from the document and not the Shadow DOM however the element in the slot is actually being added to the Shadow DOM and therefore ignoring the global styles. I have created the following example to illustrate the issue that I am having.
shared-ui
This is a Vue application that is compiled to web components using the cli (--target wc --name shared-ui ./src/components/*.vue)
CollapseComponent.vue
<template>
<div :class="[$style.collapsableComponent]">
<div :class="[$style.collapsableHeader]" #click="onHeaderClick" :title="title">
<span>{{ title }}</span>
</div>
<div :class="[$style.collapsableBody]" v-if="expanded">
<slot name="body-content"></slot>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
#Component({})
export default class CollapsableComponent extends Vue {
#Prop({ default: "" })
title!: string;
#Prop({default: false})
startExpanded!: boolean;
private expanded: boolean = false;
constructor() {
super();
this.expanded = this.startExpanded;
}
get isVisible(): boolean {
return this.expanded;
}
onHeaderClick(): void {
this.toggle();
}
public toggle(expand?: boolean): void {
if(expand === undefined) {
this.expanded = !this.expanded;
}
else {
this.expanded = expand;
}
this.$emit(this.expanded? 'expand' : 'collapse');
}
public expand() {
this.expanded = true;
}
public collapse() {
this.expanded = false;
}
}
</script>
<style module>
:host {
display: block;
}
.collapsableComponent {
background-color: white;
}
.collapsableHeader {
border: 1px solid grey;
background: grey;
height: 35px;
color: black;
border-radius: 15px 15px 0 0;
text-align: left;
font-weight: bold;
line-height: 35px;
font-size: 0.9rem;
padding-left: 1em;
}
.collapsableBody {
border: 1px solid black;
border-top: 0;
border-radius: 0 0 10px 10px;
padding: 1em;
}
</style>
shared-ui-consumer
This is a vue application that imports the shared-ui web component using a standard script include file.
App.vue
<template>
<div id="app">
<shared-ui title="Test">
<span class="testClass" slot="body-content">
Here is some text
</span>
</shared-ui>
</div>
</template>
<script lang="ts">
import 'vue'
import { Component, Vue } from 'vue-property-decorator';
#Component({ })
export default class App extends Vue {
}
</script>
<style lang="scss">
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.testClass{
color: red;
}
</style>
main.ts
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
// I needed to do this so the web component could reference Vue
(window as any).Vue = Vue;
new Vue({
render: h => h(App),
}).$mount('#app');
In this example I would expect the content inside the container to have red text however because Vue is cloning the element into the Shadow DOM the .testClass style is being ignored and the text is rendered with a black fill.
How can I apply .testClass to the element inside of my web component?
Ok, so I managed to find a workaround for this that uses native slots and renders the child components correctly in the correct place in the DOM.
In the mounted event wire up the next tick to replace the innerHtml of your slot container with a new slot. You can get fancy and do some cool replacements for named slots and whatnot but this should suffice for illustrating the workaround.
shared-ui
This is a Vue application that is compiled to web components using the cli (--target wc --name shared-ui ./src/components/*.vue)
CollapseComponent.vue
<template>
<div :class="[$style.collapsableComponent]">
<div :class="[$style.collapsableHeader]" #click="onHeaderClick" :title="title">
<span>{{ title }}</span>
</div>
<div ref="slotContainer" :class="[$style.collapsableBody]" v-if="expanded">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
#Component({})
export default class CollapsableComponent extends Vue {
#Prop({ default: "" })
title!: string;
#Prop({default: false})
startExpanded!: boolean;
private expanded: boolean = false;
constructor() {
super();
this.expanded = this.startExpanded;
}
get isVisible(): boolean {
return this.expanded;
}
onHeaderClick(): void {
this.toggle();
}
//This is where the magic is wired up
mounted(): void {
this.$nextTick().then(this.fixSlot.bind(this));
}
// This is where the magic happens
fixSlot(): void {
// remove all the innerHTML that vue has place where the slot should be
this.$refs.slotContainer.innerHTML = '';
// replace it with a new slot, if you are using named slot you can just add attributes to the slot
this.$refs.slotContainer.append(document.createElement('slot'));
}
public toggle(expand?: boolean): void {
if(expand === undefined) {
this.expanded = !this.expanded;
}
else {
this.expanded = expand;
}
this.$emit(this.expanded? 'expand' : 'collapse');
}
public expand() {
this.expanded = true;
}
public collapse() {
this.expanded = false;
}
}
</script>
<style module>
:host {
display: block;
}
.collapsableComponent {
background-color: white;
}
.collapsableHeader {
border: 1px solid grey;
background: grey;
height: 35px;
color: black;
border-radius: 15px 15px 0 0;
text-align: left;
font-weight: bold;
line-height: 35px;
font-size: 0.9rem;
padding-left: 1em;
}
.collapsableBody {
border: 1px solid black;
border-top: 0;
border-radius: 0 0 10px 10px;
padding: 1em;
}
</style>
shared-ui-consumer
This is a vue application that imports the shared-ui web component using a standard script include file.
App.vue
<template>
<div id="app">
<shared-ui title="Test">
<span class="testClass" slot="body-content">
Here is some text
</span>
</shared-ui>
</div>
</template>
<script lang="ts">
import 'vue'
import { Component, Vue } from 'vue-property-decorator';
#Component({ })
export default class App extends Vue {
}
</script>
<style lang="scss">
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.testClass{
color: red;
}
</style>
main.ts
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
// I needed to do this so the web component could reference Vue
(window as any).Vue = Vue;
new Vue({
render: h => h(App),
}).$mount('#app');