Is there a way to suppress [Vue-warn] in prop validation? - vue.js

I need to display custom validator message on prop. Along with custom error I also see default Vue warning [Vue-warn]. Is there away to suppress [Vue-warn]:
My prop look like this:
props: {
mode: String,
default: h,
validator: val => {
if(['s','y','z'].includes(v)) {
return true
}
else {
console.error("Possible values for mode are: 's', 'y' or 'z'");
return false
}
}
}

The warning only appears because the validator returns false.
To avoid the warning message in the console, return true instead:
props: {
mode: String,
default: h,
validator: val => {
πŸ‘‡
if(!['s','y','z'].includes(v)) {
console.error("Possible values for mode are: 's', 'y' or 'z'");
} πŸ‘‡
return true
}
}

Related

Why i am getting a warning about of string type if i am passing a string?

i want to pass a String to my child component like this, but previously i want to print it
this is my parent component
{{left.leftA}} // here show me 8
<CustomCard
:left="left.leftA"
export default {
name: 'Parent',
data() {},
setup() {
onMounted(async () => {
const answer = await getData(name)
left.value = answer.response //{leftA:'A', leftB:'B'...}
})
and in my child component i have this declaration
export default {
name: 'CustomCard',
props: {
left: {
type: String,
required: true,
},
i am getting this warning:
[Vue warn]: Invalid prop: type check failed for prop "left". Expected String with
value "undefined", got Undefined
Does it have something to do with how I am loading the data? is it ok to use onMounted?
This is happening because the initial value for value is null. So, on initial render it throws the warning, but upon another render it has the correct prop type (a string) and renders correctly.
You have 3 options. Allow '' as an option on the prop or don’t render the component until you have the correct data or make use of computed Property.
Option-1
{{left.leftA}} // here show me 8
<CustomCard
:left="left.leftA ? left.leftA : ''"
Option-2
{{left.leftA}} // here show me 8
<CustomCard v-if="loaded"
:left="left.leftA"
and in onMounted(}
onMounted(async () => {
const answer = await getData(name)
left.value = answer.response //{leftA:'A', leftB:'B'...}
// Set the loaded flag as true here. Also make sure its set as false inside the setup()
})
Option-3
{{left.leftA}} // here show me 8
<CustomCard
:left="sendVal"
In computed....
computed: {
sendVal() {
if(left && left.left1) return left.left1;
return '';
}
}

Prevent Vue Multiple Select to Store an Empty Array

I want this select multiple to pre-select one option, and not be able to deselect all options.
Whenever the last selected option is deselected it should be reselected. In other words when the user tries to deselect the last selected option it should visually not be deselected.
<template>
<b-select
if="Object.keys(doc).length !== 0 /* wait until firebase has loaded */"
:options="computedOptions"
v-model="model"
multiple
#input="onChange"
/>
</template>
<script>
//import Vue from 'vue'
import { fb } from "../fbconf";
export default {
name: "MyMultiSelect",
props: {
doc: Object, // firestore document
},
data() {
return {
options: []
};
},
firestore() {
var options = fb.db.collection("options");
return {
options: options
};
},
computed: {
computedOptions: function() {
return this.options.map(function(option) {
return {
text: option.name,
value: option.id
};
});
},
// to make sure mySelectedOptions is an array, before this.doc is loaded
// I use the following custom model
// because not using 'get' below causes a warning:
// [Vue warn]: <select multiple v-model="localValue"> expects an Array value for its binding, but got Undefined
model: {
get: function() {
if (!this.doc.hasOwnProperty('mySelectedOptions')) return []; // empty array before this.doc is loaded
else return this.doc['mySelectedOptions'];
},
set: function(newValue) {
// here I can prevent the empty array from being stored
// but visually the user can deselect all options, which is bad UX
//if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
}
},
},
methods: {
onChange: function(newValue){
// I can manually store the array as I want here
// but I cannot in any way prevent the user from deselecting all options
if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
else {
// none of these reselects the last selected option
var oldValue = this.doc['mySelectedOptions'];
this.doc['mySelectedOptions'] = this.doc['mySelectedOptions'];
//this.$forceUpdate();
//this.$emit("change", newValue);
//Vue.set(this.doc, 'mySelectedOptions', this.doc['mySelectedOptions']);
}
}
}
};
</script>
You could add watcher and when length becomes 0 just add previous value.
watch: {
model(val, oldVal) {
if(val.length == 0 && oldVal.length > 0) {
// take only one item in case there's clear button or etc.
this.model = [oldval[0]];
}
}
}

Binding an object from checkboxes

I need to bind an object from checkboxes, and in this example, a checkbox is its own component:
<input type="checkbox" :value="option.id" v-model="computedChecked">
Here's my data and computed:
data() {
return {
id: 1,
title: 'test title',
checked: {
'users': {
},
},
}
},
computed: {
computedChecked: {
get () {
return this.checked['users'][what here ??];
},
set (value) {
this.checked['users'][value] = {
'id': this.id,
'title': this.title,
}
}
},
....
The above example is a little rough, but it should show you the idea of what I am trying to achieve:
Check checkbox, assign an object to its binding.
Uncheck and binding is gone.
I can't seem to get the binding to worth though.
I assume you want computedChecked to act like an Array, because if it is a Boolean set, it will receive true / false on check / uncheck of the checkbox, and it should be easy to handle the change.
When v-model of a checkbox input is an array, Vue.js expects the array values to stay in sync with the checked status, and on check / uncheck it will assign a fresh array copy of the current checked values, iff:
The current model array contains the target value, and it's unchecked in the event
The current model array does not contain the target value, and it's checked in the event
So in order for your example to work, you need to set up your setter so that every time the check status changes, we can get the latest state from the getter.
Here's a reference implementation:
export default {
name: 'CheckBoxExample',
data () {
return {
id: 1,
title: 'test title',
checked: {
users: {}
}
}
},
computed: {
computedChecked: {
get () {
return Object.getOwnPropertyNames(this.checked.users).filter(p => !/^__/.test(p))
},
set (value) {
let current = Object.getOwnPropertyNames(this.checked.users).filter(p => !/^__/.test(p))
// calculate the difference
let toAdd = []
let toRemove = []
for (let name of value) {
if (current.indexOf(name) < 0) {
toAdd.push(name)
}
}
for (let name of current) {
if (value.indexOf(name) < 0) {
toRemove.push(name)
}
}
for (let name of toRemove) {
var obj = Object.assign({}, this.checked.users)
delete obj[name]
// we need to update users otherwise the getter won't react on the change
this.checked.users = obj
}
for (let name of toAdd) {
// update the users so that getter will react on the change
this.checked.users = Object.assign({}, this.checked.users, {
[name]: {
'id': this.id,
'title': this.title
}
})
}
console.log('current', current, 'value', value, 'add', toAdd, 'remove', toRemove, 'model', this.checked.users)
}
}
}
}

Vue Router: how to cast params as integers instead of strings?

When I enter a URL using the browser field, the params are cast as strings, rather than an integer, e.g. /user/1 returns {id: "1"}. However, when when using this.$route.push({}), the params are, correctly, cast as integers {id: 1}.
Is this behavior intended? If not, how do I fix it?
You have to handle casting any params values yourself. In the route object define a props function. Here is an example:
{
path: '/user/:userId',
component: UserProfile,
props: (route) => {
/**
* This would preserve the other route.params object properties overriding only
* `userId` in case it exists with its integer equivalent, or otherwise with
* undefined.
*/
return { ...route.params, ...{ userId: Number.parseInt(route.params.userId, 10) || undefined }
}
}
link to vue router docs this is under Function mode
I'm probably late to the party, but this is my take on this. I wrote a function that returns a function that casts route params values to the props with same name with the given type.
function paramsToPropsCaster(mapping) {
return function(route) {
let nameType = Object.entries(mapping); // [[param1, Number], [param2, String]]
let nameRouteParam = nameType.map(([name, fn]) => [name, fn(route.params[name])]); // [[param1, 1], [param2, "hello"]]
let props = Object.fromEntries(nameRouteParam); // {param1: 1, param2: "hello"}
return props;
}
}
And then, in your route definition:
{
path: '/projects/:param1/editor/:param2',
component: ProjectEditor,
name: 'project-editor',
props: paramsToPropsCaster({'param1': Number, 'param2': String}),
}
This is just a hint on what you can do to solve the problem asked here, don't use this verbatim!
You can use an array in props to support both types
props: {
type:[Number,String],
required:true
}
Seems like Vue Router doesn't provide a shortcut for this, so I've come up with my own. The castParams function below generates a props function that has the specified type casting built in. I've added casting for integers and booleans but you can easily extend this for whatever other types you want to cast to.
// casts should be an object where the keys are params that might appear in the route, and the values specify how to cast the parameters
const castParams = (casts) => {
return (route) => {
const props = {};
for (var key in route.params) {
const rawValue = route.params[key];
const cast = casts[key];
if (rawValue == null) {
// Don't attempt to cast null or undefined values
props[key] = rawValue;
} else if (cast == null) {
// No cast specified for this parameter
props[key] = rawValue;
} else if (cast == 'integer') {
// Try to cast this parameter as an integer
const castValue = Number.parseInt(rawValue, 10);
props[key] = isNaN(castValue) ? rawValue : castValue;
} else if (cast == 'boolean') {
// Try to cast this parameter as a boolean
if (rawValue === 'true' || rawValue === '1') {
props[key] = true;
} else if (rawValue === 'false' || rawValue === '0') {
props[key] = false;
} else {
props[key] = rawValue;
}
} else if (typeof(cast) == 'function') {
// Use the supplied function to cast this param
props[key] = cast(rawValue);
} else {
console.log("Unexpected route param cast", cast);
props[key] = rawValue;
}
}
return props;
};
};
Then you can use it in your route definitions, eg:
{
path: '/contact/:contactId',
component: 'contact-details-page',
props: castParams({contactId: 'integer'}),
},
I do prefer Rodener Dajes answer, and handle type casting and validation within the component instead of in the route definition:
props: {
id: {
type: [Number, String],
default: 0
},
},
The reason is that it will allow me to define the route much simpler and readable:
{
path: '/job/:id',
name: 'Job',
component: InvoiceJobDetail,
props: true
}
Many of these solutions seem unnecessary complex to me.
Here's what I did in my project - note that route params ending in ID or the param id itself, are automatically converted to Number, so in my case I just had to set props: typedProps(), in nearly all of my routes.
/**
* Casts props into proper data types.
* Props ending in 'ID' and the prop 'id' are cast to Number automatically.
* To cast other props or override the defaults, pass a mapping like this:
* #example
* // Truthy values like 'true', 'yes', 'on' and '1' are converted to Boolean(true)
* {
* path: '/:isNice/:age/:hatSize',
* name: 'foo route',
* props: typedProps({ isNice: Boolean, age: Number, hatSize: Number}),
* },
* #param {Object} mapping
* #returns
*/
const typedProps = (mapping) => {
if (!mapping) {
mapping = {}
}
return (route) => {
let props = {}
for (let [prop, value] of Object.entries(route.params)) {
if (prop in mapping) {
if (mapping[prop] === Boolean) {
value = ['true', '1', 'yes', 'on'].includes(value.toLowerCase())
} else {
value = mapping[prop](value)
}
} else if (prop === 'id' || prop.endsWith('ID')) {
value = Number(value)
}
props[prop] = value
}
return props
}
}
This could use some error handling in case a type coercion fails, but I'll leave that as an exercise for the reader :)
Based on the excellent answer from #pongi: https://stackoverflow.com/a/63897213 I came up with a new package: https://www.npmjs.com/package/vue-router-parse-props. It's written in typescript and has types. Please let me know, what you think.
npm i vue-router-parse-props
// src/router/index.ts
import propsParser from 'vue-router-parse-props'
import { parse } from 'date-fns'
const router = new Router({
base: process.env.BASE_URL,
mode: useHistory ? "history" : "hash",
routes: [
{
path: ':day/:userId',
name: 'UserProfile',
component: () => import('#/components/UserProfile.vue'),
props: paramsToPropsCaster({
userId: Number,
day: (val: string): Date => parse(val, 'yyyy-MM-dd', new Date()),
searchId: {
type: id,
routeKey: "query.q"
}
})
}
]
});

Vue - default values of nested properties

How can I set a default value of a nested property of a Object prop?
Apparently, Vue parse default value of nested properties only if the first level Object prop is undefined.
Example:
Vue.component('example', {
props: {
options: {
type: Object,
default: function() {
return {
nested: {
type: Object,
default: function(){
return 'default value'
}
}
}
}
}
})
Apparently, Vue parse default value of nested properties only if the
fist level Object prop is not undefined.
Yes and it makes sense because if you don't have outer object, you won't be able to have inner or nested properties.
So I think it's even more readable just set as default {} an emtpy object for the first level object and you should make your own defensive validations against undefined or null, like the bellow example:
<script>
export default {
props: {
option: {
type: Object,
default: () => {},
required: false
}
},
computed: {
optionReceived: function () {
const defaultNestedValue = 'Some default value'
const option = this.option.nested || defaultNestedValue;
return option;
}
}
}
</script>
I think it is always better to make your data structure easy to use and as flat as possible. Because nested props in Vue is never a good choice.
Assume the options you mentioned in your Vue component have a lot of properties inside.
Example:
props: {
options: {
bookAttributes: {
colorAttributes: { coverColor: 'red', ribbonColor: 'green' },
sizeAttributes: { coverSize: 10, ribbonSize: 2 },
...
}
}
}
you could make them flat like this for better comprehension.
props: {
coverSize: 10,
coverColor: 'red',
ribbonColor: 'green,
ribbonSize: 2 ...
}
And then you and your colleagues could happily use your component like this:
<your-component>
coverSize="15"
coverColor="blue"
ribbonColor="red"
ribbonSize="3"
</your-component>
Good luck and wish you well.