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