How can I modify event payload within a directive? - javascript

I have custom input component that uses v-model directive, so on input it emits input event with value, and v-mask directive, that modifies value by conforming current input value to the mask and emitting another input event with modified value. However this approach fires two input events, and toggling two model changes - one raw, and one masked. Can I modify existing input event value within a directive?
const maskDirective = (() => {
const state = new Map();
return {
bind: (el) => {
const element = el instanceof HTMLInputElement ? el : el.querySelector('input');
const textMaskInput = createTextMaskInputElement({
inputElement: element,
mask: TextMasks.phoneNumber,
});
state.set('element', element);
state.set('input', textMaskInput);
},
update: () => {
const textMaskInput = state.get('input');
const element = state.get('element');
const {
state: { previousConformedValue },
} = textMaskInput;
textMaskInput.update();
// otherwise there's call stack size exceeded error, because it constantly fires input event from component, catches it, and fires event from directive
if (previousConformedValue !== element.value) {
const event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
}
},
};
})();
<template>
<div
:class="{ 'is-disabled': disabled }"
class="c-base-input"
>
<input
ref="control"
v-bind="$attrs"
:class="{
'has-leading-icon': $slots['leading-icon'],
'has-trailing-icon': $slots['trailing-icon'],
'has-prepend-content': $slots['prepend'],
'has-append-content': $slots['append'],
'has-value': value !== null,
}"
:disabled="disabled"
:value="value"
:type="type"
class="c-base-input__control"
#input="onInput($event.target.value)"
>
<div
v-if="$slots['leading-icon']"
class="c-base-input__icon is-leading"
>
<slot name="leading-icon" />
</div>
<div
v-if="$slots['trailing-icon']"
class="c-base-input__icon is-trailing"
>
<slot name="trailing-icon" />
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'text',
validator: value => ['text', 'tel', 'email', 'password'].indexOf(value) !== -1,
},
},
methods: {
onInput(value) {
if (value === '') {
this.$emit('input', null);
} else {
this.$emit('input', value);
}
},
},
};
</script>

Related

How to trigger rules validation inside a method vue?

I'm working with a component which is a date-picker and i have some rule validations(is required, not valid etc). The thing is that right now the validations triggers every time i type in a caracter, so i want to change it to trigger when the user lose the focous(onBlur).
My component looks like this:
<template>
<v-menu
v-model="pickerVisible"
:close-on-content-click="true"
:nudge-right="40"
transition="scale-transition"
offset-x
min-width="290px"
>
<template v-slot:activator="{ attrs }">
<app-text-box
v-model="model"
#blur="textFieldBlurHandler($event.target.value)"
:label="label"
#click:append="pickerVisible = true"
append-icon="mdi-calendar"
:filled="filled"
v-bind="attrs"
:rules="componentRules"
:disabled="disabled"
hide-details="auto"
class="mb-2"
ref="picker"
></app-text-box>
</template>
<v-date-picker
v-model="pickerModel"
#input="pickerInputHandler"
ref="picker"
reactive
></v-date-picker>
</v-menu>
</template>
<script lang="ts">
import Vue from 'vue'
import { displayDate, displayUTCDate } from '#/types/string-helpers'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
export default Vue.extend({
name: 'app-date-picker',
data: function () {
return {
pickerVisible: false,
pickerModel: ''
}
},
props: {
label: {
type: String,
required: false
},
filled: {
type: Boolean,
required: false
},
disabled: {
type: Boolean,
required: false
},
rules: {
type: Array,
required: false,
default: function () {
return []
}
},
value: String
},
computed: {
model: {
get: function (): any {
return displayUTCDate(this.value) ?? null
},
set: function (newValue: string) {
// The text field binds to this setter, so we must define it to avoid runtime errors.
// However, we do not want to begin the process of formatting/updating the value until they are done typing in the date.
// Thus, we shall throw away the new value while the user is still typing; we will update the value on blur.
}
},
componentRules: function () {
return this.rules.concat([
(value: string) => this.dateIsValid(value) || 'Date must be standard MM/DD/YYYY format.'
])
}
},
watch: {
value: function (newValue: string) {
this.pickerModel = dayjs.utc(newValue).format('YYYY-MM-DD') // Update the datepicker selection when the user enters a new date
}
},
methods: {
emitNewValue (newValue: string) {
this.$emit('input', newValue)
this.$emit('blur')
this.$emit('onChange', newValue)
},
pickerInputHandler (pickerSelection: string) {
this.emitNewValue(dayjs(pickerSelection).format('MM/DD/YYYY')) // Pass the new value up to the parent component, who will pass it back down into this.value
},
textFieldBlurHandler (textFieldInput: string) {
const date = displayDate(textFieldInput)
if (date !== '') {
this.emitNewValue(date) // Pass the new value up to the parent component, who will pass it back down into this.value
}
},
dateIsValid (value: string): boolean {
if (!value || value === '') {
return true
}
return dayjs(value, 'MM/DD/YYYY', true).isValid()
}
},
mounted: function () {
const date = displayUTCDate(this.value)
if (date !== '') {
this.pickerModel = date
} else {
this.pickerModel = dayjs().format('MM/DD/YYYY')
}
this.$nextTick(() => {
this.pickerModel = dayjs.utc(this.pickerModel).format('YYYY-MM-DD') // Update the datepicker selection when the user enters a new date YYYY-MM-DD
})
}
})
</script>
I want to execute the componentRules computed property inside the textFieldBlurHandler() method.

Value not emitting from child to parent component using custom events

I have a problem with two web components created using lit-element v3.2.0 and I'm using custom events to emit the input value from the child component up to the parent component.
The form-input component is a reusable input that extracts the input value and emits it to the parent component with a custom event named "new-value" which is dispatched when the user writes in the field.
The form-container component contains the form-input, in here I'm binding the custom event "new-value" to a method called "updateInputValue" which should reassign the inputValue property with the emitted value from the child component, but instead is stuck with whatever value initialized in the parent constructor.
form-container.js
static get properties() {
return {
inputValue: { type: String },
items: { type: Array },
}
}
constructor() {
super()
this.inputValue = ''
this.items = []
}
render() {
return html`<form>
<h1>My form container</h1>
<form-input
#new-value=${this.updateInputValue}
fieldName="name"
id="name"
label="Name"
placeholder="Enter anything"
value="${this.inputValue}"
></form-input>
<button #click=${this.addNewItem} type="submit">Add</button>
<form-list .items="${this.items}"></form-list>
</form>`
}
updateInputValue(e) {
// Update input value with the value emitted from the form-input
this.inputValue = e.detail
}
addNewItem(e) {
// Add new item to the list
e.preventDefault()
console.log('add new item with the following value:', this.inputValue)
form-input.js
static get properties() {
return {
value: { type: String },
fieldName: { type: String },
label: { type: String },
placeholder: { type: String },
type: { type: String },
}
}
constructor() {
super()
}
render() {
return html`
<div>
<label for="name">${this.label}</label>
<input
#input=${this.dispatchEvent}
id="${this.fieldName}"
name="${this.fieldName}"
placeholder="${this.placeholder}"
type="${this.type || 'text'}"
value="${this.value}"
/>
</div>
`
}
dispatchEvent(e) {
// Emit the new value from the input to the parent component
const target = e.target
if (target) {
this.dispatchEvent(
new CustomEvent('new-value', {
detail: target.value,
})
)
}
}
Any help will be very much appreciated.
You are overwriting the dispatchEvent method and calling yourself. Rename the dispatchEvent Method and give it a meaningful name. It works perfectly.
<script type="module">
import {
LitElement,
html, css
} from "https://unpkg.com/lit-element/lit-element.js?module";
class FormInput extends LitElement {
static get properties() {
return {
value: { type: String },
fieldName: { type: String },
label: { type: String },
placeholder: { type: String },
type: { type: String },
};
}
render() {
return html`
<div>
<label for="name">${this.label}</label>
<input
#input=${this.changedInput}
id="${this.fieldName}"
name="${this.fieldName}"
placeholder="${this.placeholder}"
type="${this.type || 'text'}"
value="${this.value}"
/>
</div>
`
}
changedInput(e) {
// Emit the new value from the input to the parent component
console.log(e.target.value);
const myevent = new CustomEvent('my-event', {
bubbles: true,
composed: true,
detail: {
value: e.target.value
}
})
this.dispatchEvent(
myevent
);
}
}
class FormContainer extends LitElement {
static get properties() {
return {
name: {
inputValue: { type: String },
}
};
}
updateInputValue(e) {
console.log('received ' + e.detail.value);
this.inputValue = e.detail.value;
}
addNewItem(e) {
// Add new item to the list
e.preventDefault()
console.log('add new item with the following value:', this.inputValue);
}
render() {
return html`<form>
<h1>My form container</h1>
<form-input
#my-event="${this.updateInputValue}"
fieldName="name"
id="name"
label="Name"
placeholder="Enter anything"
value="${this.inputValue}"
></form-input>
<button #click=${this.addNewItem} type="submit">Add</button>
<form-list .items="${this.items}"></form-list>
</form>
`;
}
}
customElements.define("form-container", FormContainer);
customElements.define("form-input", FormInput);
</script>
<form-container></form-container>

How do I access a value in a child component in vue

Vcode is in a child component.
data() {
return {
vcode: null,
};
},
I need to access this value in a parent component method.
verifyCode() {
const code = this.vcode
}
Attempting this returns undefined. How do I access this value?
Update
I tried the suggestions and I still get an undefined value
Input field on child component
<input
class="form-control mt-5"
v-model.trim="vcode"
:class="{'is-invalid' : $v.vcode.$error }"
#input="$v.vcode.$touch(), vcodenum = $event.target.value"
placeholder="Enter your 6 digit code"
/>
On the parent component I added the following where it references the child component
<step2 ref="step2" #on-validate="mergePartialModels" v-on:vcodenum="vcode = $event"></step2>
My method in the parent component
verifyCode() {
const code = this.vcode
console.log(code)
}
I still get undefined.
I also tried this:
Child component
<input
class="form-control mt-5"
v-model.trim="vcode"
:class="{ 'is-invalid': $v.vcode.$error }"
#input="$v.vcode.$touch(), onInput"
placeholder="Enter your 6 digit code"
/>
Props
props: {
value: {
type: [String, Number, Boolean],
default: "",
},
},
method
onInput(e) {
this.$emit('on-input', e.target.value)
},
Parent
<step2 ref="step2" #on-validate="mergePartialModels" :value="vcode" #on-input="handleInput"></step2>
data() {
return {
vcode: null
};
},
method
handleInput(value) {
this.vcode = value
console.log(this.vcode)
},
The value ends up outputting null.
If I use the v-bind I get this error:
:value="value" conflicts with v-model on the same element because the latter already expands to a value binding internally
You can listen to the child's input event and send the value to the parent.
// InputComponent.vue
<input :value="value" #input="onInput" />
....
props: {
value: {
type: [String, Number, Boolean] // Add any custom types,
default: ''
}
},
methods: {
onInput(e) {
this.$emit('on-input', e.target.value)
}
}
// Parent.vue
<InputComponent :value="vCode" #on-input="handleInput" />
....
data() {
return {
vcode: null
}
},
methods: {
handleInput(value) {
this.vode = value
}
}

Vue JS prop error for value on radio button with v-model and v-bind="$attrs"

I am getting some strange behaviour that I cannot wrap my head around.
I have a simple radio button component that's used as a "wrapper" for an actual radio button.
On this component, I have inheritAttrs: false and use v-bind="$attrs" on the element itself so I can use v-model and value etc.
However, upon selecting a radio button, an error is thrown that the prop value is invalid (because it's an event and not a string) and interestingly I noticed that on initial render the value prop is blank in Vue Devtools.
I'm simply trying to get these radio buttons updating the parent's data object value for location with a string value of the radio button selected.
I can't figure out where I'm going wrong here exactly. Any help greatly appreciated.
Example project of the problem:
https://codesandbox.io/embed/m40y6y10mx
FormMain.vue
<template>
<div>
<p>Location: {{ location }}</p>
<form-radio
id="location-chicago"
v-model="location"
value="Chicago"
name="location"
label="Chicago"
#change="changed"
/>
<form-radio
id="location-london"
v-model="location"
value="London"
name="location"
label="London"
#change="changed"
/>
</div>
</template>
<script>
import FormRadio from "./FormRadio.vue";
export default {
name: "FormMain",
components: {
FormRadio
},
data() {
return {
location: ""
};
},
methods: {
changed(e) {
console.log("Change handler says...");
console.log(e);
}
}
};
</script>
FormRadio.vue
<template>
<div>
<label :for="id">
{{ label }}
<input
:id="id"
type="radio"
:value="value"
v-on="listeners"
v-bind="$attrs"
>
</label>
</div>
</template>
<script>
export default {
name: "FormRadio",
inheritAttrs: false,
props: {
id: {
type: String,
required: true
},
label: {
type: String,
required: true
},
value: {
type: String,
required: true
}
},
computed: {
listeners() {
return {
...this.$listeners,
change: event => {
console.log("Change event says...");
console.log(event.target.value);
this.$emit("change", event.target.value);
}
};
}
}
};
</script>
Edit
Found this neat article which describes the model property of a component. Basically it allows you to customise how v-model works. Using this, FormMain.vue would not have to change. Simply remove the value prop from FormRadio and add the model property with your own definition
See updated codepen:
FormRadio Script
<script>
export default {
name: "FormRadio",
inheritAttrs: false,
props: {
id: {
type: String,
required: true
},
label: {
type: String,
required: true
}
},
// customize the event/prop pair accepted by v-model
model: {
prop: "radioModel",
event: "radio-select"
},
computed: {
listeners() {
return {
...this.$listeners,
change: event => {
console.log("Change event says...");
console.log(event.target.value);
// emit the custom event to update the v-model value
this.$emit("radio-select", event.target.value);
// the change event that the parent was listening for
this.$emit("change", event.target.value);
}
};
}
}
};
</script>
Before Edit:
Vue seems to ignore the value binding attribute if v-model is present. I got around this by using a custom attribute for the value like radio-value.
FormMain.vue
<form-radio
id="location-chicago"
v-model="location"
radio-value="Chicago"
name="location"
label="Chicago"
#change="changed"
/>
<form-radio
id="location-london"
v-model="location"
radio-value="London"
name="location"
label="London"
#change="changed"
/>
The input event handler will update the v-model.
FormRadio.vue
<template>
<div>
<label :for="(id) ? `field-${id}` : false">
{{ label }}
<input
:id="`field-${id}`"
type="radio"
:value="radioValue"
v-on="listeners"
v-bind="$attrs"
>
</label>
</div>
</template>
<script>
export default {
name: "FormRadio",
inheritAttrs: false,
props: {
id: {
type: String,
required: true
},
label: {
type: String,
required: true
},
radioValue: {
type: String,
required: true
}
},
computed: {
listeners() {
return {
...this.$listeners,
input: event => {
console.log("input event says...");
console.log(event.target.value);
this.$emit("input", event.target.value);
},
change: event => {
console.log("Change event says...");
console.log(event.target.value);
this.$emit("change", event.target.value);
}
};
}
}
};
</script>
See forked codepen
I removed the v-model entirely from the child component call (this was conflicting);
<form-radio
id="location-chicago"
value="Chicago"
name="location"
label="Chicago"
#change="changed"
/>
<form-radio
id="location-london"
value="London"
name="location"
label="London"
#change="changed"
/>
I then updated your changed method to include to set the location variable
methods: {
changed(e) {
console.log("Change handler says...");
console.log(e);
this.location = e;
}
}
Updated: Link to updated CodeSandbox

VueJS pass default prop without reference to child component

I've stumbled upon this situation where I want to pass a prop to a child component that will be the default value of the component, but it will only be showed when the initial value is empty.
Parent Component:
<multi-line-input v-model="data.something" placeholder="Enter Something" :default="data.something"/>
Child Component
props: {
value: {
type: String,
default: ''
},
default: {
type: String,
default: ''
},
},
methods: {
emitBlur (e) {
if (!this.value && this.default) {
this.value = this.default
}
this.$emit('blur')
},
emitInput () {
this.$emit('input', this.$el.value)
}
}
So what I am trying to achieve basically, is when the component loads will get the value from v-model it will also receive a default value that shouldn't change, and only used as a value when the actual value is empty on blur
The default will have the initial value of data.something and it should not change!
I tried to get rid of the reference using JSON.parse(JSON.stringify(this.value)) but it doesn't seem to work either!
So if I understand your question correctly, you want this behavior: upon the blur event on your <multi-line-input> component, if the value of the input is empty, then set the value to a default value which is specified by the parent (through a prop).
First of all, it is an error to do this.value = ... in your component. You must not modify props, props pass data from parent to child only, the data passed through props is not yours to modify directly from within the component.
Try something like this:
Vue.component('multi-line-input', {
template: '<input #blur="onBlur" #input="onInput" :value="value">',
props: {
value: {
type: String,
default: '',
},
default: {
type: String,
default: '',
},
},
methods: {
onBlur() {
if (!this.value && this.default) {
this.$emit('input', this.default);
}
},
onInput(e) {
this.$emit('input', e.target.value);
},
},
});
new Vue({
el: '#app',
data: {
user: null,
initialUser: null,
},
created() {
// Pretend that I'm pulling this data from some API
this.user = {
name: 'Fred',
email: 'fred#email.com',
address: '123 Fake St',
};
// Make a copy of the data for the purpose of assigning the
// default prop of each input
this.initialUser = _.cloneDeep(this.user);
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<template v-if="user">
<multi-line-input v-model="user.name" :default="initialUser.name"></multi-line-input>
<multi-line-input v-model="user.email" :default="initialUser.email"></multi-line-input>
<multi-line-input v-model="user.address" :default="initialUser.address"></multi-line-input>
</template>
</div>
Or, if you want the default value to be determined by the component instead of the parent (through a prop), you can do something like this instead:
Vue.component('multi-line-input', {
template: '<input #blur="onBlur" #input="onInput" :value="value">',
props: {
value: {
type: String,
default: '',
},
},
created() {
this.def = this.value;
},
methods: {
onBlur() {
if (!this.value && this.def) {
this.$emit('input', this.def);
}
},
onInput(e) {
this.$emit('input', e.target.value);
},
},
});
new Vue({
el: '#app',
data: {
user: null,
},
created() {
// Pretend that I'm pulling this data from some API
this.user = {
name: 'Fred',
email: 'fred#email.com',
address: '123 Fake St',
};
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<template v-if="user">
<multi-line-input v-model="user.name"></multi-line-input>
<multi-line-input v-model="user.email"></multi-line-input>
<multi-line-input v-model="user.address"></multi-line-input>
</template>
</div>
However I do not recommend the second approach because the child component instance will only every have one default value for its entire lifetime. Vue reuses component instances whenever possible, so it wouldn't work if Vue were to bind it to a different parent component (how/when would it update its own default state?).

Categories

Resources