Typescript React onChangeHandler type error - input

This is the simplified version of my react component. The inputs are renders based on value of isCase1. I have a onChangeHandler to update formContent.
...
interface AddressForm {
name: string;
address: string;
}
interface PhoneForm {
name: string;
phoneNumber: number;
}
const [formContent, setformContent] = useState<AddressForm | PhoneForm | undefined>(
undefined
);
const onChangeHandler = (e: React.FormEvent<HTMLInputElement>) => {
const { name, value } = e.target as HTMLInputElement;
setformContent({ ...formContent, [name]: value });
};
return (
<form>
<input
type="text"
name="name"
value={formContent.name}
onChange={onChangeHandler}
/>
{isCase1 ? (
<input
type="text"
name="address"
value={formContent.address}
onChange={onChangeHandler}
/>
) : (
<input
type="number"
name="phoneNumber"
value={formContent.phoneNumber}
onChange={onChangeHandler}
/>
)}
</form>
);
However, this onChangeHandler is giving me an error saying:
"Argument of type '{} | { name: string; address: string; } | { name: string; phoneNumber: number; }' is not assignable to parameter of type 'SetStateAction<AddressForm | PhoneForm | undefined>'.
Type '{}' is not assignable to type 'SetStateAction<AddressForm | PhoneForm | undefined>'.ts(2345)"
How can I specify the type in the onChangeHandler to fix this issue?

const [formContent, setformContent] = useState<AddressForm | PhoneForm | undefined | {}>(undefined);

Related

Validating Fields of an Object in a Form - Vue 3 + Vee-validate + Yup

I have a form where certain fields are inside an object:
<script setup lang="ts">
...
const schema: yup.SchemaOf<any> =
yup.object({
group: yup.string().nullable().required(),
description: yup.string().nullable().required(),
user: yup
.object({
first_name: yup.string().required(),
last_name: yup.string().required(),
})
.required(),
})
const { errors, handleSubmit, isSubmitting } = useForm({
validationSchema: schema,
initialValues: {
group: '',
description: '',
user: {}
},
});
const { value: group } = useField<string>('group');
const { value: description } = useField<string>('description');
const { value: user } = useField<any>('user');
const isValid = useIsFormValid();
...
</script>
<template>
<div>
<label for="group">Group:</label
><input id="group" v-model="group" type="text" />
</div>
<div>
<label for="description">Description:</label
><input id="description" v-model="description" type="text" />
</div>
<div>
<label for="first-name">First name</label
><input id="first-name" v-model="user.first_name" type="text" />
</div>
<div>
<label for="last-name">Last name</label
><input id="last-name" v-model="user.last_name" type="text" />
</div>
<button :disabled="!isValid">Save</button>
...
</template>
But the data validation of this object is only done after changing a field outside of it, i.e. fill in group, description, first_name, last_name (in this same order) and the form will NOT be considered valid, only if you edit group or description again.
How can I make the validation be done when I change the field myself?
Here is the link to the complete code.
I am using the following versions:
"vue":"^3.2.37",
"vee-validate": "^4.5.11",
"yup": "^0.32.11"
When you use useField() with an Object, the nested properties lose their reactivity connection. So here are two options to resolve this: wrap useField with reactive() or use useField() for each nested property separately.
option 1
const { value: user } =reactive(useField('user'));
option 2
const { value: first_name } = useField('user.first_name');
const { value: last_name } = useField('user.last_name');
here is a working example here

Vee validate useForm with nested data

I have an object with some nested properties that I would like to bind to a form in vee validate using the composition api. This is what I'm doing right now but it feels pretty verbose. Can I utilize useForm to construct the data model and bind it to the UI?
This is what I have now:
<input type="text" v-model="field1" />
<input type="text" v-model="field2" />
<input type="text" v-model="field3" />
interface MyForm {
field1: string;
nested: { field2: string; field3: string; }
}
useForm({
validationSchema: { /* rules */ }
})
const { value: field1 } = useField<string>('field1')
const { value: field2 } = useField<string>('field2')
const { value: field3 } = useField<string>('field3')
// before sent to backend, the form needs to be reconstructed:
const form = computed<MyForm>(() => ({ field1, nested: { field2, field3 } }))
return { field1, field2, field3 }
What I'm after is something like:
<input type="text" v-model="field1" />
<input type="text" v-model="nested.field2" />
<input type="text" v-model="nested.field3" />
interface MyForm {
field1: string;
nested: { field2: string; field3: string; }
}
const {values} = useForm({
initialValues: { field1: '', nested: { field2: '', field3: '' } }
validationSchema: { /* rules */ }
})
return { ...toRefs(values) }
But according to the specification, values shouldn't be mutated. Do I have any options to my approach? I would like the complete form to be exposed to the UI and available as a Ref that I can pass along to my backend.

Buefy table not passing props in render function

I'm trying to use Buefy table component in a render function but the props are not being passed.
import { createElement as h } from '#vue/composition-api';
import { BTableColumn, BTable } from 'buefy/dist/esm/table';
h(BTable, {
class: 'table--resource-table table is-fullwidth is-hoverable',
ref: 'table',
props: {
data: filteredList.value,
backendSorting: true,
backendPagination: true,
backendFiltering: true,
sortIcon: 'angle-up',
iconPack: 'far',
defaultSortDirection: 'asc',
defaultSort: [props.sort, props.dir],
checkable: props.checkable,
isRowCheckable: props.isRowCheckable,
checkedRows: props.selectedRecords,
},
scopedSlots: {
default: (row) => {
console.log(row);
}
}
})
my console.log always return an empty object
Is it that for some reason I'm translating their example wrong to a render function?
<b-table :data="myData">
<b-table-column field="name" label="Name" v-slot="props">
{{ props.row.name }}
</b-table-column>
<b-table-column field="age" label="Age">
<template v-slot:default="props">
{{ props.row.age }}
</template>
</b-table-column>
</b-table>
After taking a look at the type definitions for createElement function I discovered you can pass a function to it:
export type ScopedSlot = (props: any) => ScopedSlotReturnValue;
export type VNodeChildren = VNodeChildrenArrayContents | [ScopedSlot] | string | boolean | null | undefined;
export interface CreateElement {
(tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), children?: VNodeChildren): VNode;
(tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), data?: VNodeData, children?: VNodeChildren): VNode;
}
This can be solved with:
h(BTable, {...}, [
(rowProps) => h('div', {}, context.slots.default(rowProps))
])

How to call components with function/method in pages?

My partner create a login page with several components like "email", "password", "phone number", "login button" and "forgot password". But then ask me to move all the components into new vue under '/components' folder. I only know to move the components but I cannot make it functional since the method is not callable. Please help.
original login page:
<template>
<div class="q-pa-md" style="width: 400px">
<q-form
#submit="onSubmit"
#reset="onReset"
class="q-gutter-md"
>
<!-- <q-input
filled
v-model="email"
label="Your email *"
lazy-rules
:rules="[val => !!val || 'Email is missing', isValidEmail]"
/>
<q-input
filled
type="password"
v-model="password"
label="Password *"
hint="Password should be 8 characters"
lazy-rules
:rules="[
val => val !== null && val !== '' || 'Please enter your password',
val => val.length === 8 || 'Please enter a valid password'
]"
/>
<q-input
filled
type="number"
v-model="phone"
label="Your phone number *"
lazy-rules
:rules="[
val => val !== null && val !== '' || 'Please enter your phone number',
val => val.length > 8 && val.length < 12 || 'Please enter a valid number'
]"
/>
<div>
<q-btn label="Login" type="submit" color="primary"/>
<q-btn flat to="/user/requestpassword" label="Forgot Password?" type="submit"/>
</div> -->
</q-form>
</div>
</template>
<script>
export default {
data () {
return {
email: null,
password: null,
phone: null,
accept: false
}
},
methods: {
isValidEmail (val) {
const emailPattern = /^(?=[a-zA-Z0-9#._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}#(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/
return emailPattern.test(val) || 'Invalid email'
},
onSubmit () {
if (this.accept !== true) {
this.$q.notify({
color: 'red-5',
textColor: 'white',
icon: 'warning'
})
} else {
this.$q.notify({
color: 'green-4',
textColor: 'white',
icon: 'cloud_done',
message: 'Submitted'
})
}
this.onReset()
},
onReset () {
this.email = null
this.password = null
this.phone = null
this.accept = false
}
}
}
</script>
new component vue:
<template>
<q-input
filled
v-model="email"
label="Your email *"
lazy-rules
:rules="[val => !!val || 'Email is missing', isValidEmail]"
/>
<q-input
filled
type="password"
v-model="password"
label="Password *"
hint="Password should be 8 characters"
lazy-rules
:rules="[
val => val !== null && val !== '' || 'Please enter your password',
val => val.length === 8 || 'Please enter a valid password'
]"
/>
<q-input
filled
type="number"
v-model="phone"
label="Your phone number *"
lazy-rules
:rules="[
val => val !== null && val !== '' || 'Please enter your phone number',
val => val.length > 8 && val.length < 12 || 'Please enter a valid number'
]"
/>
<div>
<q-btn label="Login" type="submit" color="primary"/>
<q-btn flat to="/user/requestpassword" label="Forgot Password?" type="submit"/>
</div>
</template>
<script>
export default {
}
</script>
<div id="parent">
<child :delegate="delegateMethod" ref="child" />
</div>
<script>
import { ref } from "vue";
export default({
setup() {
const child = ref()
const callChildMethod = () => {
child.method();
};
const delegateMethod = () => {
console.log("call from child");
};
return { delegateMethod };
}
});
</script>
<div id="child">
<p>child component</p>
<button #click="responseToParent()">
</div>
<script>
export default({
props: {
delegate: {
type: Function,
default: () => {}
}
},
setup(props) {
const method = () => {
console.log("call from parent");
};
const responseToParent = () => {
props.delegate();
};
return { responseToParent };
}
});
</script>
This is a very simple example. It works like this between parent and child components in Vue.

Two way data binding with :model.sync when prop has get() and set()

I have a computed property that I use as v-model on an input. I've written it this way to get reactivity -- this calls my setText Vuex action which I then can get with my getter text. It looks like this:
text: {
get() {
return this.text;
},
set(value) {
this.setText(value);
},
},
and I use it in my input like this:
<input class="input" type="text" v-model="text" />
This works well. Now, I've put the input in question into a separate component which I use. This means I have to pass the text v-model as props, which I do with :model.sync, like so:
<myInput :model.sync="text"/>
and in the myInput component I use the props like so:
<input class="input" id="search-order" type="text" :value="model" #input="$emit('update:model', $event)">
But this doesn't seem to work at all, whenever I type into the input, the input says: [object InputEvent] and if I try to see and the value of model it's {isTrusted: true}. I'm assuming it's because of the getters and setters I have on my computed property. How do I pass these down to the child component?
Instead of using the .sync modifier you can support the v-model directive in your custom component. v-model is syntax sugar for a value prop and an input event.
To support v-model just make sure your custom component has a value prop and emits an input event with the new value: this.$emit('input', event.target.value).
Here is an example of a <BaseInput> component I use, it's written in TypeScript:
<template>
<input
:type="type"
:value="value"
class="input"
v-on="listeners"
>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'BaseInput',
props: {
type: {
type: String,
default: 'text',
},
value: {
type: [String, Number],
default: '',
},
lazy: {
type: Boolean,
default: false,
},
number: {
type: Boolean,
default: false,
},
trim: {
type: Boolean,
default: false,
},
},
computed: {
modelEvent(): string {
return this.lazy ? 'change' : 'input'
},
parseModel(): (value: string) => string | number {
return (value: string) => {
if (this.type === 'number' || this.number) {
const res = Number.parseFloat(value)
// If the value cannot be parsed with parseFloat(),
// then the original value is returned.
return Number.isNaN(res) ? value : res
} else if (this.trim) {
return value.trim()
}
return value
}
},
listeners(): Record<string, Function | Function[]> {
return {
...this.$listeners,
[this.modelEvent]: (event: HTMLElementEvent<HTMLInputElement>) =>
this.$emit(this.modelEvent, this.parseModel(event.target.value)),
}
},
})
</script>
You can use it like so:
<BaseInput v-model="text" />