skandha 0.7.0 → 0.9.0

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.
package/README.md CHANGED
@@ -4,101 +4,70 @@
4
4
 
5
5
  The goal of SkandhaJS is to provide data containers that have reusable behaviors, such as
6
6
  selection, highlighting, filtering, drag-and-drop, etc. The only requirement for using these containers is
7
- that every item in the container has a unique id. When combining behaviors in a container, you need to
8
- make sure that they interact properly. For example, when selecting an item you may want the item also to
7
+ that every item in the container has a unique id. SkandhaJS allows you to combine behaviors and make them interact properly. For example, when selecting an item you may want the item also to
9
8
  be highlighted. As we will see, these interactions are achieved by implementing callback functions.
10
9
 
11
- ---
12
-
13
- **NOTE**
14
-
15
- In a Skandha container, behaviors are implemented using classes that are called facets, and they are found by their class name. The recommendation is to add a `static className` function to each facet class. If you omit this function, then Skandha will use the automatically generated class name, which can cause problems with the mangling of classnames in your minification tool.
16
-
17
- ---
18
-
19
10
  ## Links
20
11
 
21
12
  - The [skandha](http://github.com/mnieber/skandha) library contains the basic building blocks
22
13
  - The [skandha-facets](http://github.com/mnieber/skandha-facets) library contains useful facets such as selection, highlight, filtering, addition, etc.
23
14
  - The [skandha-mobx](http://github.com/mnieber/skandha-mobx) library contains bindings to MobX
24
15
  - The [aspiration](http://github.com/mnieber/aspiration) library handles aspect oriented programming (AOP)
16
+ - The [example code](http://github.com/mnieber/skandha-sample-app) shown in this README.
25
17
 
26
- ## Example code
18
+ ## Snippet
27
19
 
28
- The full example code for this README can be found [here](http://github.com/mnieber/skandha-sample-app). Note that this repository
29
- contains two version of the example code. The first version creates all the behaviours from scratch. Although it gives a good insight into
30
- the Skandha principles, the code will look bulky. The second version is reflective of a real use-case as it uses the reusable behaviours
31
- from [skandha-facets](http://github.com/mnieber/skandha-facets).
20
+ ```
21
+ // file: Selection.ts
22
+ import { input, output, operation } from 'skandha';
23
+ import { host, stub } from 'aspiration';
32
24
 
33
- ## Explanation
25
+ export type SelectionParamsT = { itemId?: string; isShift?: boolean; isCtrl?: boolean; };
34
26
 
35
- ### The Selection facet
27
+ export class Selection<ValueT = any> {
28
+ static className = () => 'Selection';
36
29
 
37
- As mentioned above, behaviors in Skandha are implemented as classes that are called facets. Let's start by looking at a reusable
38
- Selection facet. Before we show how it's implemented, here is an example of using it to select a range of
39
- items from id `2` to id `5`:
30
+ @input selectableIds: Array<string> = stub();
31
+ @output ids: Array<string> = [];
32
+ @output anchorId?: string;
33
+ @output items: Array<ValueT> = stub();
40
34
 
35
+ @operation @host(['selectionParams'], selectItemDefaultCbs) selectItem(
36
+ selectionParams: SelectionParamsT
37
+ ) {
38
+ return (cbs: Selection_selectItem) => {
39
+ cbs.select();
40
+ };
41
+ }
42
+ }
41
43
  ```
42
- const ctr = createContainer();
43
- ctr.selection.selectableIds = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
44
44
 
45
- ctr.selection.selectItem({itemId: "2"});
46
- ctr.selection.selectItem({itemId: "5", isShift: true});
47
- console.log(ctr.selection.ids);
48
- ```
45
+ ## Fact: the Selection facet has input, data and output members
49
46
 
50
- Note that the key function call here is `selection.selectItem`. Let's look at how it's implemented. First of all, we need a type that holds the selection parameters:
47
+ In the snippet above we see a so-called Facet class. A facet class implements a reusable behaviour. The `Selection` facet stores a list of selectable ids and has a `selectItem` function that allows you to select from those ids. The result of the selection is stored in the `ids`, `anchorId` and `items` fields. Note that `selectItem` is an empty function. The exact method for doing the selection is determined by a callback function, as we shall see later.
51
48
 
52
- ```
53
- export type SelectionParamsT = {
54
- itemId: string | undefined;
55
- isShift?: boolean;
56
- isCtrl?: boolean;
57
- };
58
- ```
49
+ ## Fact: the Selection facet has a static className function
50
+
51
+ Various facets are usually combined in a single container. To allow you to look up a facet by it's name, each facet class implements the static `className` function.
52
+
53
+ ## Fact: the `selectItem` function uses a callback function
54
+
55
+ We shall see later how callbacks are installed in a facet. For now, it's sufficient to understand that `selectItem` (the host function) executes the `cbs.selectItem()` callback function. This callback function will have access to all parameters of the host function (in this case: `selectionParams`).
59
56
 
60
- The next step is less obvious: we define a helper class that declares the arguments and callbacks of
61
- `selection.selectItem`. Why is this needed? The reason for using a callback function is that we
62
- don't want to hard code how selection is performed (e.g. what the effect of the shift and ctrl key is),
63
- because this would limit the reusability. The reason for storing the arguments of `selection.selectItem`
64
- is that this makes it easier to pass these arguments to other functions that want to use
65
- them. For example, if a side-effect of selection is to highlight the item, then we
66
- would like to pass the `SelectionParamsT` instance also to a `highlightSelectedItem` policy function
67
- (this will be explained in detail later). The helper class looks like this:
57
+ ## Snippet
68
58
 
69
59
  ```
60
+ // file: Selection.ts
70
61
  export class Selection_selectItem extends Cbs {
71
62
  selectionParams: SelectionParamsT = stub();
72
63
  select() {}
73
64
  }
74
65
  ```
75
66
 
76
- Next, we add the `Selection` class itself that has a `selectItem` function that checks the selection
77
- parameters and then calls the `select` callback function.
78
-
79
67
  ```
80
- class Selection<ValueT> {
81
- @data selectableIds: Array<string> = stub();
82
- @data ids: Array<string> = [];
83
- @data anchorId?: string;
84
- @data items?: Array<ValueT>; // ignore this field for now...
68
+ // file: createContainer.ts
69
+ import { registerCtr } from 'skandha';
85
70
 
86
- @operation @host selectItem(selectionParams: SelectionParamsT) {
87
- return (cbs: Selection_selectItem) => {
88
- if (!this.selectableIds.includes(selectionParams.itemId)) {
89
- throw Error(`Invalid id: ${selectionParams.itemId}`);
90
- }
91
- cbs.select();
92
- }
93
- }
94
- }
95
- ```
96
-
97
- And finally, we add the `createContainer` function that creates a `Selection` instance and implements
98
- the `select` callback. We don't show the code of `handleSelectItem`, but you can see it
99
- [here](https://github.com/mnieber/skandha-facets/blob/main/Selection.ts).
100
-
101
- ```
102
71
  const createContainer() {
103
72
  const ctr = {
104
73
  selection: new Selection()
@@ -112,47 +81,94 @@ const createContainer() {
112
81
  }
113
82
  });
114
83
 
84
+ // If you omit `members` then all fields of `ctr` are included.
85
+ registerCtr(ctr, { name: 'TodosCtr', members: ['selection'] });
115
86
  return ctr;
116
87
  }
117
88
  ```
118
89
 
119
- Notes:
90
+ ## Fact: a Cbs class defines a set of callbacks
91
+
92
+ In the above snippet we see that the `Selection_selectItem` class defines the callbacks that can be installed for the `selectItem` function of the `Selection` facet. This class also defines the parameters that are received by the host function and made available to each callback.
93
+
94
+ ## Fact: the setCallbacks function installs the callbacks for the Selection facet
95
+
96
+ In the same snippet we see that `setCallbacks` is called to install the `select` callback function. This callback function forwards to a helper function called `handleSelectItem`. Note that the `Selection_selectItem` instance is available through the `this` constant.
97
+
98
+ ## Fact: the registerCtr function registers the container with SkandhaJS
120
99
 
121
- - The `@host` decorator comes from the [Aspiration](http://github.com/mnieber/aspiration) library.
122
- - The `stub` function is used in callback objects to indicate that although the field is initialized with `undefined` it will receive a value later.
100
+ The purpose of calling registerCtr on the container is twofold:
123
101
 
124
- ## Combining Selection with Highlight
102
+ - it makes it possible to look up a facet by its name or class, e.g. `getf(ctr, Selection)` returns `ctr.selection`.
103
+ - it tells SkandhaJS what facets should be included in log reports. When the client calls any facet function that is decorated with @operation then SkandhaJS will log the entire container before and after the operation was executed. Logging can be enabled using the `setOptions` function.
125
104
 
126
- So far, we have shown a rather indirect implementation of selection. This indirect approach becomes useful
127
- when we add a second facet: Highlight. We will extend the example such that when the user selects an item,
128
- it will also be highlighted. The same approach can be used to make sure that the Filtering and Highlight
129
- facets work well together: if the filter is enabled, the highlight moves to an item that is not hidden by the
130
- filter. In general, there are many possibilities to create complex behaviors this way.
105
+ ## Snippet: the handleSelectItem functon
131
106
 
132
- Let's start by looking at the code of the `Highlight` facet:
107
+ ```
108
+ // file: handleSelectItem.ts
109
+ export function handleSelectItem(
110
+ facet: Selection,
111
+ { itemId, isShift, isCtrl }: SelectionParamsT
112
+ ) {
113
+ if (itemId === undefined) {
114
+ facet.ids = [];
115
+ facet.anchorId = undefined;
116
+ return;
117
+ }
133
118
 
119
+ if (isShift) {
120
+ // Omitted for brevity
121
+ } else if (isCtrl) {
122
+ facet.ids = facet.ids.includes(itemId)
123
+ ? facet.ids.filter((x) => x !== itemId)
124
+ : [...facet.ids, itemId];
125
+ } else {
126
+ facet.ids = [itemId];
127
+ }
128
+
129
+ // Move the anchor
130
+ if (!facet.anchorId || !(isCtrl || isShift)) {
131
+ facet.anchorId = itemId;
132
+ }
133
+ }
134
134
  ```
135
+
136
+ ## Fact: the handleSelectItem does the actual work of selecting items
137
+
138
+ For completeness, we show how `handleSelectItem` is implemented. Note that this function stores the results directly in the selection facet instance.
139
+
140
+ ## Snippet: the Highlight facet
141
+
142
+ ```
143
+ // file: Highlight.ts
135
144
  export class Highlight_highlightItem extends Cbs {
136
145
  id: string = stub();
137
146
  scrollItemIntoView() {}
138
147
  }
139
148
 
140
149
  export class Highlight<ValueT = any> {
141
- @data id: string | undefined;
142
- @data item?: ValueT; // ignore this field for now...
150
+ @data id?: string;
151
+ @data item?: ValueT;
143
152
 
144
- @operation @host highlightItem(id: string) {
153
+ @operation @host(["id"]) highlightItem(id: string) {
145
154
  return (cbs: Highlight_highlightItem) => {
146
155
  this.id = id;
147
- maybe(cbs.scrollItemIntoView).bind(cbs)();
156
+ if (cbs.scrollItemIntoView) {
157
+ cbs.scrollItemIntoView();
158
+ }
148
159
  };
149
160
  }
150
161
  }
151
162
  ```
152
163
 
153
- We can now update `createContainer` with the `Highlight` facet:
164
+ ## Fact: the `Highlight` facet implements the highlight behaviour
165
+
166
+ The `Highlight` snippet has a similar structure as the `Selection` facet. It has a callback function for scrolling the highlighted item into view. Note that the `highlightItem` function does not set the `item` field, because the facet only deals with ids. We shall later see how the `item` field is set by looking up the item in an external collection.
167
+
168
+ ## Snippet: a container with Selection and Highlight
154
169
 
155
170
  ```
171
+ // file: createContainer.ts
156
172
  const createContainer() {
157
173
  const ctr = {
158
174
  selection: new Selection(),
@@ -163,9 +179,13 @@ const createContainer() {
163
179
  selectItem: {
164
180
  select(this: Selection_selectItem): {
165
181
  handleSelectItem(ctr.selection, this.selectionParams);
182
+
166
183
  if (!this.selectionParams.isShift && !this.selectionParams.isCtrl) {
167
184
  ctr.highlight.highlightItem(this.selectionParams.itemId);
168
185
  }
186
+
187
+ // The above 3 lines could be extracted in a reusable helper function, e.g.
188
+ // highlightFollowsSelection(ctr, this.selectionParams);
169
189
  },
170
190
  }
171
191
  });
@@ -173,37 +193,14 @@ const createContainer() {
173
193
  return ctr;
174
194
  ```
175
195
 
176
- ### Adding the Filtering facet
177
-
178
- Now, let's add filtering. For brevity, the definition of `Filtering_apply` and `Filtering_enable`
179
- is skipped. Also, we assume that the logic for highlighting the select item is captured in a policy
180
- function called `highlightFollowsSelection`, and that `highlightIsCorrectedOnFilterChange` ensures
181
- that there is still a highlighted item when the filter changes.
182
-
183
- ```
184
- export class Filtering<ValueT = any> {
185
- @data isEnabled: boolean = false;
186
- @data filter: FilterT = (ValueT[]) => [];
187
- @data inputItems?: Array<ValueT>;
188
-
189
- @data get filteredItems() {
190
- return this.isEnabled ? this.filter(this.inputItems) : this.inputItems;
191
- }
196
+ ## Fact: callbacks allow you to create co-ordinated behaviours
192
197
 
193
- @operation @host apply(filter: FilterT) {
194
- return (cbs: Filtering_apply) => {
195
- this.filter = filter;
196
- this.isEnabled = true;
197
- };
198
- }
198
+ Above we see for the first time how SkandhaJS helps you to create co-ordinated behaviours. When the client calls `ctr.selection.selectItem` and the `select` callback is executed, then the callback ensures that the selected item is also highlighted. As the comment in the snippet suggests, we can make the body of the `select` callback more readable by extracting some of the code in a `highlightFollowsSelection` helper function.
199
199
 
200
- @operation @host setEnabled(flag: boolean) {
201
- return (cbs: Filtering_setEnabled) => {
202
- this.isEnabled = flag;
203
- };
204
- }
205
- }
200
+ ## Snippet: a container with Selection, Highlight and Filtering
206
201
 
202
+ ```
203
+ // file: createContainer.ts
207
204
  const createContainer() {
208
205
  const ctr = {
209
206
  selection: new Selection(),
@@ -232,129 +229,49 @@ const createContainer() {
232
229
  }
233
230
  ```
234
231
 
235
- Notes:
236
-
237
- - There are special callback functions called `enter` and `exit` that are called at the
238
- beginning and end of the operation function. An example of using the `exit` callback is shown
239
- above.
240
-
241
- ### Mapping data onto the container, and between facets
242
-
243
- At this point we have a container with facets that perform useful operations, and that interact
244
- to achieve interesting behaviours. However, we took a shortcut by hard coding the value of
245
- `Selection.selectableIds`, and also we forgot to set the value of `Filtering.inputItems`. Skandha
246
- comes with data mapping functions for this purpose. Let's extend the container with input
247
- and output data structures:
248
-
249
- ```
250
- const createContainer() {
251
- // we keep all the existing code that was added above
252
-
253
- ctr['inputs'] = {
254
- todoById: {[id: string]: TodoT} = loadTodos();
255
- };
256
-
257
- ctr['outputs'] = {
258
- filteredTodoById: {[id: string]: TodoT} = {};
259
- };
232
+ ## Fact: the `exit` callback function is called at the end of the host function
260
233
 
261
- registerFacets(ctr, { name: 'TodosCtr' });
262
- return ctr;
263
- ```
234
+ In the snippet we add a filtering facet that has an `apply` host function. In this host function, we can install an `exit` callback that corrects the highlight when the filter changes (because the filter may hide the highlighted item). The `exit` callback is called at the end of the host function. Similarly, the `enter` callback is called at the beginning.
264
235
 
265
- Note the call to `registerFacets`, which is needed for the data mapping functions that we are about to introduce.
266
- As a first step, we need to connect `Filtering.inputItems` to `Inputs.todoById` and `Outputs.filteredTodoById`.
267
- We can think of this as setting up a pipeline, where one of more source facet members are mapped onto a destination
268
- facet member:
236
+ ## Snippet: mapping data onto each facet
269
237
 
270
238
  ```
271
- mapDataToFacet(
272
- ['filtering', 'inputItems'],
273
- getm(['inputs', 'todoById']),
274
- (x: TodoByIdT) => Object.values(x)
275
- )(ctr);
276
-
277
- const listToItemById = items => items.reduce((acc: any, x: TodoT) => ({ ...acc, [x.id]: x }), {})
278
-
279
- mapDataToFacet(
280
- ['outputs', 'filteredTodoById'],
281
- getm(['filtering', 'filteredItems']),
282
- listToItemById,
283
- )(ctr);
284
- ```
285
-
286
- Notes:
287
-
288
- - the first argument to `mapDataToFacet` is a reference to the the facet member that receives the data. SkandhaJS will patch the
289
- facet, which - in the above example - means replacing the `inputItems` of `ctr.filtering` with a `get inputItems()` property.
290
- - the second argument to `mapDataToFacet` is a getter function that takes the container and returns some data.
291
- In this case, it uses the `getm` helper function to return `ctr.inputs.todoById`.
292
- - the third argument is a transformation that is applied to the result of the getter function. In this case,
293
- it means that `get inputItems()` returns `Object.values(getm(Inputs, 'todoById')(ctr))`.
294
- - in the calls to `mapDataToFacet`, we have to be careful to correctly spell the facet member names, such as 'filtering'.
295
- It's also possible to use a class instead of a member name. For example, we could replace `['filtering', 'inputItems']` with
296
- `[Filtering, 'inputItems']`.
297
-
298
- As the next step, we will connect the list of filtered items to `Selection.selectableIds` (so that selection
299
- happens in the filtered list). In addition, we will add datamappings for `Selection.items`
300
- and `Highlight.item` (these data mappings allow us to not only access the ids, but also the items themselves):
239
+ // file: createContainer.ts
240
+ import * from 'ramda' as R;
241
+
242
+ const connectContainer(ctr: any) {
243
+ const inputs = { todos: loadTodos() };
244
+ const outputs = { filteredTodoById: {} };
245
+
246
+ const con = createConnector({
247
+ selection: () => ctr.selection,
248
+ highlight: () => ctr.highlight,
249
+ filtering: () => ctr.filtering`,
250
+ inputs: () => inputs,
251
+ outputs: () => outputs,
252
+ });
301
253
 
302
- ```
303
- mapDataToFacet(
304
- ['selection', 'selectableIds'],
305
- getm(['outputs', 'filteredTodoById']),
306
- (x: TodoByIdT) => Object.keys(x)
307
- )(ctr);
308
-
309
- mapDatasToFacet(
310
- ['selection', 'items'],
311
- [
312
- getm(['selections', 'ids']),
313
- getm(['inputs', 'todoById']),
314
- ],
315
- (ids: string[], todoById: TodoByIdT) => ids.map((id) => todoById[id])
316
- )(ctr);
317
-
318
- mapDatasToFacet(
319
- [Highlight, 'item'],
320
- [
321
- getm([Highlight, 'id']),
322
- getm([Inputs, 'todoById']),
323
- ],
324
- (id: string, todoById: TodoByIdT) => todoById[id]
325
- )(ctr);
326
- ```
254
+ const lookUpTodo = (id: string) => outputs.filteredTodoById[id];
327
255
 
328
- We are done: the facets in the container are connected. Note that if we put these mapping policies in a reusable
329
- library then we can shorten the code to the following:
256
+ con.filtering.inputItems = con.inputs.todos;
257
+ con.outputs.filteredTodoById = con.filtering.filteredItems.tf(R.indexBy('id));
258
+ con.selection.selectableIds = con.outputs.filteredTodoById.tf(R.keys);
259
+ con.selection.item = con.selection.ids.tf(lookUpTodos);
260
+ con.highlight.item = con.highlight.id.tf(lookUpTodo);
330
261
 
262
+ con.connect();
331
263
  ```
332
- const filteredTodoById = ['outputs', 'filteredTodoById'];
333
- const inputTodoById = ['inputs', 'todoById'];
334
- const filteredItems = ['filtering', 'filteredItems'];
335
-
336
- const policies = [
337
- // selection
338
- selectionUsesSelectableIds(filteredTodoById, Object.keys),
339
- selectionUsesItemLookUpTable(inputTodoById),
340
264
 
341
- // highlighting
342
- highlightUsesItemLookUpTable(inputTodoById),
265
+ ## Fact: the Connector class maps data between objects
343
266
 
344
- // filtering
345
- filteringUsesInputItems(inputTodoById, Object.values),
346
- mapDataToFacet(filteredTodoById, getm(filteredItems), listToItemById),
347
- ]
348
-
349
- installPolicies<TodosCtr>(policies, ctr);
350
- ```
267
+ The Connector instance allows you to map data from one object onto another object. In our example, the `inputs.todos` field is mapped onto the `filtering.inputItems` field. What the `Connector` class does is turn `filtering.inputItems` into a `get` property that returns `filtering.inputItems`.
268
+ The snippet contains three steps. In the first step we declare the objects, such as `selection`, `filtering`, etc. For each object, we specify the getter function that returns the object. In the second step, we declare relations between the different fields of these objects. In this step, we can also declare one or more transformation functions (such as `lookUpTodo`) that should be used in the mapping. In the final step, we call `con.connect` to create the `get` properties that perform the declared mappings.
351
269
 
352
270
  ### Logging
353
271
 
354
- By calling `setOptions({logging: true})`, the Skandha library allows you to inspect each facet before and
355
- after calling an operation (remember that an operation is any facet member function decorated with @operation).
356
- This will log Facet members that are decorated with `@data` in a way that looks similar to what you are
357
- used to from Redux:
272
+ By calling `setOptions({logging: true})`, the SkandhaJS library allows you to inspect each facet before and
273
+ after calling an operation. An operation is any facet member function decorated with @operation.
274
+ This will log Facet members that are decorated with `@data` (or `@input`, or `@output`) in a way that looks similar to what you are used to from Redux:
358
275
 
359
276
  - before and after each operation, the entire container that holds the facet is logged
360
277
  - all data members of each facet in the container are included in the log
@@ -365,20 +282,15 @@ used to from Redux:
365
282
  If you want to render the facets with React then we need to let React know when data changes.
366
283
  The [skandha-mobx](http://github.com/mnieber/skandha-mobx) library was created for this purpose.
367
284
  It has a `registerCtr` function that turns each `@data` member into a Mobx property that is
368
- either observable or computed (this function call replaces the call to `registerFacets`):
285
+ either observable or computed:
369
286
 
370
287
  ```
371
288
  registerCtr({
372
289
  ctr: ctr,
373
- details: {
374
- name: 'TodosCtr',
375
- },
290
+ options: { name: 'TodosCtr'},
376
291
  });
377
292
  ```
378
293
 
379
- This means that by decorating your render function with `observer` you will get a re-render for any changes
380
- (in observable facet members) in the container.
381
-
382
294
  ## Conclusion
383
295
 
384
296
  SkandhaJS introduces a lot of new patterns and ideas (that are of course not in fact new) to allow you to
@@ -389,5 +301,4 @@ a policy that says that changing the highlight disables editing. By using facets
389
301
  editing-specific code directly in the highlight function (that would obviously be mixing concerns).
390
302
 
391
303
  In my experience, using SkandhaJS leads to code reuse and improved structure. On top of that, it offers a good
392
- debugging experience (yes, you will still have to debug) by logging all changes in the container. Finally,
393
- it works nicely with MobX to render the container contents.
304
+ debugging experience by logging all changes in the container. Finally, it works nicely with MobX to render the container contents.
package/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  export { getCtrState } from './internal/logging';
2
2
  export { getOptions, setOptions } from './internal/options';
3
+ export { createConnector } from './lib/Connector';
3
4
  export { getc, getFacetMemberNames } from './lib/ctr';
4
5
  export { data, getDataMemberNames, getLoggedMemberNames, input, log, output, } from './lib/data';
5
- export { facet, getf, getm, registerFacets } from './lib/facet';
6
+ export { getf } from './lib/facet';
6
7
  export { installPolicies } from './lib/install';
7
8
  export { getOperationMemberNames, operation } from './lib/operation';
8
- export { mapDatasToFacet, mapDataToFacet, mapDataToProp, patchFacet, } from './lib/patch';
9
+ export { mapDataToProp } from './lib/patch';
10
+ export { registerCtr } from './lib/registerCtr';
11
+ export type { OptionsT as RegisterCtrOptionsT } from './lib/registerCtr';
9
12
  export declare type ClassT = any;
10
- export declare type MemberNameT = string;
11
- export declare type ClassMemberT = [ClassT, MemberNameT, any?];
12
- export declare type GetterT<T = any> = (ctr: any) => T;
package/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  export { getCtrState } from './internal/logging';
2
2
  export { getOptions, setOptions } from './internal/options';
3
+ export { createConnector } from './lib/Connector';
3
4
  export { getc, getFacetMemberNames } from './lib/ctr';
4
5
  export { data, getDataMemberNames, getLoggedMemberNames, input, log, output, } from './lib/data';
5
- export { facet, getf, getm, registerFacets } from './lib/facet';
6
+ export { getf } from './lib/facet';
6
7
  export { installPolicies } from './lib/install';
7
8
  export { getOperationMemberNames, operation } from './lib/operation';
8
- export { mapDatasToFacet, mapDataToFacet, mapDataToProp, patchFacet, } from './lib/patch';
9
+ export { mapDataToProp } from './lib/patch';
10
+ export { registerCtr } from './lib/registerCtr';
@@ -1,6 +1,5 @@
1
1
  export declare function getOrCreate(obj: any, key: any, fn: any): any;
2
2
  export declare const zip: (arr: any, ...arrs: any) => any;
3
- export declare function getCtrClassAdmin(ctrClass: any): any;
4
3
  export declare function getCtrAdmin(ctr: any): any;
5
4
  export declare function getFacetClassAdmin(facetClass: any): any;
6
5
  export declare function getFacetAdmin(facet: any): any;
package/internal/utils.js CHANGED
@@ -23,9 +23,6 @@ export var zip = function (arr) {
23
23
  return arrs.reduce(function (a, arr) { return __spreadArray(__spreadArray([], a, true), [arr[i]], false); }, [val]);
24
24
  });
25
25
  };
26
- export function getCtrClassAdmin(ctrClass) {
27
- return getOrCreate(ctrClass, symbols.admin, function () { return ({}); });
28
- }
29
26
  export function getCtrAdmin(ctr) {
30
27
  return getOrCreate(ctr, symbols.admin, function () { return ({}); });
31
28
  }
@@ -0,0 +1,5 @@
1
+ declare type FunctionByNameT = {
2
+ [name: string]: Function;
3
+ };
4
+ export declare const createConnector: (entities: FunctionByNameT) => any;
5
+ export {};
@@ -0,0 +1,98 @@
1
+ import { mapDataToProp } from './patch';
2
+ var Connector = /** @class */ (function () {
3
+ function Connector(entities, options) {
4
+ var _this = this;
5
+ var _a;
6
+ this.entityByName = {};
7
+ this.connect = function () {
8
+ for (var _i = 0, _a = Object.values(_this.entityByName); _i < _a.length; _i++) {
9
+ var entity = _a[_i];
10
+ _this._connectEntity(entity);
11
+ }
12
+ };
13
+ this._connectEntity = function (entity) {
14
+ var _loop_1 = function (entry) {
15
+ var name_1 = entry[0], field = entry[1];
16
+ var obj = entity.getObj();
17
+ if (_this.options.strict && !(name_1 in obj)) {
18
+ throw new Error("Cannot override the field ".concat(name_1, " in ").concat(obj, ".") +
19
+ 'Field not found. Disable strict mode to ignore this error.');
20
+ }
21
+ mapDataToProp(obj, name_1, function () {
22
+ return field.getSource();
23
+ });
24
+ };
25
+ for (var _i = 0, _a = Object.entries(entity.fieldByName); _i < _a.length; _i++) {
26
+ var entry = _a[_i];
27
+ _loop_1(entry);
28
+ }
29
+ };
30
+ this.options = {
31
+ strict: (_a = options === null || options === void 0 ? void 0 : options.strict) !== null && _a !== void 0 ? _a : true,
32
+ };
33
+ for (var _i = 0, _b = Object.entries(entities); _i < _b.length; _i++) {
34
+ var entry = _b[_i];
35
+ var name_2 = entry[0], getObj = entry[1];
36
+ this.entityByName[name_2] = new Entity(getObj);
37
+ }
38
+ }
39
+ return Connector;
40
+ }());
41
+ export var createConnector = function (entities) {
42
+ var con = new Connector(entities);
43
+ return new Proxy(con, {
44
+ get: function (target, key) {
45
+ // Code example:
46
+ //
47
+ // const con = createConnector({ foo: () => myFoo });
48
+ //
49
+ // When you call `con.foo` then you
50
+ // receive `con.entityByName.foo.fieldByName`.
51
+ //
52
+ // con.foo.bar = 5; // Equivalent to: con.entityByName.foo.fieldByName.bar = 5
53
+ //
54
+ // When you call `con.connect` then a normal lookup of the `connect`
55
+ // member in `con` is performed.
56
+ var _a, _b;
57
+ return (_b = (_a = target.entityByName[key]) === null || _a === void 0 ? void 0 : _a.fieldByName) !== null && _b !== void 0 ? _b : target[key];
58
+ },
59
+ });
60
+ };
61
+ var Entity = /** @class */ (function () {
62
+ function Entity(getObj) {
63
+ var _this = this;
64
+ this.getObj = getObj;
65
+ this.fieldByName = new Proxy({}, {
66
+ get: function (target, key) {
67
+ var _a;
68
+ return (_a = target[key]) !== null && _a !== void 0 ? _a : new Field(_this, key);
69
+ },
70
+ });
71
+ }
72
+ return Entity;
73
+ }());
74
+ var Field = /** @class */ (function () {
75
+ function Field(entity, name) {
76
+ var _this = this;
77
+ this.transforms = [];
78
+ this.getSource = function () {
79
+ var obj = _this.entity.getObj();
80
+ if (!(_this.name in obj)) {
81
+ throw new Error("Field ".concat(_this.name, " does not exist on object ").concat(obj));
82
+ }
83
+ var result = obj[_this.name];
84
+ for (var _i = 0, _a = _this.transforms; _i < _a.length; _i++) {
85
+ var tf = _a[_i];
86
+ result = tf(result);
87
+ }
88
+ return result;
89
+ };
90
+ this.entity = entity;
91
+ this.name = name;
92
+ }
93
+ Field.prototype.tf = function (transform) {
94
+ this.transforms.push(transform);
95
+ return this;
96
+ };
97
+ return Field;
98
+ }());
package/lib/facet.d.ts CHANGED
@@ -1,10 +1,3 @@
1
- import { ClassMemberT, ClassT, GetterT } from '..';
2
- export declare function facet(facetHost: any, facetMember: any, descriptor?: undefined): undefined;
1
+ import { ClassT } from '..';
3
2
  export declare function facetClassName(facetClass: ClassT): any;
4
- export declare function registerFacets(ctr: any, options: {
5
- name?: string;
6
- members?: string[];
7
- ctrState?: Function;
8
- }): void;
9
3
  export declare function getf(facetClass: ClassT | string, ctr?: any): any;
10
- export declare function getm<T = any>(classMember: ClassMemberT): GetterT<T>;
package/lib/facet.js CHANGED
@@ -1,43 +1,7 @@
1
- import { getCtrState } from '../internal/logging';
2
- import { options as skandhaOptions } from '../internal/options';
3
- import { getCtrAdmin, getCtrClassAdmin, getFacetAdmin, } from '../internal/utils';
4
- import { setCtr } from './ctr';
5
- // Registers facetMember as a facet in the facetHost class
6
- export function facet(facetHost, facetMember, descriptor) {
7
- var _a;
8
- if (descriptor === void 0) { descriptor = undefined; }
9
- var ctrClassAdmin = getCtrClassAdmin(facetHost.constructor);
10
- ctrClassAdmin.facetMembers = (_a = ctrClassAdmin.facetMembers) !== null && _a !== void 0 ? _a : [];
11
- ctrClassAdmin.facetMembers.push(facetMember);
12
- return descriptor;
13
- }
1
+ import { getCtrAdmin } from '../internal/utils';
14
2
  export function facetClassName(facetClass) {
15
3
  return facetClass.className ? facetClass.className() : facetClass.name;
16
4
  }
17
- // Registers members of ctr as facets in ctr. Each facet is registered by its class name.
18
- export function registerFacets(ctr, options) {
19
- var _a, _b, _c, _d, _e;
20
- var ctrAdmin = getCtrAdmin(ctr);
21
- ctrAdmin.logName = (_a = options.name) !== null && _a !== void 0 ? _a : ctr.constructor.name;
22
- ctrAdmin.facetMembers = (_b = ctrAdmin.facetMembers) !== null && _b !== void 0 ? _b : [];
23
- ctrAdmin.facetByFacetClassName = (_c = ctrAdmin.facetByFacetClassName) !== null && _c !== void 0 ? _c : {};
24
- ctrAdmin.ctrStateOverride = options.ctrState;
25
- ((_e = (_d = options.members) !== null && _d !== void 0 ? _d : getCtrClassAdmin(ctr.constructor).facetMembers) !== null && _e !== void 0 ? _e : Object.getOwnPropertyNames(ctr)).forEach(function (member) {
26
- ctrAdmin.facetMembers.push(member);
27
- var facet = ctr[member];
28
- setCtr(facet, ctr);
29
- var facetAdmin = getFacetAdmin(facet);
30
- facetAdmin.logName = "".concat(ctrAdmin.logName, "/").concat(facetClassName(facet.constructor));
31
- var className = facetClassName(facet.constructor);
32
- if (ctrAdmin.facetByFacetClassName[className] !== undefined) {
33
- console.error("Two facets of same type ".concat(className, " in container ").concat(ctrAdmin.logName));
34
- }
35
- ctrAdmin.facetByFacetClassName[className] = facet;
36
- });
37
- if (skandhaOptions.logging) {
38
- console.log('%c %s ctr initialized', 'color: gray', ctrAdmin.logName, getCtrState(ctr));
39
- }
40
- }
41
5
  export function getf(facetClass, ctr) {
42
6
  var _a;
43
7
  if (!ctr)
@@ -51,10 +15,3 @@ export function getf(facetClass, ctr) {
51
15
  }
52
16
  return facet;
53
17
  }
54
- export function getm(classMember) {
55
- var f = function (ctr) { var _a; return getf(classMember[0], (_a = classMember[2]) !== null && _a !== void 0 ? _a : ctr)[classMember[1]]; };
56
- // Set some properties on f to ease debugging later
57
- f.facetClassName = facetClassName(classMember[0]);
58
- f.facetMemberName = classMember[1];
59
- return f;
60
- }
package/lib/patch.d.ts CHANGED
@@ -1,5 +1 @@
1
- import { ClassMemberT, GetterT } from '..';
2
1
  export declare function mapDataToProp(facet: any, prop: string, getter: () => any): void;
3
- export declare function patchFacet(facet: any, members: any, options?: any): void;
4
- export declare const mapDataToFacet: ([toFacetClass, toMember, toCtr]: ClassMemberT, getter: GetterT, transform?: Function) => (ctr: any) => void;
5
- export declare const mapDatasToFacet: ([toFacetClass, toMember, toCtr]: ClassMemberT, getters: Array<GetterT>, transform: Function) => (ctr: any) => void;
package/lib/patch.js CHANGED
@@ -1,17 +1,3 @@
1
- var __assign = (this && this.__assign) || function () {
2
- __assign = Object.assign || function(t) {
3
- for (var s, i = 1, n = arguments.length; i < n; i++) {
4
- s = arguments[i];
5
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
- t[p] = s[p];
7
- }
8
- return t;
9
- };
10
- return __assign.apply(this, arguments);
11
- };
12
- import { facetLogName } from '../internal/logging';
13
- import { getDataMemberNames } from './data';
14
- import { getf } from './facet';
15
1
  export function mapDataToProp(facet, prop, getter) {
16
2
  delete facet[prop];
17
3
  Object.defineProperty(facet, prop, {
@@ -20,68 +6,3 @@ export function mapDataToProp(facet, prop, getter) {
20
6
  configurable: true,
21
7
  });
22
8
  }
23
- export function patchFacet(facet, members, options) {
24
- var _a;
25
- var dataMemberNames = getDataMemberNames(facet);
26
- for (var prop in members) {
27
- if ((_a = options === null || options === void 0 ? void 0 : options.warnIfNotADataMember) !== null && _a !== void 0 ? _a : true) {
28
- if (!dataMemberNames.includes(prop)) {
29
- console.error("Patching a property ".concat(prop, " that wasn't decorated with ") +
30
- "@data, @input or @output in ".concat(facetLogName(facet)));
31
- }
32
- }
33
- try {
34
- delete facet[prop];
35
- }
36
- catch (_b) {
37
- console.error("Could not delete property ".concat(prop, " from facet ").concat(facetLogName(facet)) +
38
- '. In case you are using mobx (or skandha-mobx), this can happen ' +
39
- 'when makeObservable (or makeCtrObservable) is called before ' +
40
- 'patching a property. Try calling these functions as late as ' +
41
- 'possible.');
42
- }
43
- Object.defineProperty(facet, prop, __assign(__assign({}, members[prop]), { enumerable: true, configurable: true }));
44
- }
45
- }
46
- export var mapDataToFacet = function (_a, getter, transform) {
47
- var toFacetClass = _a[0], toMember = _a[1], toCtr = _a[2];
48
- return function (ctr) {
49
- var _a;
50
- var patch = (_a = {},
51
- _a[toMember] = {
52
- get: function () {
53
- // This context variable is not necessary, it has redundant data and
54
- // could be inlined, but it makes debugging easier.
55
- var context = {
56
- ctr: toCtr !== null && toCtr !== void 0 ? toCtr : ctr,
57
- getter: getter,
58
- toClassMember: [toFacetClass, toMember],
59
- data: getter(ctr),
60
- };
61
- return transform ? transform(context.data) : context.data;
62
- },
63
- },
64
- _a);
65
- patchFacet(getf(toFacetClass, toCtr !== null && toCtr !== void 0 ? toCtr : ctr), patch);
66
- };
67
- };
68
- export var mapDatasToFacet = function (_a, getters, transform) {
69
- var toFacetClass = _a[0], toMember = _a[1], toCtr = _a[2];
70
- return function (ctr) {
71
- var _a;
72
- var patch = (_a = {},
73
- _a[toMember] = {
74
- get: function () {
75
- var context = {
76
- ctr: toCtr !== null && toCtr !== void 0 ? toCtr : ctr,
77
- getters: getters,
78
- toClassMember: [toFacetClass, toMember],
79
- datas: getters.map(function (getter) { return getter(ctr); }),
80
- };
81
- return transform.apply(void 0, context.datas);
82
- },
83
- },
84
- _a);
85
- patchFacet(getf(toFacetClass, toCtr !== null && toCtr !== void 0 ? toCtr : ctr), patch);
86
- };
87
- };
@@ -0,0 +1,6 @@
1
+ export declare type OptionsT = {
2
+ name: string;
3
+ members?: string[];
4
+ ctrState?: Function;
5
+ };
6
+ export declare function registerCtr(ctr: any, options: OptionsT): void;
@@ -0,0 +1,31 @@
1
+ import { getCtrState } from '../internal/logging';
2
+ import { options as skandhaOptions } from '../internal/options';
3
+ import { getCtrAdmin, getFacetAdmin } from '../internal/utils';
4
+ import { setCtr } from './ctr';
5
+ import { facetClassName } from './facet';
6
+ export function registerCtr(ctr, options) {
7
+ var _a, _b, _c;
8
+ var ctrAdmin = getCtrAdmin(ctr);
9
+ ctrAdmin.logName = options.name;
10
+ ctrAdmin.facetMembers = (_a = ctrAdmin.facetMembers) !== null && _a !== void 0 ? _a : [];
11
+ ctrAdmin.facetByFacetClassName = (_b = ctrAdmin.facetByFacetClassName) !== null && _b !== void 0 ? _b : {};
12
+ ctrAdmin.ctrStateOverride = options.ctrState;
13
+ ((_c = options.members) !== null && _c !== void 0 ? _c : Object.getOwnPropertyNames(ctr)).forEach(function (member) {
14
+ var facet = ctr[member];
15
+ setCtr(facet, ctr);
16
+ ctrAdmin.facetMembers.push(member);
17
+ if (!facet.constructor.className) {
18
+ console.error("Facet ".concat(member, " of container ").concat(ctrAdmin.logName, " ") +
19
+ "does not have a static className function");
20
+ }
21
+ var className = facetClassName(facet.constructor);
22
+ getFacetAdmin(facet).logName = "".concat(ctrAdmin.logName, "/").concat(className);
23
+ if (ctrAdmin.facetByFacetClassName[className] !== undefined) {
24
+ console.error("Two facets of same type ".concat(className, " in container ").concat(ctrAdmin.logName));
25
+ }
26
+ ctrAdmin.facetByFacetClassName[className] = facet;
27
+ });
28
+ if (skandhaOptions.logging) {
29
+ console.log('%c %s ctr initialized', 'color: gray', ctrAdmin.logName, getCtrState(ctr));
30
+ }
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skandha",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Create data containers with modular behaviours",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/mnieber/skandha.git",