Extract modelValue logic to composable - vue.js

I'm transitioning from Vue 2 to Vue 3 and I'm having trouble with composables.
I have a bunch of components that inherits modelValue. So, for every component that uses modelValue I'm writing this code (example with a radio input component):
<script setup>
import { computed } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: [String, null],
required: true
}
})
const computedValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
</script>
<template>
<label class="radio">
<input
v-model="computedValue"
v-bind="$attrs"
type="radio"
>
<slot />
</label>
</template>
Is there a way to reuse the code for the modelValue?

I've just done this while I'm playing with Nuxt v3.
You can create a composable like this:
import { computed } from 'vue'
export function useModel(props, emit) {
return computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
}
<template>
<input type="text" v-model="value" />
</template>
<script setup lang="ts">
const props = defineProps({
modelValue: String,
})
const emit = defineEmits(['update:modelValue'])
const value = useModel(props, emit)
</script>

For completion of #BghinC's perfect answer here the fully typed version:
Composable
File: #/composables/useModelValue.ts
import {computed} from 'vue'
export default function useModelValue<T>(
props: {
modelValue: T
[key: string]: unknown
},
emit: (event: 'update:modelValue', ...args: unknown[]) => void
) {
return computed({
get: () => props.modelValue,
set: (value: T) => emit('update:modelValue', value),
})
}
Usage
<script setup lang="ts">
import useModelValue from '#/composables/useModelValue'
const props = defineProps<{
modelValue: Dog
}>()
const emit = defineEmits(['update:modelValue'])
const dog = useModelValue<Dog>(props, emit)
</script>

Related

Vue 3 - use props string to load component

I have following component:
<script setup>
import {computed, onMounted, ref, watch} from "vue";
import {useDialogStore} from "#/store/dialog";
import TableSwitcher from "#/components/Dialogs/Components/TableSwitcher.vue"
let emit = defineEmits(['confirmDialogConfirmed', 'confirmDialogClose'])
let dialogStore = useDialogStore()
let question = computed(() => dialogStore.dialogQuestion)
let mainComponent = ref('')
let props = defineProps({
open: {
type: Boolean,
required: true
},
id: {
type: String,
default: 'main-dialog'
},
component: {
type: String,
required: true,
}
})
watch(props, (newValue, oldValue) => {
mainComponent.value = props.component
console.log(mainComponent);
if(newValue.open === true)
{
dialog.showModal()
}
},
{
deep:true
}
);
let dialog = ref();
let closeDialog = (confirmAction = false) =>
{
dialog.close()
dialogStore.close(confirmAction)
}
onMounted(() => {
dialog = document.getElementById(props.id);
});
</script>
<template>
<dialog :id="id">
<component :is="mainComponent" ></component>
</dialog>
</template>
For activating component I am using this:
<main-dialog
v-if="component"
:component="component"
:open="true">
</main-dialog>
component value is created on click and passed as a prop to the main component. When I click to activate this component I am getting following error:
Invalid vnode type when creating vnode
When I hard code the component name for the mainComponent var the component is loaded correctly. What am I doing wrong here?
There are different ways to solve that. I think in your case it would make sense to use slots. But if you want to keep your approach you can globally define your components in your Vue app.
without slots
const app = createApp({});
// define components globally as async components:
app.component('first-component', defineAsyncComponent(async () => import('path/to/your/FirstComponent.vue'));
app.component('second-component', defineAsyncComponent(async () => import('path/to/your/SecondComponent.vue'));
app.mount('#app');
Then you can use strings and fix some bugs in your component:
Don’t use ids, instead use a template ref to access the dialog element
Use const instead of let for non-changing values.
props are already reactive so you can use the props also directly inside your template and they will be updated automatically when changed from the outside.
// inside <script setup>
import {computed, onMounted, ref, watch} from "vue";
import {useDialogStore} from "#/store/dialog";
import TableSwitcher from "#/components/Dialogs/Components/TableSwitcher.vue"
// use const instead of let, as the values are not changing:
const emit = defineEmits(['confirmDialogConfirmed', 'confirmDialogClose'])
const props = defineProps({
open: {
type: Boolean,
required: true
},
id: {
type: String,
default: 'main-dialog'
},
component: {
type: String,
required: true,
}
});
const dialogStore = useDialogStore()
const question = computed(() => dialogStore.dialogQuestion);
const dialog = ref(null);
watchPostEffect(
() => {
if(props.open) {
dialog.value?.showModal()
}
},
// call watcher also on first lifecycle:
{ immediate: true }
);
let closeDialog = (confirmAction = false) => {
dialog.value?.close()
dialogStore.close(confirmAction)
}
<!-- the sfc template -->
<dialog ref="dialog">
<component :is="props.component" />
</dialog>
with slots
<!-- use your main-dialog -->
<main-dialog :open="open">
<first-component v-if="condition"/>
<second-component v-else />
</main-dialog>
<!-- template of MainDialog.vue -->
<dialog ref="dialog">
<slot />
</dialog>

Is there a way to share reactive data between random components in Vue 3 Composition API?

Having some reactive const in "Component A," which may update after some user action, how could this data be imported into another component?
For example:
const MyComponent = {
import { computed, ref } from "vue";
setup() {
name: "Component A",
setup() {
const foo = ref(null);
const updateFoo = computed(() => foo.value = "bar");
return { foo }
}
}
}
Could the updated value of 'foo' be used in another Component without using provide/inject?
I am pretty new in the Vue ecosystem; kind apologies if this is something obvious that I am missing here.
One of the best things about composition API is that we can create reusable logic and use that all across the App. You create a composable functions in which you can create the logic and then import that into the components where you want to use it. Not only does this make your component much cleaner but also your APP much more maintainable. Below is a simple example of counter to show how they can be used. You can find working demo here:
Create a composable function for counter:
import { ref, computed } from "vue";
const counter = ref(0);
export const getCounter = () => {
const incrementCounter = () => counter.value++;
const decrementCounter = () => counter.value--;
const counterPositiveOrNegitive = computed(() =>
counter.value >= 0 ? " Positive" : "Negitive"
);
return {
counter,
incrementCounter,
decrementCounter,
counterPositiveOrNegitive
};
};
Then you can import this function into your components and get the function or you want to use. Component to increment counter.
<template>
<div class="hello">
<h1>Component To Increment Counter</h1>
<button #click="incrementCounter">Increment</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "IncrementCounter",
setup() {
const { incrementCounter } = getCounter();
return { incrementCounter };
},
};
</script>
Component to decrement counter:
<template>
<div class="hello">
<h1>Component To Decrement Counter</h1>
<button #click="decrementCounter">Decrement</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "DecrementCounter",
setup() {
const { decrementCounter } = getCounter();
return { decrementCounter };
},
};
</script>
Then in the main component, you can show the counter value.
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<div class="counters">
<IncrementCounter />
<DecrementCounter />
</div>
<h3>Main Component </h3>
<p>Counter: {{ counter }}</p>
<p>{{ counterPositiveOrNegitive }}</p>
</template>
<script>
import IncrementCounter from "./components/IncrementCounter.vue";
import DecrementCounter from "./components/DecrementCounter.vue";
import { getCounter } from "./composables/counterExample";
export default {
name: "App",
components: {
IncrementCounter: IncrementCounter,
DecrementCounter: DecrementCounter,
},
setup() {
const { counter, counterPositiveOrNegitive } = getCounter();
return { counter, counterPositiveOrNegitive };
},
};
Hope this was somewhat helpful. You can find a working example here:
https://codesandbox.io/s/vue3-composition-api-blfpj

Vue 3 - template ref with computed?

How may I focus on this simple input example?
Should I create one more variable const nameRef = ref(null) or there is more beauty way to resolve this?
<template>
<input ref="name" :value="name" />
</template>
<script>
import {ref, computed} from 'vue';
export default {
props: ['name'],
setup(props) {
const name = computed(() => someTextPrepare(props.name));
// how can I do name.value.focus() for example?
return { name }
}
}
</script>
Try to wrap the name value and ref in reactive property :
<template>
<input :ref="theName.ref" :value="theName.value" />
</template>
<script>
import {ref, computed,reactive} from 'vue';
export default {
props: ['name'],
setup(props) {
const theName=reactive({
value:computed(() => someTextPrepare(props.name)),
ref: ref(null)
})
return { theName }
}
}
</script>

How to use v-model in Vue with Vuex and composition API?

<template>
<div>
<h1>{{ counter }}</h1>
<input type="text" v-model="counter" />
</div>
</template>
<script>
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
const counter = computed(() => store.state.counter)
return { counter }
},
}
</script>
How to change value of counter in the store when input value changes
I am getting this error in the console:
[ operation failed: computed value is readonly ]
Try this:
...
const counter = computed({
get: () => store.state.counter,
set: (val) => store.commit('COUNTER_MUTATION', val)
})
...
https://v3.vuejs.org/api/computed-watch-api.html#computed
Try this:
<input v-model="counter">
computed: {
counter: {
get () {
return this.$store.state.a
},
set (value) {
this.$store.commit('updateA', value)
}
}
}
With composition API
When creating computed properties we can have two types, the readonly and a writable one.
To allow v-model to update a variable value we need a writable computed ref.
Example of a readonly computed ref:
const
n = ref(0),
count = computed(() => n.value);
console.log(count.value) // 0
count.value = 2 // error
Example of a writable computed ref:
const n = ref(0)
const count = computed({
get: () => n.value,
set: (val) => n.value = val
})
count.value = 2
console.log(count.value) // 2
So.. in summary, to use v-model with Vuex we need to use a writable computed ref. With composition API it would look like below:
note: I changed the counter variable with about so that the code makes more sense
<script setup>
import {computed} from 'vue'
import {useStore} from 'vuex'
const store = useStore()
const about = computed({
get: () => store.state.about,
set: (text) => store.dispatch('setAbout', text)
})
</script>
<template>
<div>
<h1>{{ about }}</h1>
<input type="text" v-model="about" />
</div>
</template>

Can you render VNodes in a Vue template?

I have an situation where I have a render function that passes some data to a scoped slot. As part of this data I'd like to include some VNodes constructed by the render function that could optionally be used by the scoped slot. Is there anyway when writing the scoped slot in a template to output the raw VNodes that were received?
Vue 3
You can use <component> in the template to render the vnode:
<SomeComponent v-slot="{ vnode }">
<div>
<component :is="vnode"/>
</div>
</SomeComponent>
This only seems to work for a single vnode. If you want to render multiple vnodes (an array) then use v-for or render it as a functional component:
<SomeComponent v-slot="{ vnodes }">
<div>
<component :is="() => vnodes"/>
</div>
</SomeComponent>
or ensure the vnode is a single <Fragment> containing the child vnodes.
You can also use a similar approach to the Vue 2 way below.
Vue 2
You can use a functional component to render the vnodes for that section of your template:
<SomeComponent v-slot="{ vnodes }">
<div>
<VNodes :vnodes="vnodes"/>
</div>
</SomeComponent>
components: {
VNodes: {
functional: true,
render: (h, ctx) => ctx.props.vnodes
}
}
Vue 3 example
<some-component>
<vnodes :vnodes="vnodes"/>
</some-component>
components: {
vNodes: {
props: ['vnodes'],
render: (h) => h.vnodes,
},
},
In vue3 with typescript you'd do this:
Define a wrapper functional component.
This is necessary because vue doesn't allow you to mix and match function and template functions.
const VNodes = (props: any) => {
return props.node;
};
VNodes.props = {
node: {
required: true,
},
};
export default VNodes;
Use the functional component in a template component
<script lang="ts" setup>
import { defineProps } from "vue";
import type { VNode } from "vue";
import VNodes from "./VNodes"; // <-- this is not a magic type, its the component from (1)
const props = defineProps<{ comment: string; render: () => VNode }>();
</script>
<template>
<div>{{ props.comment }} <v-nodes :node="props.render()" /></div>
</template>
Pass the VNode to the component
import type { Story } from "#storybook/vue3";
import { h } from "vue";
import type { VNode } from "vue";
import ExampleComponent from "./ExampleComponent.vue";
export default {
title: "ExampleComponent",
component: ExampleComponent,
parameters: {},
};
interface StoryProps {
render: () => VNode;
comment: string;
}
const Template: Story<StoryProps> = (props) => ({
components: { ExampleComponent },
setup() {
return {
props,
};
},
template: `<example-component :render="props.render" :comment="props.comment"/>`,
});
export const Default = Template.bind({});
Default.args = {
comment: "hello",
render: () => h("div", {}, ["world"]),
};
export const ChildComponent = Template.bind({});
ChildComponent.args = {
comment: "outer",
render: () =>
h(
ExampleComponent,
{
comment: "inner",
render: () => h("div", {}, ["nested component"]),
},
[]
),
};
Long story short:
The above is a work around by rendering vnodes in a functional component (no template), and rendering the functional component in a template.
So, no. You cannot render VNodes from templates.
This is functionality that vue doesn't have, compared to other component systems.