@_linked/react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.context/jest-repro-bundler.config.js +20 -0
  2. package/.context/jest-repro.config.js +20 -0
  3. package/.context/notes.md +0 -0
  4. package/.context/todos.md +0 -0
  5. package/.context/tsconfig-repro-bundler.json +14 -0
  6. package/.context/tsconfig-repro-no-paths.json +12 -0
  7. package/.context/tsconfig-repro-node-modules-paths.json +16 -0
  8. package/.context/tsconfig-repro-node16.json +14 -0
  9. package/AGENTS.md +59 -0
  10. package/LICENSE +21 -0
  11. package/README.md +250 -0
  12. package/docs/001-react-extraction.md +361 -0
  13. package/jest.config.js +20 -0
  14. package/lib/cjs/index.d.ts +4 -0
  15. package/lib/cjs/index.js +21 -0
  16. package/lib/cjs/index.js.map +1 -0
  17. package/lib/cjs/package.d.ts +10 -0
  18. package/lib/cjs/package.js +33 -0
  19. package/lib/cjs/package.js.map +1 -0
  20. package/lib/cjs/package.json +3 -0
  21. package/lib/cjs/utils/Hooks.d.ts +5 -0
  22. package/lib/cjs/utils/Hooks.js +54 -0
  23. package/lib/cjs/utils/Hooks.js.map +1 -0
  24. package/lib/cjs/utils/LinkedComponent.d.ts +52 -0
  25. package/lib/cjs/utils/LinkedComponent.js +322 -0
  26. package/lib/cjs/utils/LinkedComponent.js.map +1 -0
  27. package/lib/cjs/utils/LinkedComponentClass.d.ts +11 -0
  28. package/lib/cjs/utils/LinkedComponentClass.js +34 -0
  29. package/lib/cjs/utils/LinkedComponentClass.js.map +1 -0
  30. package/lib/esm/index.d.ts +4 -0
  31. package/lib/esm/index.js +5 -0
  32. package/lib/esm/index.js.map +1 -0
  33. package/lib/esm/package.d.ts +10 -0
  34. package/lib/esm/package.js +22 -0
  35. package/lib/esm/package.js.map +1 -0
  36. package/lib/esm/package.json +3 -0
  37. package/lib/esm/utils/Hooks.d.ts +5 -0
  38. package/lib/esm/utils/Hooks.js +50 -0
  39. package/lib/esm/utils/Hooks.js.map +1 -0
  40. package/lib/esm/utils/LinkedComponent.d.ts +52 -0
  41. package/lib/esm/utils/LinkedComponent.js +284 -0
  42. package/lib/esm/utils/LinkedComponent.js.map +1 -0
  43. package/lib/esm/utils/LinkedComponentClass.d.ts +11 -0
  44. package/lib/esm/utils/LinkedComponentClass.js +27 -0
  45. package/lib/esm/utils/LinkedComponentClass.js.map +1 -0
  46. package/package.json +57 -0
  47. package/scripts/dual-package.js +25 -0
  48. package/src/index.ts +4 -0
  49. package/src/package.ts +62 -0
  50. package/src/tests/react-component-behavior.test.tsx +578 -0
  51. package/src/tests/react-component-integration.test.tsx +378 -0
  52. package/src/utils/Hooks.ts +56 -0
  53. package/src/utils/LinkedComponent.ts +545 -0
  54. package/src/utils/LinkedComponentClass.tsx +37 -0
  55. package/tsconfig-cjs.json +8 -0
  56. package/tsconfig-esm.json +8 -0
  57. package/tsconfig-test.json +15 -0
  58. package/tsconfig.json +29 -0
@@ -0,0 +1,378 @@
1
+ import {describe, expect, test, beforeAll, afterEach} from '@jest/globals';
2
+ import React from 'react';
3
+ import {render, waitFor, cleanup} from '@testing-library/react';
4
+ import {linkedComponent, linkedSetComponent} from '../package.js';
5
+ import {Shape} from '@_linked/core/shapes/Shape';
6
+ import {ShapeSet} from '@_linked/core/collections/ShapeSet';
7
+ import {SelectQueryFactory} from '@_linked/core/queries/SelectQuery';
8
+ import {IQueryParser} from '@_linked/core/interfaces/IQueryParser';
9
+ import {UpdatePartial, NodeReferenceValue} from '@_linked/core/queries/QueryFactory';
10
+ import {CreateResponse} from '@_linked/core/queries/CreateQuery';
11
+ import {DeleteResponse} from '@_linked/core/queries/DeleteQuery';
12
+ import {NodeId} from '@_linked/core/queries/MutationQuery';
13
+ import {setDefaultPageLimit} from '@_linked/core/utils/Package';
14
+ import {LinkedStorage} from '@_linked/core/utils/LinkedStorage';
15
+ import {
16
+ Person,
17
+ Pet,
18
+ Dog,
19
+ tmpEntityBase,
20
+ name,
21
+ hobby,
22
+ bestFriend,
23
+ hasFriend,
24
+ hasPet,
25
+ personClass,
26
+ dogClass,
27
+ guardDogLevel,
28
+ } from '@_linked/core/test-helpers/query-fixtures';
29
+ import {rdf} from '@_linked/core/ontologies/rdf';
30
+ import {xsd} from '@_linked/core/ontologies/xsd';
31
+ import {InMemoryStore, Literal, NamedNode, toNamedNode} from '@_linked/rdf-mem-store';
32
+
33
+ class StoreQueryParser implements IQueryParser {
34
+ constructor(private readonly store: InMemoryStore) {}
35
+
36
+ async selectQuery<ShapeType extends Shape, ResponseType, Source, ResultType>(
37
+ query: SelectQueryFactory<ShapeType, ResponseType, Source>,
38
+ ): Promise<ResultType> {
39
+ return this.store.selectQuery(query.getQueryObject() as any) as Promise<ResultType>;
40
+ }
41
+
42
+ async updateQuery<ShapeType extends Shape, U extends UpdatePartial<ShapeType>>(
43
+ _id: string | NodeReferenceValue,
44
+ _updateObjectOrFn: U,
45
+ _shapeClass: typeof Shape,
46
+ ) {
47
+ return Promise.reject(
48
+ new Error('updateQuery is not used in this react integration test'),
49
+ ) as any;
50
+ }
51
+
52
+ async createQuery<ShapeType extends Shape, U extends UpdatePartial<ShapeType>>(
53
+ _updateObjectOrFn: U,
54
+ _shapeClass: typeof Shape,
55
+ ): Promise<CreateResponse<U>> {
56
+ return Promise.reject(
57
+ new Error('createQuery is not used in this react integration test'),
58
+ ) as any;
59
+ }
60
+
61
+ async deleteQuery(
62
+ _id: NodeId | NodeId[] | NodeReferenceValue[],
63
+ _shapeClass: typeof Shape,
64
+ ): Promise<DeleteResponse> {
65
+ return Promise.reject(
66
+ new Error('deleteQuery is not used in this react integration test'),
67
+ ) as any;
68
+ }
69
+ }
70
+
71
+ const p1Uri = `${tmpEntityBase}p1-semmy`;
72
+ const p2Uri = `${tmpEntityBase}p2-moa`;
73
+ const p3Uri = `${tmpEntityBase}p3-jinx`;
74
+ const p4Uri = `${tmpEntityBase}p4-quinn`;
75
+ const dog1Uri = `${tmpEntityBase}dog1`;
76
+ const dog2Uri = `${tmpEntityBase}dog2`;
77
+
78
+ function seedLegacyReactData() {
79
+ const rdfType = toNamedNode(rdf.type);
80
+ const personType = toNamedNode(personClass);
81
+ const dogType = toNamedNode(dogClass);
82
+
83
+ const nameNode = toNamedNode(name);
84
+ const hobbyNode = toNamedNode(hobby);
85
+ const bestFriendNode = toNamedNode(bestFriend);
86
+ const hasFriendNode = toNamedNode(hasFriend);
87
+ const hasPetNode = toNamedNode(hasPet);
88
+ const guardDogLevelNode = toNamedNode(guardDogLevel);
89
+
90
+ const p1 = NamedNode.getOrCreate(p1Uri);
91
+ const p2 = NamedNode.getOrCreate(p2Uri);
92
+ const p3 = NamedNode.getOrCreate(p3Uri);
93
+ const p4 = NamedNode.getOrCreate(p4Uri);
94
+
95
+ p1.set(rdfType, personType);
96
+ p2.set(rdfType, personType);
97
+ p3.set(rdfType, personType);
98
+ p4.set(rdfType, personType);
99
+
100
+ p1.set(nameNode, new Literal('Semmy'));
101
+ p2.set(nameNode, new Literal('Moa'));
102
+ p3.set(nameNode, new Literal('Jinx'));
103
+ p4.set(nameNode, new Literal('Quinn'));
104
+
105
+ p2.set(hobbyNode, new Literal('Jogging'));
106
+
107
+ p1.set(hasFriendNode, p2);
108
+ p1.set(hasFriendNode, p3);
109
+ p2.set(hasFriendNode, p3);
110
+ p2.set(hasFriendNode, p4);
111
+ p2.set(bestFriendNode, p3);
112
+
113
+ const dog1 = NamedNode.getOrCreate(dog1Uri);
114
+ const dog2 = NamedNode.getOrCreate(dog2Uri);
115
+ dog1.set(rdfType, dogType);
116
+ dog2.set(rdfType, dogType);
117
+ dog1.set(guardDogLevelNode, new Literal('2', toNamedNode(xsd.integer)));
118
+
119
+ p1.set(hasPetNode, dog1);
120
+ p2.set(hasPetNode, dog2);
121
+ }
122
+
123
+ beforeAll(() => {
124
+ const store = new InMemoryStore();
125
+ const parser = new StoreQueryParser(store);
126
+
127
+ LinkedStorage.setDefaultStore(store);
128
+ Person.queryParser = parser;
129
+ Pet.queryParser = parser;
130
+ Dog.queryParser = parser;
131
+
132
+ seedLegacyReactData();
133
+ });
134
+
135
+ afterEach(() => {
136
+ cleanup();
137
+ setDefaultPageLimit(12);
138
+ });
139
+
140
+ describe('React component integration', () => {
141
+ test('component with single property query', async () => {
142
+ const Component = linkedComponent(
143
+ Person.query((p) => p.name),
144
+ ({name}) => {
145
+ return <div>{name}</div>;
146
+ },
147
+ );
148
+
149
+ const component = render(<Component of={{id: p1Uri}} />);
150
+
151
+ await waitFor(() => expect(component.getByText('Semmy')).toBeTruthy(), {
152
+ timeout: 5000,
153
+ interval: 50,
154
+ });
155
+ });
156
+
157
+ test('component with where query', async () => {
158
+ const query = Person.query((p) => p.friends.where((f) => f.name.equals('Jinx')).name);
159
+
160
+ const Component2 = linkedComponent(
161
+ query,
162
+ ({friends}) => {
163
+ return <div>{friends[0].name}</div>;
164
+ },
165
+ );
166
+
167
+ const component = render(<Component2 of={{id: p1Uri}} />);
168
+ await waitFor(() => expect(component.getByText('Jinx')).toBeTruthy());
169
+ });
170
+
171
+ test('component with custom props', async () => {
172
+ const query = Person.query((p) => p.friends.where((f) => f.name.equals('Jinx')).name);
173
+
174
+ const ComponentWithCustomProps = linkedComponent<typeof query, {custom1: boolean}>(
175
+ query,
176
+ ({friends, custom1}) => {
177
+ return (
178
+ <div>
179
+ <span>{friends[0].name}</span>
180
+ <span>{custom1.toString()}</span>
181
+ </div>
182
+ );
183
+ },
184
+ );
185
+
186
+ const component = render(
187
+ <ComponentWithCustomProps of={{id: p1Uri}} custom1={true} />,
188
+ );
189
+ await waitFor(() => expect(component.getByText('Jinx')).toBeTruthy());
190
+ await waitFor(() => expect(component.getByText('true')).toBeTruthy());
191
+ });
192
+
193
+ test('component requesting data from child components', async () => {
194
+ const childQuery = Person.query((p) => p.name);
195
+
196
+ const ChildComponent = linkedComponent(childQuery, ({name}) => {
197
+ return <span>{name}</span>;
198
+ });
199
+
200
+ const parentQuery = Person.query((p) => {
201
+ return [p.hobby, p.bestFriend.preloadFor(ChildComponent)];
202
+ });
203
+
204
+ const ParentComponent = linkedComponent(parentQuery, ({hobby, bestFriend}) => {
205
+ return (
206
+ <>
207
+ <span>{hobby.toString()}</span>
208
+ <ChildComponent of={bestFriend} />
209
+ </>
210
+ );
211
+ });
212
+
213
+ const component = render(<ParentComponent of={{id: p2Uri}} />);
214
+ await waitFor(() => expect(component.getByText('Jinx')).toBeTruthy());
215
+ await waitFor(() => expect(component.getByText('Jogging')).toBeTruthy());
216
+ });
217
+
218
+ test('linked set components', async () => {
219
+ const NameList = linkedSetComponent(
220
+ Person.query((person) => [person.name, person.hobby]),
221
+ ({linkedData}) => {
222
+ const persons = linkedData;
223
+ return (
224
+ <ul>
225
+ {persons.map((person) => {
226
+ return (
227
+ <li key={person.id}>
228
+ <span>{person.name}</span>
229
+ <span>{person.hobby}</span>
230
+ </li>
231
+ );
232
+ })}
233
+ </ul>
234
+ );
235
+ },
236
+ );
237
+ const persons = new ShapeSet([
238
+ new Person({id: p1Uri}),
239
+ new Person({id: p2Uri}),
240
+ new Person({id: p3Uri}),
241
+ new Person({id: p4Uri}),
242
+ ]);
243
+
244
+ const component = render(<NameList of={persons} />);
245
+ await waitFor(() => {
246
+ persons.forEach((person) => {
247
+ expect(component.getByText(person.id === p1Uri ? 'Semmy' : person.id === p2Uri ? 'Moa' : person.id === p3Uri ? 'Jinx' : 'Quinn')).toBeTruthy();
248
+ });
249
+ expect(component.getByText('Jogging')).toBeTruthy();
250
+ });
251
+ });
252
+
253
+ test('linked set components without source', async () => {
254
+ const NameList = linkedSetComponent(
255
+ Person.query((person) => [person.name, person.hobby]),
256
+ ({linkedData}) => {
257
+ const persons = linkedData;
258
+ return (
259
+ <ul>
260
+ {persons.map((person) => {
261
+ return (
262
+ <li key={person.id}>
263
+ <span>{person.name}</span>
264
+ <span>{person.hobby}</span>
265
+ </li>
266
+ );
267
+ })}
268
+ </ul>
269
+ );
270
+ },
271
+ );
272
+
273
+ const component = render(<NameList />);
274
+ await waitFor(() => {
275
+ expect(component.getByText('Semmy')).toBeTruthy();
276
+ expect(component.getByText('Moa')).toBeTruthy();
277
+ expect(component.getByText('Jinx')).toBeTruthy();
278
+ expect(component.getByText('Quinn')).toBeTruthy();
279
+ expect(component.getByText('Jogging')).toBeTruthy();
280
+ });
281
+ });
282
+
283
+ test('linked set components with named data prop', async () => {
284
+ const query = Person.query((person) => [person.name, person.hobby]);
285
+ const NameList = linkedSetComponent({persons: query}, ({persons}) => {
286
+ return (
287
+ <ul>
288
+ {persons.map((person) => {
289
+ return (
290
+ <li key={person.id}>
291
+ <span>{person.name}</span>
292
+ <span>{person.hobby}</span>
293
+ </li>
294
+ );
295
+ })}
296
+ </ul>
297
+ );
298
+ });
299
+
300
+ const component = render(<NameList />);
301
+ await waitFor(() => {
302
+ expect(component.getByText('Semmy')).toBeTruthy();
303
+ expect(component.getByText('Moa')).toBeTruthy();
304
+ expect(component.getByText('Jinx')).toBeTruthy();
305
+ expect(component.getByText('Quinn')).toBeTruthy();
306
+ expect(component.getByText('Jogging')).toBeTruthy();
307
+ });
308
+ });
309
+
310
+ test('linked set components rendered by linked component', async () => {
311
+ const query = Person.query((person) => [person.name, person.hobby]);
312
+ const NameList = linkedSetComponent({persons: query}, ({persons}) => {
313
+ return (
314
+ <ul>
315
+ {persons.map((person) => {
316
+ return (
317
+ <li key={person.id}>
318
+ <span>{person.name}</span>
319
+ <span>{person.hobby}</span>
320
+ </li>
321
+ );
322
+ })}
323
+ </ul>
324
+ );
325
+ });
326
+
327
+ const PersonFriends = linkedComponent(
328
+ Person.query((p) => {
329
+ return [p.name, p.friends.preloadFor(NameList)];
330
+ }),
331
+ ({name, friends}) => {
332
+ return (
333
+ <div>
334
+ <span>{name}</span>
335
+ <NameList of={friends} />
336
+ </div>
337
+ );
338
+ },
339
+ );
340
+
341
+ const component = render(<PersonFriends of={{id: p1Uri}} />);
342
+ await waitFor(() => {
343
+ expect(component.getByText('Semmy')).toBeTruthy();
344
+ expect(component.getByText('Moa')).toBeTruthy();
345
+ expect(component.getByText('Jogging')).toBeTruthy();
346
+ expect(component.getByText('Jinx')).toBeTruthy();
347
+ });
348
+ });
349
+
350
+ test('linked set component with default page limit', async () => {
351
+ setDefaultPageLimit(2);
352
+
353
+ const NameList = linkedSetComponent(
354
+ Person.query((person) => [person.name, person.hobby]),
355
+ ({linkedData}) => {
356
+ const persons = linkedData;
357
+ return (
358
+ <ul>
359
+ {persons.map((person) => {
360
+ return (
361
+ <li key={person.id}>
362
+ <span role="name">{person.name}</span>
363
+ </li>
364
+ );
365
+ })}
366
+ </ul>
367
+ );
368
+ },
369
+ );
370
+
371
+ const component = render(<NameList />);
372
+ await waitFor(() => {
373
+ expect(component.getAllByRole('name').length).toBe(2);
374
+ expect(component.getByText('Semmy')).toBeTruthy();
375
+ expect(component.getByText('Moa')).toBeTruthy();
376
+ });
377
+ });
378
+ });
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Merge className/style props with local style inputs.
5
+ */
6
+ export const useStyles = (
7
+ props,
8
+ classNamesOrStyles?: string | string[] | React.CSSProperties,
9
+ styles?: React.CSSProperties,
10
+ ) => {
11
+ let classNames;
12
+ let combinedStyles;
13
+ let propsCopy = {...props};
14
+ if (props.className) {
15
+ if (typeof props.className === 'string') {
16
+ classNames = [props.className];
17
+ } else if (Array.isArray(props.className)) {
18
+ classNames = props.className;
19
+ }
20
+ delete propsCopy.className;
21
+ }
22
+ if (props.style) {
23
+ combinedStyles = props.style;
24
+ delete propsCopy.style;
25
+ }
26
+
27
+ if (classNamesOrStyles) {
28
+ let paramType = typeof classNamesOrStyles;
29
+ if (paramType === 'string') {
30
+ if (classNames) {
31
+ classNames.push(classNamesOrStyles);
32
+ } else {
33
+ classNames = [classNamesOrStyles];
34
+ }
35
+ } else if (paramType === 'object') {
36
+ if (Array.isArray(classNamesOrStyles)) {
37
+ if (classNames) {
38
+ classNames = classNames.concat(classNamesOrStyles);
39
+ } else {
40
+ classNames = classNamesOrStyles;
41
+ }
42
+ } else {
43
+ combinedStyles = {...props.style, ...(classNamesOrStyles as object)};
44
+ }
45
+ }
46
+ if (styles) {
47
+ combinedStyles = {...combinedStyles, ...styles};
48
+ }
49
+ }
50
+
51
+ return {
52
+ className: (classNames || []).filter(Boolean).join(' '),
53
+ style: combinedStyles,
54
+ ...propsCopy,
55
+ };
56
+ };