Skip to content

Commit 4920902

Browse files
authored
feat: support nested object in reflect (#46)
1 parent 276db2a commit 4920902

File tree

3 files changed

+150
-3
lines changed

3 files changed

+150
-3
lines changed

src/LensCore.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,15 @@ export class LensCore<T extends FieldValues> {
6565

6666
return result;
6767
} else if (this.override) {
68-
const overriddenLens: LensCore<T> | undefined = get(this.override, propString);
68+
const overriddenLensOrNested = get(this.override, propString);
69+
70+
let overriddenLens: LensCore<T> | undefined;
71+
72+
if (typeof overriddenLensOrNested?.reflect === 'function') {
73+
overriddenLens = overriddenLensOrNested;
74+
} else if (overriddenLensOrNested) {
75+
overriddenLens = this.reflect(() => overriddenLensOrNested);
76+
}
6977

7078
if (!overriddenLens) {
7179
const result = new (this.constructor as typeof LensCore)(this.control, nestedPath, this.cache);

src/types/Lens.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ export type LensesDictionary<T> = {
4545
[P in keyof T]: Lens<T[P]>;
4646
};
4747

48-
export type LensesGetter<T> = LensesDictionary<T> | Lens<T>;
48+
export type RecursiveLensesDictionary<T> = {
49+
[P in keyof T]: Lens<T[P]> | RecursiveLensesDictionary<T[P]>;
50+
};
51+
52+
export type LensesGetter<T> = RecursiveLensesDictionary<T> | Lens<T>;
4953

5054
export type UnwrapLens<T> = T extends BrowserNativeObject
5155
? T

tests/object-reflect.test.ts

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ test('non lens fields cannot returned from reflect', () => {
4141
return lens;
4242
});
4343

44-
// @ts-expect-error non lens fields cannot be returned from reflect
4544
assertType(result.current.reflect((_, l) => ({ b: l.focus('a'), w: 'hello' })));
4645
});
4746

@@ -223,3 +222,139 @@ test('reflected lens handles duplicate key names in different nesting levels', (
223222

224223
expect(reflectedWithDuplicateKeys.focus('nest_id').interop().name).toBe('nest.id');
225224
});
225+
226+
test('reflected lens with nested objects resolves correct paths', () => {
227+
type Input = {
228+
password: {
229+
password: string;
230+
passwordConfirm: string;
231+
};
232+
usernameNest: {
233+
name: string;
234+
};
235+
};
236+
237+
type DataLens = {
238+
password: {
239+
password_base: string;
240+
password_confirm: string;
241+
};
242+
nest2: {
243+
names: {
244+
name: string;
245+
};
246+
};
247+
};
248+
249+
const { result } = renderHook(() => {
250+
const form = useForm<Input>();
251+
const lens = useLens({ control: form.control });
252+
return lens;
253+
});
254+
255+
const lens = result.current;
256+
257+
const reflected: Lens<DataLens> = lens.reflect((_, l) => ({
258+
password: {
259+
password_base: l.focus('password.password'),
260+
password_confirm: l.focus('password.passwordConfirm'),
261+
},
262+
nest2: {
263+
names: l.focus('usernameNest'),
264+
},
265+
}));
266+
267+
expect(reflected.focus('password.password_base').interop().name).toBe('password.password');
268+
expect(reflected.focus('password.password_confirm').interop().name).toBe('password.passwordConfirm');
269+
expect(reflected.focus('nest2.names.name').interop().name).toBe('usernameNest.name');
270+
});
271+
272+
test('reflected lenses without focus do not append paths', () => {
273+
type Input = {
274+
a: {
275+
b: {
276+
c: string;
277+
};
278+
};
279+
};
280+
281+
type Result = {
282+
x: {
283+
y: string;
284+
};
285+
};
286+
287+
const { result } = renderHook(() => {
288+
const form = useForm<Input>();
289+
const lens = useLens({ control: form.control });
290+
return lens;
291+
});
292+
293+
const lens = result.current;
294+
295+
const reflected: Lens<Result> = lens.reflect((_, l) => ({
296+
x: l.reflect((_, l2) => {
297+
return {
298+
y: l2.focus('a.b.c'),
299+
};
300+
}),
301+
}));
302+
303+
expect(reflected.focus('x.y').interop().name).toBe('a.b.c');
304+
});
305+
306+
test('nested object reflect does not append paths', () => {
307+
type Input = {
308+
a: {
309+
b: {
310+
c: string;
311+
};
312+
};
313+
};
314+
315+
type Result = {
316+
x: {
317+
y: string;
318+
};
319+
};
320+
321+
const { result } = renderHook(() => {
322+
const form = useForm<Input>();
323+
const lens = useLens({ control: form.control });
324+
return lens;
325+
});
326+
327+
const lens = result.current;
328+
329+
const reflected: Lens<Result> = lens.reflect((_, l) => ({
330+
x: {
331+
y: l.focus('a.b.c'),
332+
},
333+
}));
334+
335+
expect(reflected.focus('x.y').interop().name).toBe('a.b.c');
336+
});
337+
338+
test('reflect with nested object preserve correct path', () => {
339+
const { result } = renderHook(() => {
340+
const form = useForm<{ values: { a: string } }>();
341+
const lens = useLens({ control: form.control });
342+
return lens;
343+
});
344+
345+
const lens = result.current;
346+
347+
const reflected = lens.focus('values').reflect((_, l) => {
348+
return {
349+
nested: {
350+
deeper: {
351+
field: l.focus('a'),
352+
},
353+
},
354+
};
355+
});
356+
357+
expect(reflected.focus('nested').interop().name).toBe('values');
358+
expect(reflected.focus('nested.deeper').interop().name).toBe('values');
359+
expect(reflected.focus('nested.deeper.field').interop().name).toBe('values.a');
360+
});

0 commit comments

Comments
 (0)