Skip to content

Commit ae9c429

Browse files
committed
Implement type safe lenses
1 parent 20d63c2 commit ae9c429

File tree

7 files changed

+185
-31
lines changed

7 files changed

+185
-31
lines changed

apps/food-order/src/model/dishOrder.model.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ const restaurantLens = lens(restaurantsList, $restaurantName);
1313

1414
export const $dish = restaurantLens.dishes.itemStore($dishName);
1515

16-
export const $dishAdditives = restaurantLens.dishes($dishName).additives.store;
16+
export const $dishAdditives = restaurantLens
17+
.dishes($dishName)
18+
.additives.store([]);
1719

1820
export const additivesList = keyval(() => {
1921
const $additive = createStore('');
@@ -23,7 +25,7 @@ export const additivesList = keyval(() => {
2325
$dishAdditives,
2426
$additive,
2527
(additives, additiveName) =>
26-
additives?.find((e) => e.name === additiveName) ?? {
28+
additives.find((e) => e.name === additiveName) ?? {
2729
name: '',
2830
required: false,
2931
options: [],

apps/food-order/src/model/orders.model.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,8 @@ export const ordersList = keyval(() => {
3131
const $additive = createStore('');
3232
const $choice = createStore('');
3333
const $amount = createStore(0);
34-
// const $additiveEntity = lens(restaurantsList, $restaurant)
35-
// .dishes($dish)
36-
// .additives($additive)
37-
// .store;
3834
const $additiveEntity = combine(
39-
dishLens.additives.store,
35+
dishLens.additives.store([]),
4036
$additive,
4137
(items, name) =>
4238
items?.find((e) => e.name === name) ?? {
@@ -70,7 +66,7 @@ export const ordersList = keyval(() => {
7066
};
7167
});
7268
const $totalPrice = combine(
73-
dishLens.price.store,
69+
dishLens.price.store(0),
7470
additivesList.$items,
7571
(price, additives) =>
7672
additives.reduce(

packages/core/src/__tests__/keyval/lens.test.ts

Lines changed: 148 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,84 @@ function createNestedEntities(
5454
return entities;
5555
}
5656

57+
describe('defaultValue behavior', () => {
58+
test('null when no match and no default value', () => {
59+
const setCurrentKey = createEvent<string>();
60+
const entities = createUpdatableEntities([
61+
{ id: 'foo', count: 0, tag: 'x' },
62+
{ id: 'bar', count: 0, tag: 'y' },
63+
{ id: 'baz', count: 0, tag: 'z' },
64+
]);
65+
66+
const $currentKey = createStore('none');
67+
68+
sample({
69+
clock: setCurrentKey,
70+
target: $currentKey,
71+
});
72+
73+
const $currentTag = lens(entities, $currentKey).tag.store();
74+
expect($currentTag.getState()).toBe(null);
75+
setCurrentKey('bar');
76+
expect($currentTag.getState()).toBe('y');
77+
});
78+
test('with defaultValue', () => {
79+
const setCurrentKey = createEvent<string>();
80+
const entities = createUpdatableEntities([
81+
{ id: 'foo', count: 0, tag: 'x' },
82+
{ id: 'bar', count: 0, tag: 'y' },
83+
{ id: 'baz', count: 0, tag: 'z' },
84+
]);
85+
86+
const $currentKey = createStore('none');
87+
88+
sample({
89+
clock: setCurrentKey,
90+
target: $currentKey,
91+
});
92+
93+
const $currentTag = lens(entities, $currentKey).tag.store('missed value');
94+
expect($currentTag.getState()).toBe('missed value');
95+
setCurrentKey('bar');
96+
expect($currentTag.getState()).toBe('y');
97+
});
98+
test('itemStore default is model default values', () => {
99+
const setCurrentKeyB = createEvent<string>();
100+
const entities = createNestedEntities([
101+
{
102+
id: 'foo',
103+
childs: [{ id: 'foo1', count: 0 }],
104+
},
105+
{
106+
id: 'bar',
107+
childs: [
108+
{ id: 'bar1', count: 1 },
109+
{ id: 'bar2', count: 2 },
110+
],
111+
},
112+
]);
113+
114+
const $currentKeyA = createStore('bar');
115+
const $currentKeyB = createStore('none');
116+
117+
sample({
118+
clock: setCurrentKeyB,
119+
target: $currentKeyB,
120+
});
121+
122+
const $child = lens(entities, $currentKeyA).childs.itemStore($currentKeyB);
123+
expect($child.getState()).toEqual({
124+
id: '',
125+
count: 0,
126+
});
127+
setCurrentKeyB('bar2');
128+
expect($child.getState()).toEqual({
129+
id: 'bar2',
130+
count: 2,
131+
});
132+
});
133+
});
134+
57135
describe('lens read value of field in keyval', () => {
58136
test('with store', () => {
59137
const entities = createUpdatableEntities([
@@ -64,7 +142,7 @@ describe('lens read value of field in keyval', () => {
64142

65143
const $currentKey = createStore('bar');
66144

67-
const $currentTag = lens(entities, $currentKey).tag.store;
145+
const $currentTag = lens(entities, $currentKey).tag.store();
68146
expect($currentTag.getState()).toBe('y');
69147
});
70148
test('with constant', () => {
@@ -73,7 +151,7 @@ describe('lens read value of field in keyval', () => {
73151
{ id: 'bar', count: 0, tag: 'y' },
74152
]);
75153

76-
const $currentTag = lens(entities, 'bar').tag.store;
154+
const $currentTag = lens(entities, 'bar').tag.store();
77155
expect($currentTag.getState()).toBe('y');
78156
});
79157
});
@@ -84,7 +162,7 @@ test('lens store change value when entity changed', () => {
84162
{ id: 'bar', count: 0, tag: 'y' },
85163
]);
86164

87-
const $currentTag = lens(entities, 'bar').tag.store;
165+
const $currentTag = lens(entities, 'bar').tag.store();
88166
expect($currentTag.getState()).toBe('y');
89167

90168
entities.edit.update({ id: 'bar', tag: 'z' });
@@ -101,7 +179,7 @@ test('lens store change value when key changed', () => {
101179
const $currentKey = createStore('bar');
102180
sample({ clock: changeKey, target: $currentKey });
103181

104-
const $currentTag = lens(entities, $currentKey).tag.store;
182+
const $currentTag = lens(entities, $currentKey).tag.store();
105183
expect($currentTag.getState()).toBe('y');
106184

107185
changeKey('foo');
@@ -128,8 +206,9 @@ describe('nested store lens', () => {
128206
const $currentKeyA = createStore('bar');
129207
const $currentKeyB = createStore('bar2');
130208

131-
const $currentCount = lens(entities, $currentKeyA).childs($currentKeyB)
132-
.count.store;
209+
const $currentCount = lens(entities, $currentKeyA)
210+
.childs($currentKeyB)
211+
.count.store();
133212
expect($currentCount.getState()).toBe(2);
134213
});
135214
test('with constant', () => {
@@ -149,8 +228,9 @@ describe('nested store lens', () => {
149228

150229
const $currentKey = createStore('bar2');
151230

152-
const $currentCount = lens(entities, 'bar').childs($currentKey).count
153-
.store;
231+
const $currentCount = lens(entities, 'bar')
232+
.childs($currentKey)
233+
.count.store();
154234
expect($currentCount.getState()).toBe(2);
155235
});
156236
});
@@ -172,8 +252,9 @@ describe('nested store lens', () => {
172252
const $currentKeyA = createStore('bar');
173253
const $currentKeyB = createStore('bar1');
174254

175-
const $currentCount = lens(entities, $currentKeyA).childs($currentKeyB)
176-
.count.store;
255+
const $currentCount = lens(entities, $currentKeyA)
256+
.childs($currentKeyB)
257+
.count.store();
177258
expect($currentCount.getState()).toBe(1);
178259

179260
entities.api.updateCount({
@@ -208,11 +289,66 @@ describe('nested store lens', () => {
208289

209290
$currentKeyB.on(updateKeyB, (_, upd) => upd);
210291

211-
const $currentCount = lens(entities, $currentKeyA).childs($currentKeyB)
212-
.count.store;
292+
const $currentCount = lens(entities, $currentKeyA)
293+
.childs($currentKeyB)
294+
.count.store();
213295
expect($currentCount.getState()).toBe(1);
214296

215297
updateKeyB('bar2');
216298
expect($currentCount.getState()).toBe(2);
217299
});
300+
301+
test('itemStore return store with all model fields in one object', () => {
302+
const entities = createNestedEntities([
303+
{
304+
id: 'foo',
305+
childs: [{ id: 'foo1', count: 0 }],
306+
},
307+
{
308+
id: 'bar',
309+
childs: [
310+
{ id: 'bar1', count: 1 },
311+
{ id: 'bar2', count: 2 },
312+
],
313+
},
314+
]);
315+
316+
const $currentKeyA = createStore('bar');
317+
const $currentKeyB = createStore('bar2');
318+
319+
const $child = lens(entities, $currentKeyA).childs.itemStore($currentKeyB);
320+
expect($child.getState()).toEqual({
321+
id: 'bar2',
322+
count: 2,
323+
});
324+
});
325+
test('has return store which show that item exists', () => {
326+
const setCurrentKeyB = createEvent<string>();
327+
const entities = createNestedEntities([
328+
{
329+
id: 'foo',
330+
childs: [{ id: 'foo1', count: 0 }],
331+
},
332+
{
333+
id: 'bar',
334+
childs: [
335+
{ id: 'bar1', count: 1 },
336+
{ id: 'bar2', count: 2 },
337+
],
338+
},
339+
]);
340+
341+
const $currentKeyA = createStore('bar');
342+
const $currentKeyB = createStore('none');
343+
344+
sample({
345+
clock: setCurrentKeyB,
346+
target: $currentKeyB,
347+
});
348+
349+
const $hasChild = lens(entities, $currentKeyA).childs.has($currentKeyB);
350+
expect($hasChild.getState()).toBe(false);
351+
setCurrentKeyB('bar2');
352+
expect($hasChild.getState()).toBe(true);
353+
});
218354
});

packages/core/src/example.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const fieldList1 = keyval(() => {
6969

7070
fieldList1.api.submit;
7171

72-
lens(fieldList1, $email).isValid.store;
72+
lens(fieldList1, $email).isValid.store();
7373

7474
const fieldList2 = keyval(() => {
7575
const $name = createStore('');

packages/core/src/keyval.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ export function keyval<Input, ModelEnhance, Api, Shape>(
541541
type: 'structKeyval',
542542
getKey,
543543
shape: kvModel.__struct!.shape,
544+
defaultItem: kvModel.defaultState ?? null,
544545
} as StructKeyval;
545546
for (const prop in instance.api) {
546547
const evt = createEvent<
@@ -618,6 +619,8 @@ export function keyval<Input, ModelEnhance, Api, Shape>(
618619
type: 'structKeyval',
619620
getKey,
620621
shape: itemStructShape,
622+
// TODO add support for .itemStore
623+
defaultItem: null,
621624
} as StructKeyval;
622625
// for (const key in shape) {
623626
// const def = shape[key] as OneOfShapeDef;

0 commit comments

Comments
 (0)