Vuelidate 2 with Vue 3 $model not updating - vue.js

Why isn't v$.FormData.name.$model being updated?
I am in the process of updating a Vue 2 application to Vue 3. However I am having troubles getting Vuelidate to function as expected. The v$.FormData.name.$model is not being updated, it only takes the initial value. However v$.FormData.$model.name contains the up to date value but doesn't have the validator stuff.
I have converted this control to a SFC with <script setup> however the child and parent are still using Options api.
I have tried passing v$.FormData.name.$model as the v-model as well.
const FormData = reactive({});
const rules = reactive(Validations); // { FormData : { name: {}...
const v$ = useVuelidate(rules, { FormData }, { $autoDirty: true, $lazy: true });
...
watch(
FormData,
(oldValue, newValue) => {
v$.value.FormData[control.vModel].$touch();
emit("event_form_updated", {
attributeName: props.control.vModel,
model: v$.value.FormData[control.vModel],
});
},
{ deep: true }
);
...
<component
:is="components[control.component]"
:control="control"
:options="control.options"
:values="control.value"
v-model="FormData[control.vModel]"
:v="v$.FormData[control.vModel]"
v-show="showCtrl(control)"
:tab-index="100 * (index + 1)"
/>
</span>

Posting the answer for anyone else who might run into the issue. It seems Vuelidate or one of it's dependencies doesn't like the vue/compat build. Once I swapped to the regular Vue 3 library the proxy value started pulling through correctly.

Related

Validate a form before submit in Vue3 composition api with vuelidate

According to documentation example there is the following Vue.js options api way to validate the whole form before submitting it
export default {
methods: {
async submitForm () {
const isFormCorrect = await this.v$.$validate()
// you can show some extra alert to the user or just leave the each field to show it's `$errors`.
if (!isFormCorrect) return
// actually submit form
}
}
}
I am using Vue.js 3 with composition api and simply can't make this work in my case.
In my <template> i have a form
<form
#submit="submitHandler"
>
<input>
:error="v$.productName.$invalid && v$.productName.$dirty"
#input="v$.productName.$touch()"
</input>
<input>
:error="v$.productPrice.$invalid && v$.productPrice.$dirty"
#update:model-value="v$.productPrice.$touch()"
</input>
...
</form>
Under <script setup> tag i have the following
import { useVuelidate } from '#vuelidate/core'
import { required, integer, minValue } from '#vuelidate/validators'
...
const state = reactive({
productName: '',
productPrice: '',
...
})
const rules = {
productName: { required, $lazy: true },
productPrice: { required, integer, minValue: minValue(1), $lazy: true },
...
$validationGroups: {
allProductData: [
'productName',
'productPrice' ,
...
]
}
}
const v$ = useVuelidate(rules, state)
...
const submitHandler = async () => {
try {
const isFormCorrect = await v$.$validate()
console.log('Submit Fired')
} catch (error) {
console.warn({error})
}
}
submitHandler() gives me an error saying error: TypeError: v$.$validate is not a function. I tried with and without making it async and got the same error.
I also tried to place the same code directly in the <form> #click event handler and it works perfectly fine.
<form
#submit="v$.validate()"
>
...
</form>
Am i missing something ? It seems to me like vuelidate2 v$.methodName() only works in the template which is strange because i recall using it exactly as documentation suggests in my Vue.js 2 applications
useVuelidate returns a ref, this is not well-documented but can be expected from a reactive composable.
Refs are automatically unwrapped in a template, in a script it's supposed to be:
const isFormCorrect = await unref(v$).$validate()

How to use highlight.js in a VueJS app with mixed content

I'm currently using highlight.js to hightlight the code in the HTML content being received from my backend. An example of something I might receive from the backend is:
<h3> Check this example of Javascript </h3>
<pre>
<code class="language-javascript">let x = 0;
function hello() {}
</code>
</pre>
As you can see it is a mixed content of HTML and code examples wrapped in pre -> code tags.
I have a component to render WYSIWYG content returned from the backend. In this component, I use highlight.js to highlight the code blocks.
import { defineComponent, h, nextTick, onMounted, ref, watch } from 'vue';
// No need to use a third-party component to highlight code
// since the `#tiptap/extension-code-block-lowlight` library has highlight as a dependency
import highlight from 'highlight.js'
import { QNoSsr } from 'quasar';
export const WYSIWYG = defineComponent({
name: 'WYSIWYG',
props: {
content: { type: String, required: true },
},
setup(props) {
const root = ref<HTMLElement>(null);
const hightlightCodes = async () => {
if (process.env.CLIENT) {
await nextTick();
root.value?.querySelectorAll('pre code').forEach((el: HTMLElement) => {
highlight.highlightElement(el as HTMLElement);
});
}
}
onMounted(hightlightCodes);
watch(() => props.content, hightlightCodes);
return function render() {
return h(QNoSsr, {
placeholder: 'Loading...',
}, () => h('div', {
class: 'WYSIWYG',
ref: root,
innerHTML: props.content
}));
};
},
});
Whenever I visit the page by clicking on a link the page works just fine, but when I hard refresh the page I get the following error:
`line` must be greater than 0 (lines start at line 1)
Currently, I'm not sure precisely why this happens, and tried a couple of different approaches
Aproach 1: try to build the whole content and then replace
const computedHtml = computed(() => {
if (import.meta.env.SSR) return '';
console.log(props.content);
const { value } = highlight.highlightAuto(props.content);
console.log(value);
return '';
})
With this approach, I get the same error as before
`line` must be greater than 0 (lines start at line 1)
I have checked out this error in https://github.com/withastro/astro/issues/3447 and https://github.com/vitejs/vite/issues/11037 but it looks like that this error is more related to Vite than my application - please, correct me if I'm wrong here.
Is there a way for me to highlight the code in the backend that is being returned from the backend in Vue?

How to generate computed props on the fly while accessing the Vue instance?

I was wondering if there is a way of creating computed props programatically, while still accessing the instance to achieve dynamic values
Something like that (this being undefined below)
<script>
export default {
computed: {
...createDynamicPropsWithTheContext(this), // helper function that returns an object
}
}
</script>
On this question, there is a solution given by Linus: https://forum.vuejs.org/t/generating-computed-properties-on-the-fly/14833/4 looking like
computed: {
...mapPropsModels(['cool', 'but', 'static'])
}
This works fine but the main issue is that it's fully static. Is there a way to access the Vue instance to reach upon props for example?
More context
For testing purposes, my helper function is as simple as
export const createDynamicPropsWithTheContext = (listToConvert) => {
return listToConvert?.reduce((acc, curr) => {
acc[curr] = curr
return acc
}, {})
}
What I actually wish to pass down to this helper function (via this) are props that are matching a specific prefix aka starting with any of those is|can|has|show (I'm using a regex), that I do have access via this.$options.props in a classic parent/child state transfer.
The final idea of my question is mainly to avoid manually writing all the props manually like ...createDynamicPropsWithTheContext(['canSubmit', 'showModal', 'isClosed']) but have them populated programatically (this pattern will be required in a lot of components).
The props are passed like this
<my-component can-submit="false" show-modal="true" />
PS: it's can-submit and not :can-submit on purpose (while still being hacked into a falsy result right now!).
It's for the ease of use for the end user that will not need to remember to prefix with :, yeah I know...a lot of difficulty just for a semi-colon that could follow Vue's conventions.
You could use the setup() hook, which receives props as its first argument. Pass the props argument to createDynamicPropsWithTheContext, and spread the result in setup()'s return (like you had done previously in the computed option):
import { createDynamicPropsWithTheContext } from './props-utils'
export default {
⋮
setup(props) {
return {
...createDynamicPropsWithTheContext(props),
}
}
}
demo
If the whole thing is for avoiding using a :, then you might want to consider using a simple object (or array of objects) as data source. You could just iterate over a list and bind the data to the components generated. In this scenario the only : used are in the objects
const comps = [{
"can-submit": false,
"show-modal": true,
"something-else": false,
},
{
"can-submit": true,
"show-modal": true,
"something-else": false,
},
{
"can-submit": false,
"show-modal": true,
"something-else": true,
},
]
const CustomComponent = {
setup(props, { attrs }) {
return {
attrs
}
},
template: `
<div
v-bind="attrs"
>{{ attrs }}</div>
`
}
const vm = Vue.createApp({
setup() {
return {
comps
}
},
template: `
<custom-component
v-for="(item, i) in comps"
v-bind="item"
></custom-component>
`
})
vm.component('CustomComponent', CustomComponent)
vm.mount('#app')
<script src="https://unpkg.com/vue#3"></script>
<div id="app">{{ message }}</div>
Thanks to Vue's Discord Cathrine and skirtle folks, I achieved to get it working!
Here is the thread and here is the SFC example that helped me, especially this code
created () {
const magicIsShown = computed(() => this.isShown === true || this.isShown === 'true')
Object.defineProperty(this, 'magicIsShown', {
get () {
return magicIsShown.value
}
})
}
Using Object.defineProperty(this... is helping keeping the whole state reactive and the computed(() => can reference some other prop (which I am looking at in my case).
Using a JS object could be doable but I have to have it done from the template (it's a lower barrier to entry).
Still, here is the solution I came up with as a global mixin imported in every component.
// helper functions
const proceedIfStringlean = (propName) => /^(is|can|has|show)+.*/.test(propName)
const stringleanCase = (string) => 'stringlean' + string[0].toUpperCase() + string.slice(1)
const computeStringlean = (value) => {
if (typeof value == 'string') {
return value == 'true'
}
return value
}
// the actual mixin
const generateStringleans = {
created() {
for (const [key, _value] of Object.entries(this.$props)) {
if (proceedIfStringlean(key)) {
const stringleanComputed = computed(() => this[key])
Object.defineProperty(this, stringleanCase(key), {
get() {
return computeStringlean(stringleanComputed.value)
},
// do not write any `set()` here because this is just an overlay
})
}
}
},
}
This will scan every .vue component, get the passed props and if those are prefixed with either is|can|has|show, will create a duplicated counter-part with a prefix of stringlean + pass the initial prop into a method (computeStringlean in my case).
Works great, there is no devtools support as expected since we're wiring it directly in vanilla JS.

Vue.extend and $mount() alternative for a Vue3 repository

I am implementing Micro-Frontend Architecture in my Vue2 repo, where I made various web components and injected them in the parent repo named admin_portal.
After upgrading this repository from Vue2 to Vue3, I am stuck in a method where I use Vue.extend and $mount().
Attaching a method where I am facing this issue:
renderService(node) {
const query = Object.keys(this.$route.query).length ?
this.$route.query :
"";
const attrsString = `:token="accessToken" ${
query ? ':search-params="params"' : ""
}`;
const ServiceClass = Vue.extend({
template: `<${this.service.tagname} ${attrsString}></${this.service.tagname}>`,
props: ["service", "params"]
});
const instance = new ServiceClass({
propsData: {
service: this.service,
params: JSON.stringify(query)
},
name: this.service.tagname,
store: this.$store,
computed: {
accessToken() {
return this.$store.state.auth.user.token;
}
}
});
instance.$mount();
if (node) {
this.$refs.serviceContainer.replaceChild(instance.$el, node);
} else {
this.$refs.serviceContainer.appendChild(instance.$el);
}
this.instance = instance.$el;
},
Vue.extend & $mount() are deprecated in Vue3.
Attaching a documentation URL of Vue2 where the above code is still working.
After my repo was upgraded from Vue2 to Vue3, I again want to implement the same kind of thing.

Parametized getter in Vuex - trigger udpate

My Vuex store has a collection of data, say 1,000 records. There is a getter with a parameter, getItem, taking an ID and returning the correct record.
I need components accessing that getter to know when the data is ready (when the asynchronous fetching of all the records is done).
However since it's a parametized getter, Vue isn't watching the state it depends on to know when to update it. What should I do?
I keep wanting to revert to a BehaviorSubject pattern I used in Angular a lot, but Vuex + rxJS seems heavy for this, right?
I feel I need to somehow emit a trigger for the getter to recalculate.
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import VueResource from 'vue-resource'
Vue.use(VueResource);
Vue.use(Vuex)
export default new Vuex.Store({
state: {
numberOfPosts : -1,
posts : {}, //dictionary keyed on slug
postsLoaded : false,
},
getters : {
postsLoaded : function(state){
return state.postsLoaded;
},
totalPosts : function(state){
return state.numberOfPosts;
},
post: function( state ){
return function(slug){
if( state.posts.hasOwnProperty( slug ) ){
return state.posts.slug;
}else{
return null;
}
}
}
},
mutations: {
storePosts : function(state, payload){
state.numberOfPosts = payload.length;
for( var post of payload ){
state.posts[ post.slug ] = post;
}
state.postsLoaded = true;
}
},
actions: {
fetchPosts(context) {
return new Promise((resolve) => {
Vue.http.get(' {url redacted} ').then((response) => {
context.commit('storePosts', response.body);
resolve();
});
});
}
}
})
Post.vue
<template>
<div class="post">
<h1>This is a post page for {{ $route.params.slug }}</h1>
<div v-if="!postsLoaded">LOADING</div>
<div v-if="postNotFound">No matching post was found.</div>
<div v-if="postsLoaded && !postNotFound" class="post-area">
{{ this.postData.title.rendered }}
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'post',
data : function(){
return {
loading : true,
postNotFound : false,
postData : null
}
},
mounted : function(){
this.postData = this.post( this.$route.params.slug );
if ( this.postData == null ){
this.postNotFound = true;
}
},
computed : mapGetters([
'postsLoaded',
'post'
])
}
</script>
As it stands, it shows the "post not found" message because when it accesses the getter, the data isn't ready yet. If a post isn't found, I need to distinguish between (a) the data is loaded and there isn't a post that matches, and (b) the data isn't loaded so wait
I suspect the problem lies with how your are setting the posts array in your storePosts mutation, specifially this line:
state.posts[ post.slug ] = post
VueJs can't track that operation so has no way of knowing that the array has updated, thus your getter is not updated.
Instead your need to use Vue set like this:
Vue.set(state.posts, post.slug, post)
For more info see Change Detection Caveats documentation
code sample of mark's answer
computed: {
...mapGetters([
'customerData',
])
},
methods: {
...mapActions(['customerGetRecords']),
},
created() {
this.customerGetRecords({
url: this.currentData
});
Sorry I can't use code to illustrate my idea as there isn't a running code snippet for now. I think what you need to do is that:
Access the vuex store using mapGetters in computed property, which you already did in Post.vue.
Watch the mapped getters property inside your component, in your case there would be a watcher function about postsLanded or post, whatever you care about its value changes. You may need deep or immediate property as well, check API.
Trigger mutations to the vuex store through actions, and thus would change the store's value which your getters will get.
After the watched property value changes, the corresponding watch function would be fired with old and new value and you can complete your logic there.