Skip to content

Commit e442fe6

Browse files
authored
fix: support dot notation in reflected lens
1 parent 35eb6f8 commit e442fe6

File tree

2 files changed

+147
-2
lines changed

2 files changed

+147
-2
lines changed

src/LensCore.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type Control, type FieldValues, get, set } from 'react-hook-form';
22

3-
import type { LensesStorage, LensesStorageComplexKey } from './LensesStorage';
3+
import { LensesStorage, type LensesStorageComplexKey } from './LensesStorage';
44
import type { Lens } from './types';
55

66
export interface LensCoreInteropBinding<T extends FieldValues> {
@@ -38,6 +38,15 @@ export class LensCore<T extends FieldValues> {
3838

3939
public focus(prop: string | number): LensCore<T> {
4040
const propString = prop.toString();
41+
42+
if (typeof prop === 'string' && prop.includes('.')) {
43+
const dotIndex = prop.indexOf('.');
44+
const firstSegment = prop.slice(0, dotIndex);
45+
const remainingPath = prop.slice(dotIndex + 1);
46+
47+
return this.focus(firstSegment).focus(remainingPath);
48+
}
49+
4150
const nestedPath = this.path ? `${this.path}.${propString}` : propString;
4251

4352
const fromCache = this.cache?.get(nestedPath, this.reflectedKey);
@@ -92,7 +101,8 @@ export class LensCore<T extends FieldValues> {
92101
return fromCache;
93102
}
94103

95-
const template = new LensCore(this.control, this.path, this.cache);
104+
const nestedCache = new LensesStorage(this.control);
105+
const template = new LensCore(this.control, this.path, nestedCache);
96106

97107
const dictionary = new Proxy(
98108
{},

tests/object-reflect.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,138 @@ test('reflect return an object contains Date, File, FileList', () => {
8888
}>
8989
>();
9090
});
91+
92+
test('basic lens focus with dot notation works correctly', () => {
93+
const { result } = renderHook(() => {
94+
const form = useForm<{
95+
password: { password: string; passwordConfirm: string };
96+
usernameNest: { name: string };
97+
}>();
98+
const lens = useLens({ control: form.control });
99+
return lens;
100+
});
101+
102+
const lens = result.current;
103+
104+
expect(lens.focus('password.password').interop().name).toBe('password.password');
105+
expect(lens.focus('usernameNest.name').interop().name).toBe('usernameNest.name');
106+
});
107+
108+
test('reflected lens with chained focus calls works correctly', () => {
109+
type Input = {
110+
name: string;
111+
password: {
112+
password: string;
113+
passwordConfirm: string;
114+
};
115+
usernameNest: {
116+
name: string;
117+
};
118+
};
119+
120+
type DataLens = {
121+
userName: string;
122+
password: {
123+
password_base: string;
124+
password_confirm: string;
125+
};
126+
nest2: {
127+
names: {
128+
name: string;
129+
};
130+
};
131+
};
132+
133+
const { result } = renderHook(() => {
134+
const form = useForm<Input>();
135+
const lens = useLens({ control: form.control });
136+
return lens;
137+
});
138+
139+
const lens = result.current;
140+
141+
const reflected: Lens<DataLens> = lens.reflect((dic, l) => ({
142+
userName: dic.name,
143+
password: lens.focus('password').reflect<DataLens['password']>((pas) => ({
144+
password_base: pas.password,
145+
password_confirm: pas.passwordConfirm,
146+
})),
147+
nest2: lens.reflect<DataLens['nest2']>(() => ({
148+
names: l.focus('usernameNest'),
149+
})),
150+
}));
151+
152+
expect(reflected.focus('password').focus('password_base').interop().name).toBe('password.password');
153+
expect(reflected.focus('password').focus('password_confirm').interop().name).toBe('password.passwordConfirm');
154+
expect(reflected.focus('nest2').focus('names').focus('name').interop().name).toBe('usernameNest.name');
155+
});
156+
157+
test('reflected lens with dot notation resolves correct paths', () => {
158+
type Input = {
159+
password: {
160+
password: string;
161+
passwordConfirm: string;
162+
};
163+
usernameNest: {
164+
name: string;
165+
};
166+
};
167+
168+
type DataLens = {
169+
password: {
170+
password_base: string;
171+
password_confirm: string;
172+
};
173+
nest2: {
174+
names: {
175+
name: string;
176+
};
177+
};
178+
};
179+
180+
const { result } = renderHook(() => {
181+
const form = useForm<Input>();
182+
const lens = useLens({ control: form.control });
183+
return lens;
184+
});
185+
186+
const lens = result.current;
187+
188+
const reflected: Lens<DataLens> = lens.reflect((_, l) => ({
189+
password: lens.focus('password').reflect<DataLens['password']>((pas) => ({
190+
password_base: pas.password,
191+
password_confirm: pas.passwordConfirm,
192+
})),
193+
nest2: lens.reflect<DataLens['nest2']>(() => ({
194+
names: l.focus('usernameNest'),
195+
})),
196+
}));
197+
198+
expect(reflected.focus('password.password_base').interop().name).toBe('password.password');
199+
expect(reflected.focus('password.password_confirm').interop().name).toBe('password.passwordConfirm');
200+
expect(reflected.focus('nest2.names.name').interop().name).toBe('usernameNest.name');
201+
});
202+
203+
test('reflected lens handles duplicate key names in different nesting levels', () => {
204+
type Input = {
205+
id: string;
206+
nest: {
207+
id: string;
208+
};
209+
};
210+
211+
const { result } = renderHook(() => {
212+
const form = useForm<Input>();
213+
const lens = useLens({ control: form.control });
214+
return lens;
215+
});
216+
217+
const lens = result.current;
218+
219+
const reflectedWithDuplicateKeys = lens.reflect((_, l) => ({
220+
id: l.focus('id'),
221+
nest_id: l.focus('nest').focus('id'),
222+
}));
223+
224+
expect(reflectedWithDuplicateKeys.focus('nest_id').interop().name).toBe('nest.id');
225+
});

0 commit comments

Comments
 (0)