Unable to validate a child component from a parent component - vue.js

I have a selectbox component. I want to reused it in other components.I'm able to reused the selectbox component in other components by adding v-model directive on custom input component but unable to validate selectbox component from other components by using vuetify validation rules.
Test.vue
<v-form ref="form" class="mx-4" v-model="valid">
<Selectbox :name="ward_no" :validationRule="required()" v-model="test.ward_no"/>
<v-btn primary v-on:click="save" class="primary">Submit</v-btn>
export default {
data() {
return {
test: {
ward_no: ''
},
valid: false,
required(propertyType) {
return v => (v && v.length > 0) || `${propertyType} is required`;
},
};
}
Selectbox.vue
<select #change="$emit('input', $event.target.value)" >
<option
v-for="opt in options"
:key="opt.value"
:value="opt.value"
:selected="value === opt.value"
>
{{errorMessage}}
{{ opt.label || "No label" }}
</option>
</select>
export default {
props: {
label: {
type: String,
required: true
},
validationRule: {
type: String,
default: 'text',
validate: (val) => {
// we only cover these types in our input components.
return['requred'].indexOf(val) !== -1;
}
},
name: {
type: String
},
value: {
type: String,
required: true
}
},
data() {
return {
errorMessage: '',
option: "lorem",
options: [
{ label: "lorem", value: "lorem" },
{ label: "ipsum", value: "ipsum" }
]
};
},
methods:{
checkValidationRule(){
if(this.validationRule!="")
{
return this.validationRule.split('|').filter(function(item) {
return item();
})
// this.errorMessage!= ""?this.errorMessage + " | ":"";
}
},
required() {
this.errorMessage!= ""?this.errorMessage + " | ":"";
this.errorMessage += name+" is required";
},
}
};

Related

How to reset v-model

i have toggle and reset button:
<template>
<label :for='id + "_button"' :class='{"active": isActive}' class='toggle__button'>
<input type='checkbox' :id='id + "_button"' v-model='checkedValue'>
<span class='toggle__switch'></span>
<button #click='reset()'>reset</button>
</label>
</template>
script:
export default ({
props: {
defaultState: {
type: Boolean,
default: false
},
id: {
type: String,
default: 'primary'
}
},
data() {
return {
currentState: this.defaultState
};
},
computed: {
isActive() {
return this.currentState;
},
checkedValue: {
get() {
return this.defaultState;
},
set(newValue) {
this.currentState = newValue;
this.$emit('change', newValue);
}
}
},
methods: {
reset() {
this.checkedValue = false ;
}
}
});
toggle works well. But when you press the reset button, a bug appears. At which, toggle works only from the 2nd time. What needs to be added/fixed?
Your component is a custom component which could interact with parent one using two-way binding by defining a value as prop and emit the new value when the checkbox changes state :
<template>
<label :for='id + "_button"' :class='{"active": isActive}' class='toggle__button'>
<input type='checkbox' :id='id + "_button"' :value='value' #change="emitVal($event)">
<span class='toggle__switch'></span>
<button #click='emitVal(false)'>reset</button>
</label>
</template>
script :
export default ({
props: {
value: {
type: Boolean,
default: false
},
id: {
type: String,
default: 'primary'
}
},
methods: {
emitVal(val) {
this.$emit('input',val)
}
}
});
in parent :
<customCheckbox id="secondary" v-model="someValue"/>

vue 3 - binding problem with select, if option selected is not showing in the new values, select is in blank,but it doesn't change to ""

I have two components, an Input and a Select, the select show options depend on what you write in the Input, but if the option selected isn't showing in the new options, select the default option or the first option "select option"
When I select an option and then change the options in the Select and the option selected is not in the new options, the Select shows in blank, but when I change the option selected to "", the Select doesn't change to it(Select option that has value ""), I don't know why but if I change to other option the Select changes to it...
Example:
select option 2 > write 3 characters in the input
as you can see the varaible option_selected that is binding with the select change to "", but the select dosn't change to "Select option"
Link on the documentation for this example:
Component custom events
const input_text = {
name: "input-min-length",
props: {
text: String,
is_min_length: Boolean
},
emits: ["update:text", "update:is_min_length"],
computed: {
is_min(){
return this.text.length >= 3;
}
},
watch: {
is_min(new_value){
this.$emit("update:is_min_length", this.is_min);
}
},
template: `<div><input type="text" :value="text" #input="$emit('update:text', $event.target.value)" /></div>`
};
const select_options = {
name: "select-options",
props: {
option_selected: String,
is_min_length: Boolean
},
emits: ["update:option_selected"],
data() {
return {
options: [
{text: "Select option", value: ""},
{text: "option 1", value: "1", is_min_length: true},
{text: "option 2", value: "2"},
{text: "option 3", value: "3", is_min_length: true},
]
};
},
computed: {
options_filtered(){
if(this.is_min_length == false) return this.options;
return this.options.filter((option) => option.is_min_length == true || option.value == "")
}
},
watch: {
is_min_length(){
const is_presented = this.options_filtered.some((option) => option.value == this.option_selected)
if (is_presented == false){
setTimeout(() => {
this.$emit("update:option_selected", "");
}, 0); // I try to use setTimeout, to see if it changes at all
}
}
},
template: `
<select :value="option_selected" #change="$emit('update:option_selected', $event.target.value)">
<option v-for="option in options_filtered" :value="option.value" :key="option.value">
{{ option.text }}
</option>
</select>
`
}
const app = {
components:{
"input-min-length": input_text,
"select-options": select_options
},
data() {
return {
text: "",
is_min_length: false,
option_selected: ""
}
},
template: `
<div>
<input-min-length v-model:text="text" v-model:is_min_length="is_min_length" />
<select-options v-model:option_selected="option_selected" :is_min_length="is_min_length" /><br>
<button #click="option_selected='1'">change to opt 1</button><br>
<button #click="option_selected=''">change to opt ""</button><br>
<div>
<strong>DATA:</strong><br>
<strong>text:</strong> "{{text}}"<br>
<strong>is_min_length:</strong> {{is_min_length}}<br>
<strong>option_selected:</strong> "{{option_selected}}"
</div>
</div>`
}
Vue.createApp(app)
.mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<div id="app"></div>
Wow, this was a tricky one to drill down into.
What is "value"
So <select> element has no HTML value attribute, but there is a el.value property provided by the DOM API on HTMLSelectElement that gives you the selected option's value. Vue provides a binding to this value property (to be used via v-model). And that's why we can simply use :value in the <select> element.
Problem with your code:
When 'option 2' is selected, and then removed from the DOM via optionsFiltered, it sets the <select> element into an invalid state. In invalid state the element's el.value returns '' (an empty string) (Note: this is DOM API, not Vue). Now your watcher on is_min_length is triggered and emits an update:option_selected event with value '' (an empty string). As you know, Vue is reactive. Since el.value is already '', I imagine Vue does not see any need to update the DOM, and hence never calls el.value = ''. (Setting el.value to empty string even though its already an empty string does give desired behaviour, DOM API seems quite robust, its Vue that's not calling it).
Solution(s):
The easiest way would be to set the default "Select option" value to something other that '' so that it doesn't clash with the invalid state's ''. For e.g. you can set it to 0. Or maybe a string 'none'.
const input_text = {
name: "input-min-length",
props: {
text: String,
is_min_length: Boolean
},
emits: ["update:text", "update:is_min_length"],
computed: {
is_min(){
return this.text.length >= 3;
}
},
watch: {
is_min(new_value){
this.$emit("update:is_min_length", this.is_min);
}
},
template: `<div><input type="text" :value="text" #input="$emit('update:text', $event.target.value)" /></div>`
};
const select_options = {
name: "select-options",
props: {
option_selected: String,
is_min_length: Boolean
},
emits: ["update:option_selected"],
data() {
return {
options: [
{text: "Select option", value: "0"},
{text: "option 1", value: "1", is_min_length: true},
{text: "option 2", value: "2"},
{text: "option 3", value: "3", is_min_length: true},
]
};
},
computed: {
options_filtered(){
if(this.is_min_length == false) return this.options;
return this.options.filter((option) => option.is_min_length == true || option.value == "0")
}
},
watch: {
is_min_length(){
const is_presented = this.options_filtered.some((option) => option.value == this.option_selected)
if (is_presented == false){
setTimeout(() => {
this.$emit("update:option_selected", '0');
}, 0); // I try to use setTimeout, to see if it changes at all
}
}
},
template: `
<select :value="option_selected" #change="$emit('update:option_selected', $event.target.value)">
<option v-for="option in options_filtered" :value="option.value" :key="option.value">
{{ option.text }}
</option>
</select>
`
}
const app = {
components:{
"input-min-length": input_text,
"select-options": select_options
},
data() {
return {
text: "",
is_min_length: false,
option_selected: "0"
}
},
template: `
<div>
<input-min-length v-model:text="text" v-model:is_min_length="is_min_length" />
<select-options v-model:option_selected="option_selected" :is_min_length="is_min_length" /><br>
<button #click="option_selected='1'">change to opt 1</button><br>
<button #click="option_selected='0'">change to opt ""</button><br>
<div>
<strong>DATA:</strong><br>
<strong>text:</strong> "{{text}}"<br>
<strong>is_min_length:</strong> {{is_min_length}}<br>
<strong>option_selected:</strong> "{{option_selected}}"
</div>
</div>`
}
Vue.createApp(app)
.mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<div id="app"></div>
Another solution is to call the el.value = '' yourself alongside emitting the event, but I don't recommend it as it makes your code harder to understand for fellow Vue developers (you should adhere good coding practices even if you're the only one working on the project):
const input_text = {
name: "input-min-length",
props: {
text: String,
is_min_length: Boolean
},
emits: ["update:text", "update:is_min_length"],
computed: {
is_min(){
return this.text.length >= 3;
}
},
watch: {
is_min(new_value){
this.$emit("update:is_min_length", this.is_min);
}
},
template: `<div><input type="text" :value="text" #input="$emit('update:text', $event.target.value)" /></div>`
};
const select_options = {
name: "select-options",
props: {
option_selected: String,
is_min_length: Boolean
},
emits: ["update:option_selected"],
data() {
return {
options: [
{text: "Select option", value: ""},
{text: "option 1", value: "1", is_min_length: true},
{text: "option 2", value: "2"},
{text: "option 3", value: "3", is_min_length: true},
]
};
},
computed: {
options_filtered(){
if(this.is_min_length == false) return this.options;
return this.options.filter((option) => option.is_min_length == true || option.value == "")
}
},
watch: {
is_min_length(){
const is_presented = this.options_filtered.some((option) => option.value == this.option_selected)
if (is_presented == false){
setTimeout(() => {
this.$emit("update:option_selected", "");
document.getElementById("myselect").value = "";
}, 0); // I try to use setTimeout, to see if it changes at all
}
}
},
template: `
<select id="myselect" :value="option_selected" #change="$emit('update:option_selected', $event.target.value)">
<option v-for="option in options_filtered" :value="option.value" :key="option.value">
{{ option.text }}
</option>
</select>
`
}
const app = {
components:{
"input-min-length": input_text,
"select-options": select_options
},
data() {
return {
text: "",
is_min_length: false,
option_selected: ""
}
},
template: `
<div>
<input-min-length v-model:text="text" v-model:is_min_length="is_min_length" />
<select-options v-model:option_selected="option_selected" :is_min_length="is_min_length" /><br>
<button #click="option_selected='1'">change to opt 1</button><br>
<button #click="option_selected=''">change to opt ""</button><br>
<div>
<strong>DATA:</strong><br>
<strong>text:</strong> "{{text}}"<br>
<strong>is_min_length:</strong> {{is_min_length}}<br>
<strong>option_selected:</strong> "{{option_selected}}"
</div>
</div>`
}
Vue.createApp(app)
.mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<div id="app"></div>

Vue: Accessing prop to set class?

I am using a pagination library ( https://github.com/arnedesmedt/vue-ads-pagination ) and the VueAdsPageButton has a hidden prop on it called active that is a boolean value depending on whether or not the button is active. I am trying to set the id based on whether or not the active prop is true so I can style it accordingly. I tried:
v-bind:id="{ selected: active} but I get the warning that active is referenced in the render but doesn't exist. Not sure what I am doing wrong here.
This is my code below:
<VueAdsPagination
:total-items="totalOrdersNumber ? totalOrdersNumber : 0"
:page="page"
:loading="loading"
:items-per-page="10"
:max-visible-pages="10"
#page-change="pageChange"
#range-change="rangeChange"
>
<template
slot="buttons"
slot-scope="props"
>
<VueAdsPageButton
v-for="(button, key) in props.buttons"
v-bind:id="{ selected: active}"
:key="key"
:page="page"
v-bind="button"
#page-change="page = button.page;"
/>
</template>
</VueAdsPagination>
EDIT:
here is the component code from the library for VueAdsPageButton
<template>
<button
:class="buttonClasses"
:disabled="disabled"
:title="title"
#click="pageChange"
>
<i
v-if="loading"
class="fa fa-spinner fa-spin"
/>
<span
v-else
v-html="html"
/>
</button>
</template>
<script>
export default {
name: 'VueAdsPageButton',
props: {
page: {
type: [
Number,
String,
],
required: true,
},
active: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
html: {
type: String,
required: true,
},
title: {
type: String,
default: '',
},
loading: {
type: Boolean,
default: false,
},
disableStyling: {
type: Boolean,
default: false,
},
},
computed: {
buttonClasses () {
if (this.disableStyling) {
return {};
}
return {
'focus:vue-ads-outline-none': true,
'vue-ads-ml-1': true,
'vue-ads-leading-normal': true,
'vue-ads-w-6': true,
'vue-ads-bg-teal-500': this.active,
'vue-ads-text-white': this.active,
'vue-ads-cursor-default': this.active || this.disabled,
'vue-ads-bg-gray-200': this.disabled && this.page !== '...',
'vue-ads-text-gray': this.disabled && this.page !== '...',
'hover:vue-ads-bg-gray-100': !this.active && !this.disabled,
};
},
},
methods: {
pageChange () {
if (
this.page === '...' ||
this.disabled ||
this.active
) {
return;
}
this.$emit('page-change');
},
},
};
</script>
You can bind custom id or class on active button like this:
<vue-ads-page-button
v-for="(button, key) in props.buttons"
:key="key"
v-bind="button"
:id="button.active ? 'some-id' : null"
:class="{'some-class': button.active}"
#page-change="page = button.page"
#range-change="start = button.start; end = button.end"
/>
Here is also JSFiddle from library documentation where you can also see this - Link

Establishing initial value in a vue subcomponent

I have created a vue subcomponent to select an element from a list:
Vue.component('select-value-from-list', {
template: `
<select v-model="currentValue">
<option v-for='op in options' :value='op.value'>
{{op.label}}
</option>
</select>
`,
props: {
initialValue: {
type: String
},
options: {
type: Array,
required: true
}
},
data: function() {
return {
currentValue: "Indefinido"
}
},
watch: {
currentValue: function () {
this.$emit('value-change', this.currentValue);
}
},
created: function() {
if(this.initialValue)
this.currentValue = this.initialValue;
}
})
And I want the menu with options to appear with the option corresponding to the initialValue selected. How should I do this?

Vue: v-model and input event in custom component derived of a custom component

I have a custom input where I recieve a value prop and emit a input event on the input event. I can use this custom input without problems with a model, now I'm creating a custom password input that I initialize as a custom input but I can't bind the model using value and input event handlers (passing them to the custom input). How can I approach this?
Custom Input:
My program model > custom input (value and input event handler) : works
My program model > custom password (value and input event handler) > custom input: doesn't work.
Code:
Input.vue:
<template>
<div class="form-group">
<label for="" v-if="typeof label !== 'undefined'">{{ label }}</label>
<!-- GROUP -->
<template v-if="isGroup">
<div class="input-group">
<!-- PREPEND -->
<div v-if="hasPrepend" class="input-group-prepend"
:class="{'inside bg-transparent' : prependInside, 'pointer': prependPointer}"
#click="clickPrepend">
<span class="input-group-text"
:class="{'bg-transparent' : prependInside}">
<i aria-hidden="true"
v-if="prependType === 'icon'"
:class="'fa fa-' + prependContent"></i>
<template v-if="prependType === 'text'">{{ prependContent }}</template>
</span>
</div>
<!-- INPUT -->
<input class="form-control"
:type="type"
:class="generatedInputClass"
:readonly="readonly"
:disabled="disabled"
:value="value"
#input="inputEvent"
#change="onChange">
<!-- APPEND -->
<div v-if="hasAppend" class="input-group-append"
:class="{'inside bg-transparent' : appendInside, 'pointer': appendPointer}"
#click="clickAppend">
<span class="input-group-text"
:class="{'bg-transparent' : appendInside}">
<i aria-hidden="true"
v-if="appendType === 'icon'"
:class="'fa fa-' + appendContent"></i>
<template v-if="appendType === 'text'">{{ appendContent }}</template>
</span>
</div>
</div>
</template>
<!-- INPUT -->
<template v-else>
<input class="form-control"
:type="type"
:class="generatedInputClass"
:readonly="readonly"
:disabled="disabled"
:value="value"
#input="inputEvent"
#change="onChange"
>
</template>
<small class="form-text"
v-if="typeof helpText !== 'undefined'"
:class="generatedHelperClass">
{{ helpText }}
</small>
</div>
</template>
<script>
export default {
name: 'InputGroup',
props: {
value: String,
label: String,
helpText: String,
size: String,
prependContent: String,
appendContent: String,
prependType: {
type: String,
default: 'icon',
},
appendType: {
type: String,
default: 'icon',
},
prependInside: {
type: Boolean,
default: false,
},
appendInside: {
type: Boolean,
default: false,
},
prependPointer: {
type: Boolean,
default: false,
},
appendPointer: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'text',
},
valid: {
type: Boolean,
default: null,
},
},
watch: {
valid() {
},
},
computed: {
isGroup() {
return this.hasPrepend || this.hasAppend;
},
hasPrepend() {
return typeof this.prependContent !== 'undefined';
},
hasAppend() {
return typeof this.appendContent !== 'undefined';
},
generatedInputClass() {
const size = typeof this.size !== 'undefined' ? `form-control-${this.size}` : '';
let valid = '';
if (this.valid !== null) {
valid = this.valid ? 'is-valid' : 'is-invalid';
}
return `${size} ${valid}`;
},
generatedHelperClass() {
let valid = 'text-muted';
if (this.valid !== null) {
valid = this.valid ? 'valid-feedback' : 'invalid-feedback';
}
return `${valid}`;
},
},
methods: {
inputEvent(e) {
this.$emit('input', e.target.value);
},
clickPrepend(e) {
this.$emit('click-prepend', e);
},
clickAppend(e) {
this.$emit('click-append', e);
},
onChange(e) {
this.$emit('change', this.value, e);
},
},
};
</script>
Password.vue:
<template>
<div>
<app-input
:label="label"
:type="type"
prepend-content="lock"
:append-content="passwordIcon"
:append-inside="true"
:append-pointer="true"
#click-append="tooglePassword"
:value="value"
#input="inputEvent">
</app-input>
</div>
</template>
<script>
import Input from './Input';
export default {
name: 'Password',
components: {
appInput: Input,
},
props: {
value: String,
label: {
type: String,
default: 'ContraseƱa',
},
readonly: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
valid: {
type: Boolean,
default: null,
},
},
data() {
return {
pass: '',
type: 'password',
};
},
computed: {
passwordIcon() {
return this.type === 'password' ? 'eye' : 'eye-slash';
},
},
methods: {
tooglePassword() {
this.type = this.type === 'password' ? 'text' : 'password';
},
inputEvent(e) {
this.$emit('input', e.target.value);
},
},
};
</script>
The thing is the input event your Password listens form app-input component, has as value the actual string value already, not the element (that you'd have to call e.target.value to get the string value)
In other words, in Password.vue, instead of:
inputEvent(e) {
this.$emit('input', e.target.value);
},
Do:
inputEvent(e) {
this.$emit('input', e);
},
CodeSandbox demo here.