Repository: logaretm/vee-validate (ts)

Issue:
isFieldDirty on array of object field
### What happened?

I'm using field array, and I want to check if a specific field is dirty using `isFieldDirty`. It worked properly on basic field, but it's not as I expected when it used on array field.
Here I forked the field array example stackblitz and modify it to use `isFieldDirty`.
So, when I change a field that part of the array field, the dirty flag from the meta is true (work as expected). But the dirty flag from `isFieldDirty('users')` resulted as false, isn't should be true as well? Because the field `users` value has been changed and the dirty flag from meta is true tho
But, when I check the field array to the specific index and field like `isFieldDirty('users[0].name')`, it resulted true. Why when the path is refer to the field array name is not resulted as expected, compared with specific index path?

Or may be is there a function or prop that return the dirty field? For example
`const { dirtyFields } = useForm()`, that will be return the dirty field's name like `{'users'}`

Also i got question for the similar issue with this one on https://github.com/logaretm/vee-validate/issues/3448, related to initialValue that affected from conditional rendering, so the dirty flag is not accurate anymore

### Reproduction steps

Stackblitz link: https://stackblitz.com/edit/vee-validate-v4-array-fields-n33ulm?file=src%2FApp.vue
Step:
1. change user data
2. expected: `isFieldDirty('users')` would be true, actual: it is still false, but the `meta.dirty` of the form is true

### Version

Vue.js 3.x and vee-validate 4.x

### What browsers are you seeing the problem on?

- [ ] Firefox
- [X] Chrome
- [ ] Safari
- [ ] Microsoft Edge

### Relevant log output

_No response_

### Demo link

https://stackblitz.com/edit/vee-validate-v4-array-fields-n33ulm?file=src%2FApp.vue

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](CONDUCT.md)

Pull request description:
feat: allow path meta querying for nested fields closes #4575
🔎 __Overview__

Allows querying nested paths meta value by matching fields with starting path.


closes #4575 


Patch:
diff --git a/.changeset/shy-scissors-hug.md b/.changeset/shy-scissors-hug.md
new file mode 100644
index 00000000..a9f4387f
--- /dev/null
+++ b/.changeset/shy-scissors-hug.md
@@ -0,0 +1,5 @@
+---
+"vee-validate": patch
+---
+
+feat: allow path meta querying for nested fields closes #4575
diff --git a/docs/src/pages/examples/checkboxes-and-radio.mdx b/docs/src/pages/examples/checkboxes-and-radio.mdx
index dad88b1d..99d40917 100644
--- a/docs/src/pages/examples/checkboxes-and-radio.mdx
+++ b/docs/src/pages/examples/checkboxes-and-radio.mdx
@@ -27,7 +27,7 @@ The only requirements are that the fields:
 When using `Field` slot props with checkbox/radio components, you still need to provide the `type` and `value` props to the `Field` node itself.
 
 ```vue-html
-<Field v-slot="{ field }" name="terms" type="checkbox" :value="true">
+<Field v-slot="{ field }" name="terms" type="checkbox" :value="true" :unchecked-value="false">
   <label>
     <input type="checkbox" name="terms" v-bind="field" :value="true" />
     I agree
diff --git a/packages/vee-validate/src/useForm.ts b/packages/vee-validate/src/useForm.ts
index 7bc4e2b8..e6092664 100644
--- a/packages/vee-validate/src/useForm.ts
+++ b/packages/vee-validate/src/useForm.ts
@@ -719,15 +719,31 @@ export function useForm<
   }
 
   function isFieldTouched(field: Path<TValues>) {
-    return !!findPathState(field)?.touched;
+    const pathState = findPathState(field);
+    if (pathState) {
+      return pathState.touched;
+    }
+
+    // Find all nested paths and consider their touched state
+    return pathStates.value.filter(s => s.path.startsWith(field)).some(s => s.touched);
   }
 
   function isFieldDirty(field: Path<TValues>) {
-    return !!findPathState(field)?.dirty;
+    const pathState = findPathState(field);
+    if (pathState) {
+      return pathState.dirty;
+    }
+
+    return pathStates.value.filter(s => s.path.startsWith(field)).some(s => s.dirty);
   }
 
   function isFieldValid(field: Path<TValues>) {
-    return !!findPathState(field)?.valid;
+    const pathState = findPathState(field);
+    if (pathState) {
+      return pathState.valid;
+    }
+
+    return pathStates.value.filter(s => s.path.startsWith(field)).every(s => s.valid);
   }
 
   /**


Test patch:
diff --git a/packages/vee-validate/tests/useForm.spec.ts b/packages/vee-validate/tests/useForm.spec.ts
index f0b6cff7..9e95a0ce 100644
--- a/packages/vee-validate/tests/useForm.spec.ts
+++ b/packages/vee-validate/tests/useForm.spec.ts
@@ -1183,6 +1183,8 @@ describe('useForm()', () => {
       setup() {
         form = useForm();
         useField('fname');
+        useField('nested.lname');
+        useField('nested.fname');
 
         return {};
       },
@@ -1192,10 +1194,13 @@ describe('useForm()', () => {
     await flushPromises();
     expect(form.meta.value.touched).toBe(false);
     expect(form.isFieldTouched('fname')).toBe(false);
+    expect(form.isFieldTouched('nested')).toBe(false);
     form.setFieldTouched('fname', true);
+    form.setFieldTouched('nested.lname', true);
     await flushPromises();
     expect(form.meta.value.touched).toBe(true);
     expect(form.isFieldTouched('fname')).toBe(true);
+    expect(form.isFieldTouched('nested')).toBe(true);
   });
 
   test('can query field dirty state', async () => {
@@ -1204,6 +1209,8 @@ describe('useForm()', () => {
       setup() {
         form = useForm();
         useField('fname');
+        useField('nested.lname');
+        useField('nested.fname');
 
         return {};
       },
@@ -1213,10 +1220,13 @@ describe('useForm()', () => {
     await flushPromises();
     expect(form.meta.value.dirty).toBe(false);
     expect(form.isFieldDirty('fname')).toBe(false);
+    expect(form.isFieldDirty('nested')).toBe(false);
     form.setFieldValue('fname', 'value');
+    form.setFieldValue('nested.lname', 'value');
     await flushPromises();
     expect(form.meta.value.dirty).toBe(true);
     expect(form.isFieldDirty('fname')).toBe(true);
+    expect(form.isFieldDirty('nested')).toBe(true);
   });
 
   test('can query field valid state', async () => {
@@ -1225,6 +1235,8 @@ describe('useForm()', () => {
       setup() {
         form = useForm();
         useField('fname');
+        useField('nested.lname');
+        useField('nested.fname');
 
         return {};
       },
@@ -1234,10 +1246,13 @@ describe('useForm()', () => {
     await flushPromises();
     expect(form.meta.value.valid).toBe(true);
     expect(form.isFieldValid('fname')).toBe(true);
+    expect(form.isFieldValid('nested')).toBe(true);
     form.setFieldError('fname', 'ERROR');
+    form.setFieldError('nested.lname', 'ERROR');
     await flushPromises();
     expect(form.meta.value.valid).toBe(false);
     expect(form.isFieldValid('fname')).toBe(false);
+    expect(form.isFieldValid('nested')).toBe(false);
   });
 
   // #4438