skandha 0.6.0 → 0.7.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
@@ -2,25 +2,17 @@
2
2
 
3
3
  ## Rationale
4
4
 
5
- Despite having a lot of good libraries, programmers still find themselves reimplementing
6
- the same solutions over and over.
7
- The reason is that not every solution can be captured in a library. When there are cross-cutting concerns (aka
8
- aspects, as in aspect oriented programming), then out-of-the-box solutions often do not fit well enough. For this
9
- reason, programmers are often reluctant to store their data in framework-specific data-structures, even though such frameworks could potentially save time.
10
-
11
- Skandha proposes an alternative solution (somewhat inspired by Erlang) that aims to let the programmer re-use existing logic,
12
- while allowing them to use their own data-structures. This is achieved by mapping the user's data-structures onto a set of
13
- minimal interfaces called facets. These facets perform tasks by interacting via so-called policies. By keeping facets
14
- minimal, they can be generic, which leads to better code reuse.
5
+ The goal of SkandhaJS is to provide data containers that have reusable behaviors, such as
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
9
+ be highlighted. As we will see, these interactions are achieved by implementing callback functions.
15
10
 
16
11
  ---
17
12
 
18
13
  **NOTE**
19
14
 
20
- The binding of facets to containers in Skandha relies on class names. Therefore you
21
- need to disable the mangling of classnames in your minification tool, or disable minification completely. A future version of Skandha will allow you to declare
22
- facet names by adding a property in the facet class, or specify the names when calling
23
- `registerFacets`.
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.
24
16
 
25
17
  ---
26
18
 
@@ -34,127 +26,166 @@ facet names by adding a property in the facet class, or specify the names when c
34
26
  ## Example code
35
27
 
36
28
  The full example code for this README can be found [here](http://github.com/mnieber/skandha-sample-app). Note that this repository
37
- contains two version of the example code. The frst version creates all the behaviours from scratch. Although it gives a good insight into
29
+ contains two version of the example code. The first version creates all the behaviours from scratch. Although it gives a good insight into
38
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
39
31
  from [skandha-facets](http://github.com/mnieber/skandha-facets).
40
32
 
41
33
  ## Explanation
42
34
 
43
- The Skandha library will be explained using an example that progressively introduces various concepts
44
- and functions. The example shows how to add reusable selection, highlight and filtering behaviour to a container
45
- with Todo items.
46
-
47
- ### Containers and Facets
48
-
49
- A container is a set of related data, e.g. todo items together with data about selection, highlight, etc. A facet is a
50
- member of a container that represents a single aspect such as `Selection`. In the example below we see a `TodosCtr`
51
- with five facets: `Selection`, `Highlight`, `Filtering`, `Inputs` and `Outputs`. As we shall see, `Selection`,
52
- `Highlight` and `Filtering` are generic (not tied to the concept of a Todo) and therefore reusable.
35
+ ### The Selection facet
53
36
 
54
- The `Inputs` facet contains the data (i.e. the todos) that is selected, highlighted and filtered, whereas the `Outputs` facet
55
- contains some of the results that are produced by the container (e.g. the filtered list of todos). It's not strictly
56
- necessary to use `Inputs` and `Outputs` as you could directly connect `Selection`, `Highlight`, and `Filtering` to
57
- data-sources that are outside of the container, but it's easier to reason about the container if they exist.
58
-
59
- Our initial code is:
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`:
60
40
 
61
41
  ```
62
- class Selection<TodoT> {}
63
-
64
- class Highlight<TodoT> {}
65
-
66
- class Filtering<TodoT> {}
67
-
68
- class Inputs {
69
- @data todoById: TodoByIdT = {};
70
- }
71
-
72
- class Outputs {
73
- @data filteredTodoById: TodoByIdT = {};
74
- }
42
+ const ctr = createContainer();
43
+ ctr.selection.selectableIds = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
75
44
 
76
- class TodosCtr {
77
- @facet inputs: Inputs = new Inputs();
78
- @facet outputs: Outputs = new Outputs();
79
- @facet selection: Selection<TodoT> = new Selection<TodoT>();
80
- @facet highlight: Highlight<TodoT> = new Highlight<TodoT>();
81
- @facet filtering: Filtering<TodoT> = new Filtering<TodoT>();
82
- }
45
+ ctr.selection.selectItem({itemId: "2"});
46
+ ctr.selection.selectItem({itemId: "5", isShift: true});
47
+ console.log(ctr.selection.ids);
83
48
  ```
84
49
 
85
- Notes:
86
-
87
- - The `@data` and `@operation` decorators mark the fields in the facet that make up its abstract interface.
88
- Clients should use only these fields.
89
-
90
- ### Implementing the Selection facet
91
-
92
- We will first implement the Selection facet. The key requirements are that:
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:
93
51
 
94
- - this facet is only concerned with ids. To make a selection in any data-structure, it suffices to map this data-structure
95
- to a list of ids. And to obtain the list of selected items, it suffices to map in the opposite direction: from ids to items.
96
- - the facet has to be generic, which means that the client code has control over how selection is actually
97
- performed, and over possible side-effects. This means the client can determine
98
- the effect of the shift key, or decide that selecting an item should also highlight it.
99
- - preferably the facet has to perform some useful work, and not just be an empty abstraction.
52
+ ```
53
+ export type SelectionParamsT = {
54
+ itemId: string | undefined;
55
+ isShift?: boolean;
56
+ isCtrl?: boolean;
57
+ };
58
+ ```
100
59
 
101
- This is our proposal for the Selection facet:
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:
102
68
 
103
69
  ```
104
- export class Selection_select extends Cbs {
70
+ export class Selection_selectItem extends Cbs {
105
71
  selectionParams: SelectionParamsT = stub();
106
- selectItem() {}
72
+ select() {}
107
73
  }
74
+ ```
75
+
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.
108
78
 
109
- class Selection<TodoT> {
79
+ ```
80
+ class Selection<ValueT> {
110
81
  @data selectableIds: Array<string> = stub();
111
82
  @data ids: Array<string> = [];
112
83
  @data anchorId?: string;
113
- @data items?: Array<ValueT>;
84
+ @data items?: Array<ValueT>; // ignore this field for now...
114
85
 
115
86
  @operation @host selectItem(selectionParams: SelectionParamsT) {
116
- return (cbs: Selection_select) => {
87
+ return (cbs: Selection_selectItem) => {
117
88
  if (!this.selectableIds.includes(selectionParams.itemId)) {
118
89
  throw Error(`Invalid id: ${selectionParams.itemId}`);
119
90
  }
120
- cbs.selectItem();
91
+ cbs.select();
121
92
  }
122
93
  }
123
94
  }
124
95
  ```
125
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
+ const createContainer() {
103
+ const ctr = {
104
+ selection: new Selection()
105
+ };
106
+
107
+ setCallbacks(ctr.selection, {
108
+ selectItem: {
109
+ select(this: Selection_selectItem): {
110
+ handleSelectItem(ctr.selection, this.selectionParams);
111
+ },
112
+ }
113
+ });
114
+
115
+ return ctr;
116
+ }
117
+ ```
118
+
126
119
  Notes:
127
120
 
128
- - The `@host` decorator comes from the [Aspiration](http://github.com/mnieber/aspiration) library. It makes
129
- the function take a callback object (i.e. a set of callbacks) that is installed by the client. This allows us to
130
- write a `Selection` facet that is generic (our 2nd requirement) but also does useful work (3rd requirement).
131
- We shall discuss below how the `cbs.selectItem` callback is implemented.
132
- - The `stub` function is used in callback objects to indicate that although the field is initialized with `undefined`
133
- it will receive a value later.
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.
123
+
124
+ ## Combining Selection with Highlight
134
125
 
135
- ### Implementing the Highlight and Filtering facets
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.
136
131
 
137
- Since `Highlight` and `Filtering` follow the same structure as the `Selection` facet, we will only
138
- show the code. For brevity, we will leave out the code for the callback objects such as
139
- `Highlight_highlightItem` (they are straightforward and not that interesting):
132
+ Let's start by looking at the code of the `Highlight` facet:
140
133
 
141
134
  ```
135
+ export class Highlight_highlightItem extends Cbs {
136
+ id: string = stub();
137
+ scrollItemIntoView() {}
138
+ }
139
+
142
140
  export class Highlight<ValueT = any> {
143
141
  @data id: string | undefined;
144
- @data item?: ValueT;
142
+ @data item?: ValueT; // ignore this field for now...
145
143
 
146
144
  @operation @host highlightItem(id: string) {
147
145
  return (cbs: Highlight_highlightItem) => {
148
146
  this.id = id;
147
+ maybe(cbs.scrollItemIntoView).bind(cbs)();
149
148
  };
150
149
  }
151
150
  }
151
+ ```
152
+
153
+ We can now update `createContainer` with the `Highlight` facet:
154
+
155
+ ```
156
+ const createContainer() {
157
+ const ctr = {
158
+ selection: new Selection(),
159
+ highlight: new Highlight()
160
+ };
161
+
162
+ setCallbacks(ctr.selection, {
163
+ selectItem: {
164
+ select(this: Selection_selectItem): {
165
+ handleSelectItem(ctr.selection, this.selectionParams);
166
+ if (!this.selectionParams.isShift && !this.selectionParams.isCtrl) {
167
+ ctr.highlight.highlightItem(this.selectionParams.itemId);
168
+ }
169
+ },
170
+ }
171
+ });
172
+
173
+ return ctr;
174
+ ```
175
+
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.
152
182
 
183
+ ```
153
184
  export class Filtering<ValueT = any> {
154
185
  @data isEnabled: boolean = false;
155
- @data filter: FilterT = () => [];
156
-
186
+ @data filter: FilterT = (ValueT[]) => [];
157
187
  @data inputItems?: Array<ValueT>;
188
+
158
189
  @data get filteredItems() {
159
190
  return this.isEnabled ? this.filter(this.inputItems) : this.inputItems;
160
191
  }
@@ -172,172 +203,150 @@ export class Filtering<ValueT = any> {
172
203
  };
173
204
  }
174
205
  }
175
- ```
176
-
177
- ### Implementing the callback objects
178
-
179
- We've set up the basic structure of the facets, but the callbacks still have to be implemented.
180
- This step happens in the container class. We will use the following requirements:
181
-
182
- - the `cbs.selectItem` callback should make a selection that takes the shift and control keys into account.
183
- We will not show the code (you can see it
184
- [here](https://github.com/mnieber/skandha-facets/blob/main/Selection.ts)) but assume we have a library
185
- function `handleSelectItem` for this.
186
- - items that are selected should become highlighted. We will introduce a reusable policy
187
- function for this called `highlightFollowsSelection`.
188
- - if applying the filter hides the highlighted item then we want to correct the highlight.
189
- We will use a another reusable policy called `highlightIsCorrectedOnFilterChange` (shown
190
- [here](https://github.com/mnieber/skandha-facets/blob/main/policies/highlightIsCorrectedOnFilterChange.ts))
191
206
 
192
- ```
193
- class TodosCtr {
194
- // ...
195
-
196
- constructor() {
197
- registerFacets(this);
198
- this._setCallbacks();
199
- }
200
-
201
- _setCallbacks() {
202
- const ctr = this;
203
-
204
- setCallbacks(this.selection, {
205
- select: {
206
- selectItem(this: Selection_select): {
207
- handleSelectItem(ctr.selection, this.selectionParams);
208
- highlightFollowsSelection(ctr.selection, this.selectionParams); // See below
209
- },
210
- }
211
- });
207
+ const createContainer() {
208
+ const ctr = {
209
+ selection: new Selection(),
210
+ highlight: new Highlight(),
211
+ filtering: new Filtering()
212
+ };
213
+
214
+ setCallbacks(ctr.selection, {
215
+ selectItem: {
216
+ select(this: Selection_selectItem): {
217
+ handleSelectItem(ctr.selection, this.selectionParams);
218
+ highlightFollowsSelection(ctr, this.selectionParams);
219
+ },
220
+ }
221
+ });
212
222
 
213
- setCallbacks(this.filtering, {
214
- apply: {
215
- exit() {
216
- FacetPolicies.highlightIsCorrectedOnFilterChange(ctr.filtering);
217
- },
223
+ setCallbacks(filtering, {
224
+ apply: {
225
+ exit() {
226
+ highlightIsCorrectedOnFilterChange(ctr);
218
227
  },
219
- });
220
- }
221
- }
228
+ },
229
+ });
222
230
 
223
- export function highlightFollowsSelection(
224
- selection: Selection,
225
- selectionParams: SelectionParamsT
226
- ) {
227
- const {isCtrl, isShift, itemId} = selectionParams;
228
- const ctr = getc(selection);
229
- if (!isCtrl && !isShift) {
230
- getf(Highlight, ctr).highlightItem(itemId);
231
- }
231
+ return ctr;
232
232
  }
233
233
  ```
234
234
 
235
235
  Notes:
236
236
 
237
- - The call to `registerFacets` is mandatory. It creates a back-reference to the container in each facet
238
- instance.
239
- - The `getc` gets the container for a given facet, whereas `getf` gets a facet for a given container.
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
240
 
241
241
  ### Mapping data onto the container, and between facets
242
242
 
243
243
  At this point we have a container with facets that perform useful operations, and that interact
244
- to achieve interesting behaviours. However, the container will not yet work because `Selection.selectableIds`
245
- and `Filtering.inputItems` are not connected to any data sources. Note that the client of the container
246
- is responsible for setting `Inputs.todoById` to the collection of todos (at some time after constructing the
247
- container).
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
+ };
248
256
 
257
+ ctr['outputs'] = {
258
+ filteredTodoById: {[id: string]: TodoT} = {};
259
+ };
260
+
261
+ registerFacets(ctr, { name: 'TodosCtr' });
262
+ return ctr;
263
+ ```
264
+
265
+ Note the call to `registerFacets`, which is needed for the data mapping functions that we are about to introduce.
249
266
  As a first step, we need to connect `Filtering.inputItems` to `Inputs.todoById` and `Outputs.filteredTodoById`.
250
267
  We can think of this as setting up a pipeline, where one of more source facet members are mapped onto a destination
251
268
  facet member:
252
269
 
253
270
  ```
254
- _applyPolicies() {
255
- mapDataToFacet(
256
- [Filtering, 'inputItems'],
257
- getm([Inputs, 'todoById']),
258
- (x: TodoByIdT) => Object.values(x)
259
- )(this);
260
-
261
- const listToItemById = items => items.reduce((acc: any, x: TodoT) => ({ ...acc, [x.id]: x }), {})
262
-
263
- mapDataToFacet(
264
- [Outputs, 'filteredTodoById'],
265
- getm([Filtering, 'filteredItems']),
266
- listToItemById,
267
- )(this);
268
- }
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);
269
284
  ```
270
285
 
271
286
  Notes:
272
287
 
273
- - the first argument to `mapDataToFacet` is a getter function that takes the container and returns some data.
274
- In this case, it uses the `getm` helper function to return the `todoById` member of the `Inputs` facet.
275
- - the second argument to `mapDataToFacet` is the facet member that receives the data. SkandhaJS will patch the
276
- facet, replacing the `inputItems` member with a `get inputItems()` property that runs the
277
- getter function.
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`.
278
292
  - the third argument is a transformation that is applied to the result of the getter function. In this case,
279
- it means that `get inputItems()` returns `Object.values(getm(Inputs, 'todoById')(ctr))`,
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']`.
280
297
 
281
298
  As the next step, we will connect the list of filtered items to `Selection.selectableIds` (so that selection
282
299
  happens in the filtered list). In addition, we will add datamappings for `Selection.items`
283
- and `Selection.item`:
300
+ and `Highlight.item` (these data mappings allow us to not only access the ids, but also the items themselves):
284
301
 
285
302
  ```
286
- _applyPolicies() {
287
- // ...
288
-
289
- mapDataToFacet(
290
- [Selection, 'selectableIds'],
291
- getm([Outputs, 'filteredTodoById']),
292
- (x: TodoByIdT) => Object.keys(x)
293
- )(this);
294
-
295
- mapDatasToFacet(
296
- [Selection, 'items'],
297
- [
298
- getm([Selection, 'ids']),
299
- getm([Inputs, 'todoById']),
300
- ],
301
- (ids: string[], todoById: TodoByIdT) => ids.map((id) => todoById[id])
302
- )(this);
303
-
304
- mapDatasToFacet(
305
- [Highlight, 'item'],
306
- [
307
- getm([Highlight, 'id']),
308
- getm([Inputs, 'todoById']),
309
- ],
310
- (id: string, todoById: TodoByIdT) => todoById[id]
311
- )(this);
312
- }
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);
313
326
  ```
314
327
 
315
328
  We are done: the facets in the container are connected. Note that if we put these mapping policies in a reusable
316
- library then we can shorten `_applyPolicies` to:
329
+ library then we can shorten the code to the following:
317
330
 
318
331
  ```
319
- _applyPolicies() {
320
- // ...
321
-
322
- const policies = [
323
- // selection
324
- SelectionUsesSelectableIds([Outputs, 'filteredTodoById'], Object.keys),
325
- SelectionUsesItemLookUpTable([Inputs, 'todoById']),
326
-
327
- // highlighting
328
- HighlightUsesItemLookUpTable([Inputs, 'todoById']),
329
-
330
- // filtering
331
- FilteringUsesInputItems([Inputs, 'todoById'], Object.values),
332
- mapDataToFacet(
333
- [Outputs, 'filteredTodoById'],
334
- getm([Filtering, 'filteredItems']),
335
- listToItemById,
336
- ),
337
- ]
338
-
339
- installPolicies<TodosCtr>(policies, this);
340
- }
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
+
341
+ // highlighting
342
+ highlightUsesItemLookUpTable(inputTodoById),
343
+
344
+ // filtering
345
+ filteringUsesInputItems(inputTodoById, Object.values),
346
+ mapDataToFacet(filteredTodoById, getm(filteredItems), listToItemById),
347
+ ]
348
+
349
+ installPolicies<TodosCtr>(policies, ctr);
341
350
  ```
342
351
 
343
352
  ### Logging
@@ -355,25 +364,21 @@ used to from Redux:
355
364
 
356
365
  If you want to render the facets with React then we need to let React know when data changes.
357
366
  The [skandha-mobx](http://github.com/mnieber/skandha-mobx) library was created for this purpose.
358
- It turns each `@data` member into a Mobx property that is either observable or computed. By decorating
359
- your render function with `observer` you will get a re-render for any changes (in observable facet members)
360
- in the container:
367
+ 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`):
361
369
 
362
370
  ```
363
- class TodosCtr {
364
- // ...
365
-
366
- constructor() {
367
- registerFacets(this);
368
- this._setCallbacks();
369
- this._applyPolicies();
370
-
371
- // Turn all facets in the container into observable objects.
372
- // Members annotated with @data will become either @observable or @computed.
373
- makeCtrObservable(this);
374
- }
371
+ registerCtr({
372
+ ctr: ctr,
373
+ details: {
374
+ name: 'TodosCtr',
375
+ },
376
+ });
375
377
  ```
376
378
 
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
+
377
382
  ## Conclusion
378
383
 
379
384
  SkandhaJS introduces a lot of new patterns and ideas (that are of course not in fact new) to allow you to
package/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- export { ctrState } from "./internal/logging";
2
- export { getc, getFacetMemberNames } from "./lib/ctr";
3
- export { input, output, data, log, getLoggedMemberNames, getDataMemberNames, } from "./lib/data";
4
- export { mapDataToFacet, mapDatasToFacet, patchFacet, mapDataToProp, } from "./lib/patch";
5
- export { getOptions, setOptions } from "./internal/options";
6
- export { operation, getOperationMemberNames } from "./lib/operation";
7
- export { facet, registerFacets, getm, getf } from "./lib/facet";
8
- export { installPolicies } from "./lib/install";
1
+ export { getCtrState } from './internal/logging';
2
+ export { getOptions, setOptions } from './internal/options';
3
+ export { getc, getFacetMemberNames } from './lib/ctr';
4
+ export { data, getDataMemberNames, getLoggedMemberNames, input, log, output, } from './lib/data';
5
+ export { facet, getf, getm, registerFacets } from './lib/facet';
6
+ export { installPolicies } from './lib/install';
7
+ export { getOperationMemberNames, operation } from './lib/operation';
8
+ export { mapDatasToFacet, mapDataToFacet, mapDataToProp, patchFacet, } from './lib/patch';
9
9
  export declare type ClassT = any;
10
10
  export declare type MemberNameT = string;
11
11
  export declare type ClassMemberT = [ClassT, MemberNameT, any?];
package/index.js CHANGED
@@ -1,8 +1,8 @@
1
- export { ctrState } from "./internal/logging";
2
- export { getc, getFacetMemberNames } from "./lib/ctr";
3
- export { input, output, data, log, getLoggedMemberNames, getDataMemberNames, } from "./lib/data";
4
- export { mapDataToFacet, mapDatasToFacet, patchFacet, mapDataToProp, } from "./lib/patch";
5
- export { getOptions, setOptions } from "./internal/options";
6
- export { operation, getOperationMemberNames } from "./lib/operation";
7
- export { facet, registerFacets, getm, getf } from "./lib/facet";
8
- export { installPolicies } from "./lib/install";
1
+ export { getCtrState } from './internal/logging';
2
+ export { getOptions, setOptions } from './internal/options';
3
+ export { getc, getFacetMemberNames } from './lib/ctr';
4
+ export { data, getDataMemberNames, getLoggedMemberNames, input, log, output, } from './lib/data';
5
+ export { facet, getf, getm, registerFacets } from './lib/facet';
6
+ export { installPolicies } from './lib/install';
7
+ export { getOperationMemberNames, operation } from './lib/operation';
8
+ export { mapDatasToFacet, mapDataToFacet, mapDataToProp, patchFacet, } from './lib/patch';
@@ -2,4 +2,5 @@ export declare function _defaultFacetLogName(facet: any): string;
2
2
  export declare function facetLogName(facet: any): any;
3
3
  export declare function log(facet: any, operationMember: any, args: any, start: any): void;
4
4
  export declare function facetState(facet: any): {};
5
- export declare function ctrState(ctr: any): any;
5
+ export declare function defaultGetCtrState(ctr: any): any;
6
+ export declare function getCtrState(ctr: any): any;
@@ -9,13 +9,13 @@ var __assign = (this && this.__assign) || function () {
9
9
  };
10
10
  return __assign.apply(this, arguments);
11
11
  };
12
- import { getLoggedMemberNames } from "../lib/data";
13
- import { getCtrAdmin, getFacetAdmin } from "../internal/utils";
14
- import { getc, getFacetMemberNames } from "../lib/ctr";
15
- import { options } from "./options";
12
+ import { getCtrAdmin, getFacetAdmin } from '../internal/utils';
13
+ import { getc, getFacetMemberNames } from '../lib/ctr';
14
+ import { getLoggedMemberNames } from '../lib/data';
15
+ import { options } from './options';
16
16
  export function _defaultFacetLogName(facet) {
17
17
  var ctr = getc(facet);
18
- var prefix = ctr ? ctr.constructor.name + "/" : "";
18
+ var prefix = ctr ? ctr.constructor.name + '/' : '';
19
19
  return prefix + facet.constructor.name;
20
20
  }
21
21
  export function facetLogName(facet) {
@@ -25,26 +25,23 @@ export function facetLogName(facet) {
25
25
  function camelToSnake(string) {
26
26
  return string
27
27
  .replace(/[\w]([A-Z])/g, function (m) {
28
- return m[0] + "_" + m[1];
28
+ return m[0] + '_' + m[1];
29
29
  })
30
30
  .toLowerCase();
31
31
  }
32
32
  var opName = function (operationMember) { return camelToSnake(operationMember).toUpperCase(); };
33
33
  export function log(facet, operationMember, args, start) {
34
- var _a;
35
34
  var ctr = getc(facet);
36
- var ctrAdmin = ctr ? getCtrAdmin(ctr) : undefined;
37
- var getCtrState = (_a = ctrAdmin === null || ctrAdmin === void 0 ? void 0 : ctrAdmin.ctrStateOverride) !== null && _a !== void 0 ? _a : ctrState;
38
35
  var getState = ctr ? function () { return getCtrState(ctr); } : function () { return facetState(facet); };
39
36
  var operationName = opName(operationMember);
40
- var label = facetLogName(facet) + "." + operationName;
37
+ var label = facetLogName(facet) + '.' + operationName;
41
38
  if (start) {
42
39
  console.group(label);
43
- console.log("%c args: ", "color: gray", args);
44
- console.log("%c state", "color: gray", getState());
40
+ console.log('%c args: ', 'color: gray', args);
41
+ console.log('%c state', 'color: gray', getState());
45
42
  }
46
43
  else {
47
- console.log("%c next", "color: gray", getState());
44
+ console.log('%c next', 'color: gray', getState());
48
45
  // @ts-ignore
49
46
  console.groupEnd(label);
50
47
  }
@@ -61,7 +58,7 @@ export function facetState(facet) {
61
58
  }
62
59
  }, {});
63
60
  }
64
- export function ctrState(ctr) {
61
+ export function defaultGetCtrState(ctr) {
65
62
  if (ctr) {
66
63
  return getFacetMemberNames(ctr).reduce(function (acc, facetMemberName) {
67
64
  var _a;
@@ -70,3 +67,7 @@ export function ctrState(ctr) {
70
67
  }, {});
71
68
  }
72
69
  }
70
+ export function getCtrState(ctr) {
71
+ var _a, _b;
72
+ return ((_b = (_a = getCtrAdmin(ctr)) === null || _a === void 0 ? void 0 : _a.ctrStateOverride) !== null && _b !== void 0 ? _b : defaultGetCtrState)(ctr);
73
+ }
@@ -1,3 +1,3 @@
1
1
  export var symbols = {
2
- admin: Symbol("skandha administration"),
2
+ admin: Symbol('skandha administration'),
3
3
  };
package/internal/utils.js CHANGED
@@ -1,9 +1,13 @@
1
- var __spreadArray = (this && this.__spreadArray) || function (to, from) {
2
- for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
3
- to[j] = from[i];
4
- return to;
1
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
2
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
3
+ if (ar || !(i in from)) {
4
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
5
+ ar[i] = from[i];
6
+ }
7
+ }
8
+ return to.concat(ar || Array.prototype.slice.call(from));
5
9
  };
6
- import { symbols } from "./symbols";
10
+ import { symbols } from './symbols';
7
11
  export function getOrCreate(obj, key, fn) {
8
12
  if (!obj[key]) {
9
13
  obj[key] = fn();
@@ -16,7 +20,7 @@ export var zip = function (arr) {
16
20
  arrs[_i - 1] = arguments[_i];
17
21
  }
18
22
  return arr.map(function (val, i) {
19
- return arrs.reduce(function (a, arr) { return __spreadArray(__spreadArray([], a), [arr[i]]); }, [val]);
23
+ return arrs.reduce(function (a, arr) { return __spreadArray(__spreadArray([], a, true), [arr[i]], false); }, [val]);
20
24
  });
21
25
  };
22
26
  export function getCtrClassAdmin(ctrClass) {
package/lib/ctr.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getCtrAdmin, getFacetAdmin } from "../internal/utils";
1
+ import { getCtrAdmin, getFacetAdmin } from '../internal/utils';
2
2
  export function getc(facet) {
3
3
  return getFacetAdmin(facet).parentContainer;
4
4
  }
package/lib/data.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getFacetClassAdmin } from "../internal/utils";
1
+ import { getFacetClassAdmin } from '../internal/utils';
2
2
  export function log(dataHost, dataMember, descriptor) {
3
3
  var _a;
4
4
  if (descriptor === void 0) { descriptor = undefined; }
package/lib/facet.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { ClassT, GetterT, ClassMemberT } from "..";
1
+ import { ClassMemberT, ClassT, GetterT } from '..';
2
2
  export declare function facet(facetHost: any, facetMember: any, descriptor?: undefined): undefined;
3
+ export declare function facetClassName(facetClass: ClassT): any;
3
4
  export declare function registerFacets(ctr: any, options: {
4
5
  name?: string;
5
6
  members?: string[];
6
7
  ctrState?: Function;
7
8
  }): void;
8
- export declare function getf(facetClass: ClassT, ctr?: any): any;
9
+ export declare function getf(facetClass: ClassT | string, ctr?: any): any;
9
10
  export declare function getm<T = any>(classMember: ClassMemberT): GetterT<T>;
package/lib/facet.js CHANGED
@@ -1,7 +1,8 @@
1
- import { setCtr } from "./ctr";
2
- import { getCtrClassAdmin, getCtrAdmin, getFacetAdmin, } from "../internal/utils";
3
- import { options as skandhaOptions } from "../internal/options";
4
- import { ctrState as getCtrState } from "../internal/logging";
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
5
6
  export function facet(facetHost, facetMember, descriptor) {
6
7
  var _a;
7
8
  if (descriptor === void 0) { descriptor = undefined; }
@@ -10,10 +11,14 @@ export function facet(facetHost, facetMember, descriptor) {
10
11
  ctrClassAdmin.facetMembers.push(facetMember);
11
12
  return descriptor;
12
13
  }
14
+ export function facetClassName(facetClass) {
15
+ return facetClass.className ? facetClass.className() : facetClass.name;
16
+ }
17
+ // Registers members of ctr as facets in ctr. Each facet is registered by its class name.
13
18
  export function registerFacets(ctr, options) {
14
19
  var _a, _b, _c, _d, _e;
15
- var ctrName = (_a = options.name) !== null && _a !== void 0 ? _a : ctr.constructor.name;
16
20
  var ctrAdmin = getCtrAdmin(ctr);
21
+ ctrAdmin.logName = (_a = options.name) !== null && _a !== void 0 ? _a : ctr.constructor.name;
17
22
  ctrAdmin.facetMembers = (_b = ctrAdmin.facetMembers) !== null && _b !== void 0 ? _b : [];
18
23
  ctrAdmin.facetByFacetClassName = (_c = ctrAdmin.facetByFacetClassName) !== null && _c !== void 0 ? _c : {};
19
24
  ctrAdmin.ctrStateOverride = options.ctrState;
@@ -22,31 +27,34 @@ export function registerFacets(ctr, options) {
22
27
  var facet = ctr[member];
23
28
  setCtr(facet, ctr);
24
29
  var facetAdmin = getFacetAdmin(facet);
25
- facetAdmin.logName = ctrName + "/" + facet.constructor.name;
26
- var className = facet.constructor.name;
30
+ facetAdmin.logName = "".concat(ctrAdmin.logName, "/").concat(facetClassName(facet.constructor));
31
+ var className = facetClassName(facet.constructor);
27
32
  if (ctrAdmin.facetByFacetClassName[className] !== undefined) {
28
- console.error("Two facets of same type " + className + " in container " + ctrName);
33
+ console.error("Two facets of same type ".concat(className, " in container ").concat(ctrAdmin.logName));
29
34
  }
30
35
  ctrAdmin.facetByFacetClassName[className] = facet;
31
- if (skandhaOptions.logging) {
32
- console.log("%c Ctr initialized", "color: gray", getCtrState(ctr));
33
- }
34
36
  });
37
+ if (skandhaOptions.logging) {
38
+ console.log('%c %s ctr initialized', 'color: gray', ctrAdmin.logName, getCtrState(ctr));
39
+ }
35
40
  }
36
41
  export function getf(facetClass, ctr) {
37
42
  var _a;
38
43
  if (!ctr)
39
44
  return function (ctr) { return getf(facetClass, ctr); };
40
- var facet = (_a = getCtrAdmin(ctr).facetByFacetClassName) === null || _a === void 0 ? void 0 : _a[facetClass.name];
45
+ var facet = typeof facetClass === 'string'
46
+ ? ctr[facetClass]
47
+ : (_a = getCtrAdmin(ctr).facetByFacetClassName) === null || _a === void 0 ? void 0 : _a[facetClassName(facetClass)];
41
48
  if (!facet) {
42
- console.error("No facet " + facetClass.name + " in container " + ctr.constructor.name);
49
+ var facetName = facetClass === 'string' ? facetClass : facetClassName(facetClass);
50
+ console.error("No facet ".concat(facetName, " in container ").concat(getCtrAdmin(ctr).logName));
43
51
  }
44
52
  return facet;
45
53
  }
46
54
  export function getm(classMember) {
47
55
  var f = function (ctr) { var _a; return getf(classMember[0], (_a = classMember[2]) !== null && _a !== void 0 ? _a : ctr)[classMember[1]]; };
48
- // Set some properties on f to easy debugging later
49
- f.facetClassName = classMember[0].name;
56
+ // Set some properties on f to ease debugging later
57
+ f.facetClassName = facetClassName(classMember[0]);
50
58
  f.facetMemberName = classMember[1];
51
59
  return f;
52
60
  }
package/lib/operation.js CHANGED
@@ -34,9 +34,9 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
34
34
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
35
35
  }
36
36
  };
37
- import { options } from "../internal/options";
38
- import { log } from "../internal/logging";
39
- import { getFacetClassAdmin } from "../internal/utils";
37
+ import { log } from '../internal/logging';
38
+ import { options } from '../internal/options';
39
+ import { getFacetClassAdmin } from '../internal/utils';
40
40
  function _operation(target, propertyName, descriptor, punctualOptions) {
41
41
  var _a;
42
42
  var f = descriptor.value;
@@ -44,7 +44,7 @@ function _operation(target, propertyName, descriptor, punctualOptions) {
44
44
  facetClassAdmin.operationMemberNames =
45
45
  (_a = facetClassAdmin.operationMemberNames) !== null && _a !== void 0 ? _a : [];
46
46
  facetClassAdmin.operationMemberNames.push(propertyName);
47
- if (typeof descriptor.value === "function") {
47
+ if (typeof descriptor.value === 'function') {
48
48
  descriptor.value = !!punctualOptions.async
49
49
  ? function () {
50
50
  var _a;
package/lib/patch.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { GetterT, ClassMemberT } from "..";
1
+ import { ClassMemberT, GetterT } from '..';
2
2
  export declare function mapDataToProp(facet: any, prop: string, getter: () => any): void;
3
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 | undefined) => (ctr: any) => void;
4
+ export declare const mapDataToFacet: ([toFacetClass, toMember, toCtr]: ClassMemberT, getter: GetterT, transform?: Function) => (ctr: any) => void;
5
5
  export declare const mapDatasToFacet: ([toFacetClass, toMember, toCtr]: ClassMemberT, getters: Array<GetterT>, transform: Function) => (ctr: any) => void;
package/lib/patch.js CHANGED
@@ -9,9 +9,9 @@ var __assign = (this && this.__assign) || function () {
9
9
  };
10
10
  return __assign.apply(this, arguments);
11
11
  };
12
- import { getf } from "./facet";
13
- import { facetLogName } from "../internal/logging";
14
- import { getDataMemberNames } from "./data";
12
+ import { facetLogName } from '../internal/logging';
13
+ import { getDataMemberNames } from './data';
14
+ import { getf } from './facet';
15
15
  export function mapDataToProp(facet, prop, getter) {
16
16
  delete facet[prop];
17
17
  Object.defineProperty(facet, prop, {
@@ -26,19 +26,19 @@ export function patchFacet(facet, members, options) {
26
26
  for (var prop in members) {
27
27
  if ((_a = options === null || options === void 0 ? void 0 : options.warnIfNotADataMember) !== null && _a !== void 0 ? _a : true) {
28
28
  if (!dataMemberNames.includes(prop)) {
29
- console.error("Patching a property " + prop + " that wasn't decorated with " +
30
- ("@data, @input or @output in " + facetLogName(facet)));
29
+ console.error("Patching a property ".concat(prop, " that wasn't decorated with ") +
30
+ "@data, @input or @output in ".concat(facetLogName(facet)));
31
31
  }
32
32
  }
33
33
  try {
34
34
  delete facet[prop];
35
35
  }
36
36
  catch (_b) {
37
- console.error("Could not delete property " + prop + " from facet " + 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.");
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
42
  }
43
43
  Object.defineProperty(facet, prop, __assign(__assign({}, members[prop]), { enumerable: true, configurable: true }));
44
44
  }
@@ -50,6 +50,8 @@ export var mapDataToFacet = function (_a, getter, transform) {
50
50
  var patch = (_a = {},
51
51
  _a[toMember] = {
52
52
  get: function () {
53
+ // This context variable is not necessary, it has redundant data and
54
+ // could be inlined, but it makes debugging easier.
53
55
  var context = {
54
56
  ctr: toCtr !== null && toCtr !== void 0 ? toCtr : ctr,
55
57
  getter: getter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skandha",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Create data containers with modular behaviours",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/mnieber/skandha.git",