Repository: logaretm/vee-validate
Pull request #4270 (2023-05-18 13:23:40)

Add `componentField` to Field's slot
🔎 __Overview__

This PR adds an ability to easily bind all useful props to complex components, same as it currently works with native elements.

🤓 __Code snippets/examples (if applicable)__

```vue
<Field name="name" v-slot="{ componentField }">
  <ComplexInput v-bind="componentField" /> // some component that expects value to be set as `modelValue` prop
</Field>
```

I was looking for such feature, as it's a bit tedious and too verbose binding all props / listeners to custom components manually. Probably there is already an easy solution that I've missed?
 


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/logaretm/vee-validate/pull/4270?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) Report
> Merging [#4270](https://app.codecov.io/gh/logaretm/vee-validate/pull/4270?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) (ca99911) into [main](https://app.codecov.io/gh/logaretm/vee-validate/commit/12399d2395ee831ea5603a8f6e9f444597bf8cdd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) (12399d2) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad).

```diff
@@           Coverage Diff           @@
##             main    #4270   +/-   ##
=======================================
  Coverage   98.10%   98.11%           
=======================================
  Files          73       73           
  Lines        5753     5777   +24     
  Branches     1092     1095    +3     
=======================================
+ Hits         5644     5668   +24     
  Misses        109      109           
```


| [Impacted Files](https://app.codecov.io/gh/logaretm/vee-validate/pull/4270?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) | Coverage Δ | |
|---|---|---|
| [packages/vee-validate/src/Field.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/4270?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmVlLXZhbGlkYXRlL3NyYy9GaWVsZC50cw==) | `98.71% <100.00%> (+0.10%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad)

FedorSherbakov: Cool! I reused the `defineComponentBinds` test with a small extension. LMK if it's OK, or you see something worth adding now

Patch:
diff --git a/docs/src/pages/api/field.mdx b/docs/src/pages/api/field.mdx
index 58330146..b6966f18 100644
--- a/docs/src/pages/api/field.mdx
+++ b/docs/src/pages/api/field.mdx
@@ -329,3 +329,7 @@ An array containing a few listeners for the `input` event, it involves updating
 </CodeTitle>
 
 An array containing a few listeners for the `change` event, it involves updating the field value and triggering validation.
+
+#### `componentField`
+
+Same as `field`, but provides value as `modelValue` to use with components
diff --git a/packages/vee-validate/src/Field.ts b/packages/vee-validate/src/Field.ts
index 2e85b389..d150c16a 100644
--- a/packages/vee-validate/src/Field.ts
+++ b/packages/vee-validate/src/Field.ts
@@ -15,22 +15,30 @@ interface ValidationTriggersProps {
   validateOnModelUpdate: boolean;
 }
 
-interface FieldBindingObject<TValue = any> {
+interface SharedBindingObject<TValue = any> {
   name: string;
   onBlur: (e: Event) => void;
   onInput: (e: Event) => void;
   onChange: (e: Event) => void;
   'onUpdate:modelValue'?: ((e: TValue) => unknown) | undefined;
+}
+
+interface FieldBindingObject<TValue = any> extends SharedBindingObject<TValue> {
   value?: TValue;
   checked?: boolean;
 }
 
+interface ComponentFieldBindingObject<TValue = any> extends SharedBindingObject<TValue> {
+  modelValue?: TValue;
+}
+
 interface FieldSlotProps<TValue = unknown>
   extends Pick<
     FieldContext,
     'validate' | 'resetField' | 'handleChange' | 'handleReset' | 'handleBlur' | 'setTouched' | 'setErrors'
   > {
   field: FieldBindingObject<TValue>;
+  componentField: ComponentFieldBindingObject<TValue>;
   value: TValue;
   meta: FieldMeta<TValue>;
   errors: string[];
@@ -147,7 +155,7 @@ const FieldImpl = /** #__PURE__ */ defineComponent({
       ctx.emit('update:modelValue', value.value);
     };
 
-    const fieldProps = computed(() => {
+    const sharedProps = computed(() => {
       const { validateOnInput, validateOnChange, validateOnBlur, validateOnModelUpdate } =
         resolveValidationTriggers(props);
 
@@ -185,6 +193,14 @@ const FieldImpl = /** #__PURE__ */ defineComponent({
 
       attrs['onUpdate:modelValue'] = e => onChangeHandler(e, validateOnModelUpdate);
 
+      return attrs;
+    });
+
+    const fieldProps = computed(() => {
+      const attrs = {
+        ...sharedProps.value,
+      };
+
       if (hasCheckedAttr(ctx.attrs.type) && checked) {
         attrs.checked = checked.value;
       }
@@ -197,9 +213,17 @@ const FieldImpl = /** #__PURE__ */ defineComponent({
       return attrs;
     });
 
+    const componentProps = computed(() => {
+      return {
+        ...sharedProps.value,
+        modelValue: value.value,
+      };
+    });
+
     function slotProps(): FieldSlotProps {
       return {
         field: fieldProps.value,
+        componentField: componentProps.value,
         value: value.value,
         meta,
         errors: errors.value,


Test patch:
diff --git a/packages/vee-validate/tests/Field.spec.ts b/packages/vee-validate/tests/Field.spec.ts
index eff72777..13a282bd 100644
--- a/packages/vee-validate/tests/Field.spec.ts
+++ b/packages/vee-validate/tests/Field.spec.ts
@@ -1,7 +1,8 @@
-import { defineRule, configure } from '@/vee-validate';
+import { defineRule, configure, useForm } from '@/vee-validate';
 import { mountWithHoc, setValue, dispatchEvent, setChecked, flushPromises, dispatchFileEvent } from './helpers';
 import * as yup from 'yup';
 import { computed, reactive, ref, Ref } from 'vue';
+import ModelComp from './helpers/ModelComp';
 
 vi.useFakeTimers();
 
@@ -1208,4 +1209,36 @@ describe('<Field />', () => {
     expect(lastCallOf(radioType)).toHaveProperty('checked');
     expect(lastCallOf(radioType)).not.toHaveProperty('value');
   });
+
+  describe('componentField', () => {
+    test('provides bindable object to components', async () => {
+      mountWithHoc({
+        components: {
+          ModelComp,
+        },
+        template: `
+          <Field name="name" v-slot="{ componentField, errorMessage, value }" rules="required">
+            <ModelComp v-bind="componentField" />
+            <span id="errors">{{ errorMessage }}</span>
+            <span id="values">{{ value }}</span>
+          </Field>
+        `,
+      });
+
+      await flushPromises();
+      const errorEl = document.getElementById('errors');
+      const valuesEl = document.getElementById('values');
+      const inputEl = document.querySelector('input') as HTMLInputElement;
+      expect(inputEl.getAttribute('name')).toBe('name');
+      setValue(inputEl, '');
+      dispatchEvent(inputEl, 'blur');
+      await flushPromises();
+      expect(errorEl?.textContent).toBe('This field is required');
+      setValue(inputEl, '123');
+      dispatchEvent(inputEl, 'blur');
+      await flushPromises();
+      expect(errorEl?.textContent).toBe('');
+      expect(valuesEl?.textContent).toBe('123');
+    });
+  });
 });
diff --git a/packages/vee-validate/tests/helpers/ModelComp.ts b/packages/vee-validate/tests/helpers/ModelComp.ts
index d2ffb868..bba001f1 100644
--- a/packages/vee-validate/tests/helpers/ModelComp.ts
+++ b/packages/vee-validate/tests/helpers/ModelComp.ts
@@ -1,6 +1,7 @@
 export default {
-  props: ['modelValue', 'test'],
+  props: ['modelValue', 'name', 'test'],
   emits: ['blur', 'update:modelValue'],
-  template: `<input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" @blur="$emit('blur')">
+  inheritAttrs: false,
+  template: `<input type="text" :name="name" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" @blur="$emit('blur')">
   <div v-if="test">{{ test }}</div>`,
 };