Vue JS - variable from javascript module is not reactive - javascript

I created a small module as a validator inspired from vee-validate and I would like to use this in conjunction with the composition api.
I have a list of errorMessages that are stored in a reactive array, however when I retrieve this variable in my vue component, despite the error messages being stored in the array accordingly, the variable is not updating in the vue template.
Iā€™m not very savvy with this so I might not be concise with my explanation. The refs in the module seem to be working properly.
Can someone kindly indicate what I might be doing wrong? I'm completely stuck and I don't know how else I can proceed.
Validator.js (Npm module - located in node_modules)
import Vue from 'vue';
import VueCompositionAPI from '#vue/composition-api'
import {ref} from '#vue/composition-api'
Vue.use(VueCompositionAPI)
class Validator {
ā€¦.
register({fieldName, rules, type}) {
if (!fieldName || rules === null || rules === undefined) {
console.error('Please pass in fieldName and rules');
return false;
}
let errorMessages = ref([]);
// define callback for pub-sub
const callback = ({id, messages}) => {
if (fieldId === id) {
errorMessages.value = Object.assign([], messages);
console.log(errorMessages.value); // this contains the value of the error messages.
}
};
return {
errorMessages,
};
}
ā€¦ā€¦
InputField.vue
<template>
<div :style="{'width': fieldWidth}" class="form-group">
<label :for="fieldName">
<input
ref="inputField"
:type="type"
:id="fieldName"
:name="fieldName"
:class="[{'field-error': apiError || errorMessages.length > 0}, {'read-only-input': isReadOnly}]"
#input="handleInput"
v-model="input"
class="form-input"/>
</label>
<div>
<p class="text-error">{{errorMessages}}</p> // Error messages not displaying
</div>
</div>
</template>
<script>
import {ref, watch} from '#vue/composition-api';
import Validator from "validator";
export default {
props: {
fieldTitle: {
required: true
},
fieldName: {
required: true
},
type: {
required: true
},
rules: {
default: 'required'
}
},
setup(props) {
// The error messages are returned in the component but they are not reactive. Therefore they only appear after its re-rendered.
const {errorMessages, handleInput, setFieldData} = Validator.register(props);
return {
errorMessages,
handleInput,
}
}
}
</script>

You should be using Vue.Set(), it directly triggers related values to be updated

The problem is Validator.register() directly destructures props, which removes the reactivity from the resulting values.
Solution
Use toRefs(props) to create an object of refs for each prop, and pass that to Validator.register():
import { toRefs } from 'vue'
šŸ‘‡
Validator.register(toRefs(props))
Then update Validator.register() to unwrap the refs where needed:
class Validator {
register({ fieldName, rules, type }) {
šŸ‘‡ šŸ‘‡ šŸ‘‡
if (!fieldName.value || rules.value === null || rules.value === undefined) {
console.error('Please pass in fieldName and rules');
return false;
}
ā‹®
}
}

Related

Vue/JS - How to use ref from the composition api in an external Javascript module?

I'm building a javascript module (npm package) that serves as a validator for vue projects inspired from vee-validate. In the validator I have a few variables that I would need to reference in a vue component by destructuring and I need them to be reactive, therefore in the javascript module I import ref from the composition api and apply it on my variable.
Validator.js
import {ref} from '#vue/composition-api'
class Validator {
ā€¦.
register({fieldName, rules, type}) {
if (!fieldName || rules === null || rules === undefined) {
console.error('Please pass in fieldName and rules');
return false;
}
let errorMessages = ref([]); // Ref is used on this variable which gets populated with error messages (if any)
const callback = ({id, messages}) => {
if (fieldId === id) {
errorMessages.value = Object.assign([], messages);
console.log(errorMessages.value); // this contains the value of error messages. As you can see I still need to reference it using .value as one would on refs.
}
};
return {
errorMessages,
};
}
ā€¦ā€¦
In my vue project I install the validator via NPM, and its core functionality works as it should. However, my issue is when I try to display the error messages on the template. Despite the errorMessages array getting the data it should, the variable is not reactive and therefore it does not update on the template. It feels like the reactivity is breaking somewhere, or it could be a scoping issue with I'm not too savvy with.
InputField.vue
<template>
<div :style="{'width': fieldWidth}" class="form-group">
<label :for="fieldName">
<input
ref="inputField"
:type="type"
:id="fieldName"
:name="fieldName"
:class="[{'field-error': apiError || errorMessages.length > 0}
#input="handleInput"
v-model="input"
class="form-input"/>
</label>
<div>
<p class="text-error">{{errorMessages}}</p> // Error messages not displaying
</div>
</div>
</template>
<script>
import {ref, watch} from '#vue/composition-api';
import Validator from "validator";
export default {
props: {
fieldTitle: {
required: true
},
fieldName: {
required: true
},
type: {
required: true
},
rules: {
default: 'required'
}
},
setup(props) {
// The error messages are returned in the component but they are not reactive. Therefore they only appear after its re-rendered.
const {errorMessages, handleInput, setFieldData} = Validator.register(props);
return {
errorMessages,
handleInput,
}
}
}
</script>
To build this package I'm not using a bundler as I simply need to export the classes/functions in order to publish to npm and install it in another project.
Can someone kindly suggest what the issue could be here?

Vue 3, call $emit on variable change

So I'm trying to build a component in Vue 3 that acts as a form, and in order for the data to be processed by the parent I want it to emit an object with all the inputs on change. The issue I'm having is that I don't seem to be able to call $emit from within watch() (probably because of the context, but I also don't see why the component-wide context isn't passed by default, and it doesn't accept this). I also cannot call any method because of the same reason.
I do see some people using the watch: {} syntax but as I understand it that is deprecated and it doesn't make a whole lot of sense to me either.
Here's a minimal example of what I'm trying to accomplish. Whenever the input date is changed, I want the component to emit a custom event.
<template>
<input
v-model="date"
name="dateInput"
type="date"
>
</template>
<script>
import { watch, ref } from "vue";
export default {
name: 'Demo',
props: {
},
emits: ["date-updated"],
setup() {
const date = ref("")
watch([date], (newVal) => {
// $emit is undefined
console.log(newVal);
$emit("date-updated", {newVal})
// watchHandler is undefined
watchHandler(newVal)
})
return {
date
}
},
data() {
return {
}
},
mounted() {
},
methods: {
watchHandler(newVal) {
console.log(newVal);
$emit("date-updated", {newVal})
}
},
}
</script>
Don't mix between option and composition api in order to keep the component consistent, the emit function is available in the context parameter of the setup hook::
<template>
<input
v-model="date"
name="dateInput"
type="date"
>
</template>
<script>
import { watch, ref } from "vue";
export default {
name: 'Demo',
props: {},
emits: ["date-updated"],
setup(props,context) {// or setup(props,{emit}) then use emit directly
const date = ref("")
watch(date, (newVal) => {
context.emit("date-updated", {newVal})
})
return {
date
}
},
}
</script>
if you want to add the method watchHandler you could define it a plain js function like :
...
watch(date, (newVal) => {
context.emit("date-updated", {newVal})
})
function watchHandler(newVal) {
console.log(newVal);
context.emit("date-updated", {newVal})
}
...

Vue 3.0 How to assign a prop to a ref without changing the prop

I'm sending from the parent component a prop: user. Now in the child component I want to make a copy of it without it changing the prop's value.
I tried doing it like this:
export default defineComponent({
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const user = ref(props.apiUser);
return { user };
}
});
But then if I change a value of the user object it also changes the apiUser prop. I thought maybe using Object.assign would work but then the ref isn't reactive anymore.
In Vue 2.0 I would do it like this:
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
return {
user: {}
}
},
mounted() {
this.user = this.apiUser;
// Now I can use this.user without changing this.apiUser's value.
}
};
Credits to #butttons for the comment that lead to the answer.
const user = reactive({ ...props.apiUser });
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const userCopy = toRef(props, 'apiUser')
}
With the composition API we have the toRef API that allows you to create a copy from any source reactive object. Since the props object is a reactive, you use toRef() and it won't mutate your prop.
This is what you looking for: https://vuejs.org/guide/components/props.html#one-way-data-flow
Create data where you add the prop to
export default {
props: ['apiUser'],
data() {
return {
// user only uses this.apiUser as the initial value;
// it is disconnected from future prop updates.
user: this.apiUser
}
}
}
Or if you use api composition:
import {ref} from "vue";
const props = defineProps(['apiUser']);
const user = ref(props.apiUser);
You also may want to consider using computed methods (see also linked doc section from above) or v-model.
Please note that the marked solution https://stackoverflow.com/a/67820271/2311074 is not working. If you try to update user you will see a readonly error on the console. If you don't need to modify user, you may just use the prop in the first place.
As discussed in comment section, a Vue 2 method that I'm personally fond of in these cases is the following, it will basically make a roundtrip when updating a model.
Parent (apiUser) ->
Child (clone apiUser to user, make changes, emit) ->
Parent (Set changes reactively) ->
Child (Automatically receives changes, and creates new clone)
Parent
<template>
<div class="parent-root"
<child :apiUser="apiUser" #setUserData="setUserData" />
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of child component etc.)
export default {
data() {
apiUser: {
id: 'e134',
age: 27
}
},
methods: {
setUserData(payload) {
this.$set(this.apiUser, 'age', payload);
}
}
}
Child
<template>
<div class="child-root"
{{ apiUser }}
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of components etc.)
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
user: null
},
watch: {
apiUser: {
deep: true,
handler() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
}
}
},
mounted() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
},
methods: {
// Whatever function catching the changes you want to do
setUserData(payload) {
this.$emit('setUserData', this.user);
}
}
}
Apologies for any miss types

using pascal case for vue component fails to register while built as library

When use PascalCase for the component name the component is not registered by vue. Below is how it looks like
<template>
<div>
<h6>This is a sample reusable component</h6>
</div>
</template>
<script>
export default {
name: "SampleComponent",
data() {
return {
};
}
}
</script>
This is how i am registering components.
import SampleComponent from './components/SampleComponent'
const components:any = {SampleComponent}
const ComponentLibrary = {
install(Vue:any, options = {}) {
for (const componentName in components) {
const component = components[componentName];
Vue.component(component.name, component);
}
}
}
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(ComponentLibrary)
}
export default ComponentLibrary;
While importing the library i get the below error
Unknown custom element: <samplecomponent> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
(found in <Root>)
If i use just 'Sample' then it works. Not sure how to fix this.
Referencing the component name with kebab case in DOM has fixed the isssue. In any other vue project Pascal case worked.

how to access "this" in props validator

I'm working on a project using nuxt.js, I'm injecting a function in the context of the application as recommended in the official documentation
https://nuxtjs.org/guide/plugins/#inject-in-root-amp-context
but when I try to call the function inside a props validation I get an error
/plugins/check-props.js
import Vue from 'vue'
Vue.prototype.$checkProps = function(value, arr) {
return arr.indexOf(value) !== -1
}
in a component vue
export default {
props: {
color: {
type: String,
validator: function (value, context) {
this.$checkProps(value, ['success', 'danger'])
}
}
}
ERROR: Cannot read property '$checkProps' of undefined
Does anyone know how I can access "this" within validation?
thanks in advance!
Props validation is done before the component is initialized, so you won't have access to this as you are extending Vue.prototype.
Form their documentation:
Note that props are validated before a component instance is created, so instance properties (e.g. data, computed, etc) will not be available inside default or validator functions.
In general, if $checkProps is only used for checking the value of these props, I would just use a helper function.
// array.helpers.js
export function containsValue(arr, val) {
return arr.indexOf(value) !== -1
}
// component
import { containsValue } from 'path/to/helpers/array.helpers';
props: {
foo: {
//
validator(value) {
return containsValue(['foo', 'bar'], value);
}
}
}
Update
Based on your comments, if you don't want to import this specific function over and over again, you can just Array.prototype.includes see docs
// component
props: {
color: {
//
validator(value) {
return ['success', 'danger'].includes(value);
}
}
}
From the doc:
props are validated before a component instance is created, so
instance properties (e.g. data, computed, etc) will not be available
inside default or validator functions
If you want to access the nuxt plugins you can always use the window object.This is how I do it to access the i18n library
{
validator: (value: any): boolean => {
return window.$nuxt.$te(value);
}
}
In your case:
{
validator: (value: any): boolean => {
return window.$nuxt.$checkProps(value, ['success', 'danger']);
}
}
In any case you should never prototype in the window object. Let nuxt handle it with a plugin:
path: plugins/check-props.js or .ts
function checkProps(value: any, arr: string[]): boolean {
return arr.indexOf(value) !== -1
}
const $checkProps: Plugin = (_context: Context, inject: Inject) => {
inject('checkProps', checkProps);
};
export default $checkProps;
And then in nuxt config
{
plugins: [{ src: 'plugins/check-props.js', ssr: true }]
}

Categories