How to create reactive() object with custom setter (debounced) like with customRef()
You can use computed inside reactive. Namely, you might want to use a Writable Computed (getter/setter):
import { ref, reactive, computed } from 'vue'
const whatever = ref('test')
const state = reactive({
myProp: computed({
get() {
console.log('myProp was read');
return whatever.value
},
set(val) {
console.log(`myProp was set to ${val}`)
whatever.value = val
}
})
})
Test:
const { createApp, ref, reactive, computed, toRefs } = Vue
const app = createApp({
setup() {
const whatever = ref('test')
const state = reactive({
myProp: computed({
get() {
console.log('myProp was read');
return whatever.value
},
set(val) {
console.log(`myProp was set to ${val}`)
whatever.value = val
}
}),
emitter: computed({
get() {
return true
},
set(val) {
console.log(`I was set to ${val} but I'm not changing value...`)
}
})
})
return { ...toRefs(state), whatever }
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.2.41/dist/vue.global.prod.js"></script>
<div id="app">
<input v-model="myProp" />
<pre v-text="{
whatever,
myProp,
emitter
}"></pre>
<button #click="emitter = !emitter">Click me!</button>
</div>
Notice the getter is only called once per change, not once for each place where it's used in <template>.
Related
I'm attempting to mix composables into a class based component setup, as part of a slow migration from Vue 2 to Vue 3. However, I am struggling to referenced return values from the setup function within the class itself.
I have something similar to:
#Component({
setup() {
const fullscreenElement = ref<HTMLElement | undefined>();
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(fullscreenElement);
return {
fullscreenElement,
isFullscreen,
toggleFullscreen,
};
},
})
export default class MyClassComponent extends Vue {
// How to access isFullscreen et al. here ??
}
As in the above example, I can't seem to reference how I would use e.g., isFullscreen etc from within the component itself?
Docs:
ref()
Computed Properties
Composables
const { ref, computed, createApp } = Vue;
const useFullscreen = function() {
const _isFullscreen = ref(false);
const isFullscreenFunc = function() {
return _isFullscreen;
}
const isFullscreenComputed = computed(function() {
return _isFullscreen;
})
const toggleFullscreen = function() {
_isFullscreen.value = !_isFullscreen.value;
}
return {isFullscreenFunc, isFullscreenComputed, toggleFullscreen}
}
const MyComponent = {
setup() {
const { isFullscreenFunc, isFullscreenComputed, toggleFullscreen } = useFullscreen();
return {
toggleFullscreen,
isFullscreenFunc,
isFullscreenComputed
}
},
methods: {
toggle() {
this.toggleFullscreen();
},
show() {
alert(`isFullscreenFunc: ${this.isFullscreenFunc().value}\n isFullscreenComputed: ${this.isFullscreenComputed.value}`);
}
},
template: `
<div>
isFullscreenFunc: {{isFullscreenFunc().value}}<br /><br />
isFullscreenComputed: {{isFullscreenComputed.value}}
<br/><br/><button type="button" #click="toggle()">toggle</button>
<button type="button" #click="show()">show</button>
</div>`
}
const App = {
components: {
MyComponent
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<my-component>
</my-component>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
I have a design in setting page,every one of them hava reset button, now i using pinia to be store library.
I kown $reset is reset the whole pinia state,so,how to reset one of data in pinia state?
The typical way I do this:
const defaultState = {
foo: 'bar'
}
export const useFoo = defineStore('foo', {
state: () => ({ ...defaultState }),
actions: {
reset() {
Object.assign(this, defaultState);
}
}
})
You get the initial state and a reset() action which resets whatever state has to the initial. Obviously, you can pick and choose what you put in defaultState.
If you only want to reset one particular state prop, without touching anything else, just assign the default value to it:
useFoo().foo = 'bar';
If you find it useful, you can also have a generic update, where you can assign multiple values to state in one call:
actions: {
update(payload) {
Object.assign(this, payload)
}
}
Use it like:
useFoo().update({
foo: 'bar',
// add more props if needed...
});
Last, but not least, lodash's pick can be used to pick and choose what gets reset, from the defaultState values, without having to specify the actual values:
import { pick } from 'lodash-es';
const defaultState = {
foo: 'bar',
boo: 'far'
};
export const useFoo = defineStore('foo', {
state: () => ({ ...defaultState }),
actions: {
reset(keys) {
Object.assign(this, keys?.length
? pick(defaultState, keys)
: defaultState // if no keys provided, reset all
);
}
}
})
use it like:
useFoo().reset(['foo']);
This only resets foo to 'bar', but doesn't touch current value of boo.
To reset both (using the action above):
useFoo().reset(['foo', 'boo']);
...or useFoo().reset() or useFoo().reset([]), both of which reset all the state, because the keys?.length condition is falsey.
Here's a working example:
const { createPinia, defineStore, storeToRefs } = Pinia;
const { createApp, reactive, toRefs } = Vue;
const defaultState = {
foo: "bar",
boo: "far",
};
const useStore = defineStore("foobar", {
state: () => ({ ...defaultState }),
actions: {
reset(keys) {
Object.assign(
this,
keys?.length ? _.pick(defaultState, keys) : defaultState
);
},
},
});
const pinia = createPinia();
const app = createApp({
setup() {
const store = useStore();
const localState = reactive({
resetFoo: false,
resetBoo: false,
});
const resetStore = () => store.reset(
[
localState.resetFoo ? "foo" : null,
localState.resetBoo ? "boo" : null,
].filter((o) => o)
);
return { ...storeToRefs(store), ...toRefs(localState), resetStore };
},
});
app.use(pinia);
app.mount("#app");
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-demi"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pinia/2.0.28/pinia.iife.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<div id="app">
<input v-model="foo" />
<input v-model="boo" />
<pre v-text="JSON.stringify({foo, boo}, null, 2)"></pre>
<hr>
<label>
<input type="checkbox" v-model="resetFoo" />ResetFoo</label>
<label>
<input type="checkbox" v-model="resetBoo" />ResetBoo</label>
<button #click="resetStore">Reset</button>
</div>
Above example doesn't reset one property to the default value when the property is already changed.
That's because the defaultState is reactive, you need to copy the defaultState so it's not reactive anymore.
import _pick from 'lodash.pick';
const defaultState = {
foo: 'bar',
};
export const useStore = defineStore('store', {
state: () => ({...defaultState}),
actions: {
reset(keys) {
Object.assign(this, keys?.length
? _pick(defaultState, keys)
: defaultState // if no keys provided, reset all
);
}
}
})
Use it like this
useStore().reset(['foo']);
This will now reset foo back to bar
I have a Vue component. This component has a computed prop & a watch:
const resetComponent = computed(()=>{
return store.state.filtros.room_amount
})
watch(resetComponent, () => {
if(resetComponent.value.compare == '>' && resetComponent.value.valor == '' ){
console.log('RESET COMPONENT')
}
})
My console.log('RESET COMPONENT') runs correctly, when it should.
But instead, i want re-render all my component, that is, return to its initial state. There's some way?
This is my full component
<template>
<FiltroCantidad :data="data" />
</template>
<script>
import SelectButton from "primevue/selectbutton";
import FiltroCantidad from "../utils/FiltroCantidad.vue";
import { computed, ref, watch } from "vue";
import { useStore } from 'vuex';
export default {
setup(props, context) {
const store = useStore()
const data = ref({
label: "Ambientes",
value: "room_amount",
action: "roomAmountAction",
});
const resetComponent = computed(()=>{
return store.state.filtros.room_amount
})
watch(resetComponent, () => {
if(resetComponent.value.compare == '>' && resetComponent.value.valor == '' ){
console.log('RESET COMPONENT')
}
})
return { data, resetComponent };
},
components: {
SelectButton,
FiltroCantidad,
},
};
</script>
One way to re-render the component is to apply a key attribute that changes:
<FiltroCantidad :data="data" :key="myKey" />
export default {
setup() {
//...
const myKey = ref(0)
watch(resetComponent, () => {
if(/* need to reset */) {
myKey.value++
}
})
return { myKey }
}
}
I am trying to create a multi step form with composition api.
In vue 2 I used to do it this way
email: {
get() {
return this.$store.state.email
},
set(value) {
this.$store.commit("setEmail", value)
}
},
Now I have my own store, I made this computed property to pass to my component stEmail: computed(() => state.email). How can I actually use this in get set?
I am trying do something like this but completely doesn't work.
let setMail = computed(({
get() {
return stEmail;
},
set(val) {
stEmail.value = val;
}
}))
const state = reactive({
email: "",
})
export function useGlobal() {
return {
...toRefs(state),
number,
}
}
Or is there better way now to make multi step forms?
You can do the same with the Composition API. Import useStore from the vuex package and computed from vue:
import { computed } from 'vue';
import { useStore } from 'vuex';
And then use it in your setup() function like this:
setup: () => {
const store = useStore();
const email = computed({
get() {
return store.state.email;
},
set(value) {
store.commit("setEmail", value);
}
});
return { email };
}
If you want to avoid using vuex, you can just define variables with ref() and export them in a regular JavaScript file. This would make your state reusable in multiple files.
state.js
export const email = ref('initial#value');
Form1.vue/Form2.vue
<template>
<input v-model="email" />
</template>
<script>
import { email } from './state';
export default {
setup() {
return { email };
}
};
</script>
As Gregor pointed out, the accepted answer included an anonymous function that doesn't seem to work, but it will work if you just get rid of that part. Here's an example using <script setup> SFC
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const email = computed({
get() {
return store.state.email
},
set(value) {
store.commit("setEmail", value)
}
})
</script>
<template>
<input type="email" v-model="email" />
</template>
I am currently working on a custom validation and would like to, if possible, access a child components and call a method in there.
Form wrapper
<template>
<form #submit.prevent="handleSubmit">
<slot></slot>
</form>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup(props, { slots }) {
const validate = (): boolean => {
if (slots.default) {
slots.default().forEach((vNode) => {
if (vNode.props && vNode.props.rules) {
if (vNode.component) {
vNode.component.emit('validate');
}
}
});
}
return false;
};
const handleSubmit = (ev: any): void => {
validate();
};
return {
handleSubmit,
};
},
});
</script>
When I call slot.default() I get proper list of child components and can see their props. However, vNode.component is always null
My code is based from this example but it is for vue 2.
If someone can help me that would be great, or is this even possible to do.
I found another solution, inspired by quasar framework.
Form component provide() bind and unbind function.
bind() push validate function to an array and store in Form component.
Input component inject the bind and unbind function from parent Form component.
run bind() with self validate() function and uid
Form listen submit event from submit button.
run through all those validate() array, if no problem then emit('submit')
Form Component
import {
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
toRefs,
provide
} from "vue";
export default defineComponent({
name: "Form",
emits: ["submit"],
setup(props, { emit }) {
const state = reactive({
validateComponents: []
});
provide("form", {
bind,
unbind
});
onMounted(() => {
state.form.addEventListener("submit", onSubmit);
});
onBeforeUnmount(() => {
state.form.removeEventListener("submit", onSubmit);
});
function bind(component) {
state.validateComponents.push(component);
}
function unbind(uid) {
const index = state.validateComponents.findIndex(c => c.uid === uid);
if (index > -1) {
state.validateComponents.splice(index, 1);
}
}
function validate() {
let valid = true;
for (const component of state.validateComponents) {
const result = component.validate();
if (!result) {
valid = false;
}
}
return valid;
}
function onSubmit() {
const valid = validate();
if (valid) {
emit("submit");
}
}
}
});
Input Component
import { defineComponent } from "vue";
export default defineComponent({
name: "Input",
props: {
rules: {
default: () => [],
type: Array
},
modelValue: {
default: null,
type: String
}
}
setup(props) {
const form = inject("form");
const uid = getCurrentInstance().uid;
onMounted(() => {
form.bind({ validate, uid });
});
onBeforeUnmount(() => {
form.unbind(uid);
});
function validate() {
// validate logic here
let result = true;
props.rules.forEach(rule => {
const value = rule(props.modelValue);
if(!value) result = value;
})
return result;
}
}
});
Usage
<template>
<form #submit="onSubmit">
<!-- rules function -->
<input :rules="[(v) => true]">
<button label="submit form" type="submit">
</form>
</template>
In the link you provided, Linus mentions using $on and $off to do this. These have been removed in Vue 3, but you could use the recommended mitt library.
One way would be to dispatch a submit event to the child components and have them emit a validate event when they receive a submit. But maybe you don't have access to add this to the child components?
JSFiddle Example
<div id="app">
<form-component>
<one></one>
<two></two>
<three></three>
</form-component>
</div>
const emitter = mitt();
const ChildComponent = {
setup(props, { emit }) {
emitter.on('submit', () => {
console.log('Child submit event handler!');
if (props && props.rules) {
emit('validate');
}
});
},
};
function makeChild(name) {
return {
...ChildComponent,
template: `<input value="${name}" />`,
};
}
const formComponent = {
template: `
<form #submit.prevent="handleSubmit">
<slot></slot>
<button type="submit">Submit</button>
</form>
`,
setup() {
const handleSubmit = () => emitter.emit('submit');
return { handleSubmit };
},
};
const app = Vue.createApp({
components: {
formComponent,
one: makeChild('one'),
two: makeChild('two'),
three: makeChild('three'),
}
});
app.mount('#app');