skandha 0.6.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,27 +2,7 @@
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.
15
-
16
- ---
17
-
18
- **NOTE**
19
-
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`.
24
-
25
- ---
5
+ The goal of SkandhaJS is to provide data containers that have reusable behaviors, such as selection, highlighting, filtering, drag-and-drop, etc. The only requirement for using these containers is 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 be highlighted. As we will see, these interactions are achieved by implementing callback functions.
26
6
 
27
7
  ## Links
28
8
 
@@ -30,322 +10,254 @@ facet names by adding a property in the facet class, or specify the names when c
30
10
  - The [skandha-facets](http://github.com/mnieber/skandha-facets) library contains useful facets such as selection, highlight, filtering, addition, etc.
31
11
  - The [skandha-mobx](http://github.com/mnieber/skandha-mobx) library contains bindings to MobX
32
12
  - The [aspiration](http://github.com/mnieber/aspiration) library handles aspect oriented programming (AOP)
13
+ - The [example code](http://github.com/mnieber/skandha-sample-app) shown in this README.
33
14
 
34
- ## Example code
15
+ ## Glossary (conceptual)
35
16
 
36
- 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
38
- 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
- from [skandha-facets](http://github.com/mnieber/skandha-facets).
17
+ - callbacks object: a collection of callback functions that is used by an operation.
18
+ - container: a class that contains multiple facets.
19
+ - facet: a class that implements a reusable behaviour, such as selection.
20
+ - operation: a member function of a facet that changes the data of that facet.
40
21
 
41
- ## Explanation
22
+ ## Glossary (technical)
42
23
 
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.
24
+ - aspiration: a library that implements a form of Aspect Oriented Programming using callback functions.
25
+ - registerCtr: the function that tells SkandhaJS which containers and facets exist.
26
+ - setCallbacks: the function that installs the callbacks objects that are used by a facet.
46
27
 
47
- ### Containers and Facets
28
+ ## Example: a simplified Selection facet
48
29
 
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.
30
+ In Skandha, behaviours (such as selection, or filtering) are implemented in so-called facets that live in containers. For example, a todo container may have facets for selecting, highlighting and filtering todos. Because facets are reusable, they can also be used in other containers. And because facets are abstractions, you can use them to create UI components that handle selection in any container. In this example, we will show a simplified Selection facet class.
53
31
 
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.
32
+ ```
33
+ // file: Selection.ts
34
+ import { input, output, operation } from 'skandha';
35
+
36
+ export type SelectionParamsT = { itemId?: string; isShift?: boolean; isCtrl?: boolean; };
58
37
 
59
- Our initial code is:
38
+ export class Selection<ValueT = any> {
39
+ static className = () => 'Selection';
60
40
 
41
+ @input selectableIds: Array<string> = [];
42
+ @output ids: Array<string> = [];
43
+ @output anchorId?: string;
44
+ @output items: Array<ValueT> = [];
45
+
46
+ @operation selectItem(selectionParams: SelectionParamsT) {
47
+ // This function calls a callback function to do the actual work.
48
+ // Details about how this works are omitted for now.
49
+ }
50
+ }
61
51
  ```
62
- class Selection<TodoT> {}
63
52
 
64
- class Highlight<TodoT> {}
53
+ ## Fact: facets have @input, @data and @output members
65
54
 
66
- class Filtering<TodoT> {}
55
+ In `Selection.ts` we see the Selection facet class. It has a `selectableIds` field that stores the list of ids from which we can select. It is marked with the `@input` decorator because this field stores an input. The `ids`, `anchorId` and `items` fields are marked as `@output` fields. They are used to store the result of calling `selectItem()`.
67
56
 
68
- class Inputs {
69
- @data todoById: TodoByIdT = {};
70
- }
57
+ ## Fact: operations are decorated with `@operation`.
71
58
 
72
- class Outputs {
73
- @data filteredTodoById: TodoByIdT = {};
74
- }
59
+ Conceptually, an operation is a member function that is used to manipulate the facet. The `Selection` facet has a `selectItems` operation that allows you to select from the selectable ids. It stores its results in the `ids`, `anchorId` and `items` fields.
60
+ As the comment mentions, `selectItem()` calls a callback function to do the actual work. The reason is that the correct way to make a selection (e.g. what should happen if shift is pressed?) depends on the use-case. Therefore, we want to leave it up to the client to decide this.
75
61
 
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
- }
83
- ```
62
+ ## Fact: operations are logged
84
63
 
85
- Notes:
64
+ As a side effect of calling an operation, the entire container is logged to the console; this includes the Selection facet, and any other facets that the container may have. The log message contains all @input, @data and @output fields of all facets in the container.
86
65
 
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.
66
+ ## Example: a container with Selection, Highlight and Filtering
89
67
 
90
- ### Implementing the Selection facet
68
+ Now that we have some idea of what a facet is, we will discuss containers.
69
+ In this example we will take a look at a todos container. It uses the `Selection` facet that we saw earlier.
91
70
 
92
- We will first implement the Selection facet. The key requirements are that:
71
+ ```
72
+ // file: createContainer.ts
73
+ import { Selection_selectItem } from 'SelectionCbs';
74
+
75
+ const createContainer() {
76
+ const ctr = {
77
+ selection: new Selection(),
78
+ highlight: new Highlight(),
79
+ filtering: new Filtering()
80
+ };
81
+
82
+ setCallbacks(ctr.selection, {
83
+ selectItem: {
84
+ select(this: Selection_selectItem): {
85
+ handleSelectItem(ctr.selection, this.selectionParams);
86
+ highlightFollowsSelection(ctr, this.selectionParams);
87
+ },
88
+ }
89
+ });
93
90
 
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.
91
+ setCallbacks(ctr.filtering, {
92
+ apply: {
93
+ exit() {
94
+ highlightIsCorrectedOnFilterChange(ctr);
95
+ },
96
+ },
97
+ });
100
98
 
101
- This is our proposal for the Selection facet:
99
+ registerCtr({ ctr: ctr, options: { name: 'Todos'} });
100
+ return ctr;
101
+ }
102
+ ```
102
103
 
103
104
  ```
104
- export class Selection_select extends Cbs {
105
- selectionParams: SelectionParamsT = stub();
106
- selectItem() {}
105
+ // file: highlightFollowsSelection.ts
106
+ const highlightFollowsSelection = (ctr: any, selectionParams: SelectionParamsT) => {
107
+ if (!selectionParams.isShift && !selectionParams.isCtrl) {
108
+ ctr.highlight.highlightItem(selectionParams.itemId);
109
+ }
107
110
  }
111
+ ```
108
112
 
109
- class Selection<TodoT> {
110
- @data selectableIds: Array<string> = stub();
111
- @data ids: Array<string> = [];
112
- @data anchorId?: string;
113
- @data items?: Array<ValueT>;
114
-
115
- @operation @host selectItem(selectionParams: SelectionParamsT) {
116
- return (cbs: Selection_select) => {
117
- if (!this.selectableIds.includes(selectionParams.itemId)) {
118
- throw Error(`Invalid id: ${selectionParams.itemId}`);
119
- }
120
- cbs.selectItem();
121
- }
113
+ ```
114
+ // file: handleSelectItem.ts
115
+ export function handleSelectItem(
116
+ facet: Selection,
117
+ { itemId, isShift, isCtrl }: SelectionParamsT
118
+ ) {
119
+ if (itemId === undefined) {
120
+ facet.ids = [];
121
+ facet.anchorId = undefined;
122
+ return;
123
+ }
124
+
125
+ if (isShift) {
126
+ // Omitted for brevity
127
+ } else if (isCtrl) {
128
+ facet.ids = facet.ids.includes(itemId)
129
+ ? facet.ids.filter((x) => x !== itemId)
130
+ : [...facet.ids, itemId];
131
+ } else {
132
+ facet.ids = [itemId];
133
+ }
134
+
135
+ // Move the anchor
136
+ if (!facet.anchorId || !(isCtrl || isShift)) {
137
+ facet.anchorId = itemId;
122
138
  }
123
139
  }
124
140
  ```
125
141
 
126
- Notes:
142
+ ## Note: the todos container has selection, highlight and filtering
127
143
 
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.
144
+ In `createContainer.ts` we see how `createContainer()` creates a `ctr` object that has three facets: selection, highlight and filtering. After creating the container, the `installCallbacks` function is called to determine how the facet operations are implemented. Finally, `registerCtr()` is called to allow SkandhaJS to introspect the container. This is necessary for two reasons. First, it makes it possible to look up a facet by its name or class, e.g. `getf(ctr, Selection)` returns `ctr.selection`. Second, it informs SkandhaJS how to log all facets of the container
134
145
 
135
- ### Implementing the Highlight and Filtering facets
146
+ ## Fact: callbacks are installed with `setCallbacks`
136
147
 
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):
148
+ We call `setCallbacks` to install a callbacks object for each operation of the `Selection` facet. The callbacks object contains all callbacks for an operation. In our case we only have one operation (`selectItem()`) that has only one callback (`select()`). This callback function calls `handleSelectItem` to take care of doing the selection. This is a typical pattern: you want the call to `setCallbacks` to be as short and readable as possible, so that it's easy to understand what happens in each operation of each facet.
140
149
 
141
- ```
142
- export class Highlight<ValueT = any> {
143
- @data id: string | undefined;
144
- @data item?: ValueT;
150
+ ## Fact: callbacks can take care of side effects
145
151
 
146
- @operation @host highlightItem(id: string) {
147
- return (cbs: Highlight_highlightItem) => {
148
- this.id = id;
149
- };
150
- }
151
- }
152
+ An important advantage of using callbacks is that they can also take care of side effects. In this case, it calls `highlightFollowsSelection()` to ensure that selected items are also highlighted. Note that the callback function is able to connect selection and highlight because it has access to the entire container.
152
153
 
153
- export class Filtering<ValueT = any> {
154
- @data isEnabled: boolean = false;
155
- @data filter: FilterT = () => [];
154
+ ## Fact: the callback function can access the callbacks object through `this`.
156
155
 
157
- @data inputItems?: Array<ValueT>;
158
- @data get filteredItems() {
159
- return this.isEnabled ? this.filter(this.inputItems) : this.inputItems;
160
- }
156
+ When the clients calls `ctr.selection.selectItem()` and `select` is called back, then all arguments of the operation are available through `this`. In our case, we see that the `select` callback function is accessing `this.selectionParams`.
161
157
 
162
- @operation @host apply(filter: FilterT) {
163
- return (cbs: Filtering_apply) => {
164
- this.filter = filter;
165
- this.isEnabled = true;
166
- };
167
- }
158
+ ## Fact: the `exit` callback function is called at the end of an operation
168
159
 
169
- @operation @host setEnabled(flag: boolean) {
170
- return (cbs: Filtering_setEnabled) => {
171
- this.isEnabled = flag;
160
+ In `createContainer.ts` we also have a `Filtering` facet that has an `apply(f)` operation (where `f` is the filter function). In this operation, we install the `exit()` callback that corrects the highlight when the filter function changes (because the new filter may hide the highlighted item). The `exit()` callback is called automatically at the end of each operation. Similarly, the `enter()` callback is called at the beginning.
161
+
162
+ ## Example: a complete Selection facet
163
+
164
+ It's time now to return to the Selection facet and show how callbacks really work.
165
+
166
+ ```
167
+ // file: Selection.ts
168
+ import { input, output, operation } from 'skandha';
169
+ import { host, stub } from 'aspiration';
170
+ import { Selection_selectItem, SelectionParamsT } from 'SelectionCbs';
171
+
172
+ export class Selection<ValueT = any> {
173
+ static className = () => 'Selection';
174
+
175
+ @input selectableIds: Array<string> = stub();
176
+ @output ids: Array<string> = [];
177
+ @output anchorId?: string;
178
+ @output items: Array<ValueT> = stub();
179
+
180
+ @operation @host(['selectionParams']) selectItem(
181
+ selectionParams: SelectionParamsT
182
+ ) {
183
+ return (cbs: Selection_selectItem) => {
184
+ cbs.select();
172
185
  };
173
186
  }
174
187
  }
175
188
  ```
176
189
 
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
-
192
190
  ```
193
- class TodosCtr {
194
- // ...
195
-
196
- constructor() {
197
- registerFacets(this);
198
- this._setCallbacks();
199
- }
191
+ // file: SelectionCbs.ts
192
+ import { stub } from 'aspiration';
200
193
 
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
- });
212
-
213
- setCallbacks(this.filtering, {
214
- apply: {
215
- exit() {
216
- FacetPolicies.highlightIsCorrectedOnFilterChange(ctr.filtering);
217
- },
218
- },
219
- });
220
- }
221
- }
194
+ export type SelectionParamsT = { itemId?: string; isShift?: boolean; isCtrl?: boolean; };
222
195
 
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
- }
196
+ export class Selection_selectItem extends Cbs {
197
+ selectionParams: SelectionParamsT = stub();
198
+ select() {}
232
199
  }
233
200
  ```
234
201
 
235
- Notes:
202
+ ## Fact: the `@host` decorator is used to enable callbacks in an operation.
236
203
 
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.
204
+ The `@host` decorator comes from the `Aspiration` library. When you call a function `f` that is decorated with `@host` then the following happens:
240
205
 
241
- ### Mapping data onto the container, and between facets
206
+ - Aspiration looks up the callbacks object for the operation (that was installed with `setCallbacks`)
207
+ - it copies the operation arguments to the callbacks object (so that callback functions can access these arguments)
208
+ - if calls `f(arguments)(callbacksObject)`.
242
209
 
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).
210
+ Remember that `f` is the original function. We see from the example that `f` returns a closure. This closure receives the callbacks object, but (because it's a closure) it also has access to the arguments of f. This approach allows the client code to call `f` in the usual way; it can be agnostic of the fact that callbacks are used to help implement `f`.
248
211
 
249
- As a first step, we need to connect `Filtering.inputItems` to `Inputs.todoById` and `Outputs.filteredTodoById`.
250
- We can think of this as setting up a pipeline, where one of more source facet members are mapped onto a destination
251
- facet member:
212
+ ## Fact: callback functions have access to all operation arguments
252
213
 
253
- ```
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
- }
269
- ```
214
+ As we mentioned earlier, each callback function of the callbacks object has access to all arguments of the operation. For this to work, it's required that you pass the list of argument names to the `@host` decorator (unfortunately, typescript does not have introspection capabilities that allow to automate this). These names need to match the corresponding fields of the callbacks object class (`Selection_selectItem`).
270
215
 
271
- Notes:
216
+ ## Fact: facets have a static `className()` function
272
217
 
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.
278
- - 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))`,
218
+ To allow SkandhaJS to introspect the container, you need to add the `className()` function to each facet class, so that Skandha knows the name of the facet.
280
219
 
281
- As the next step, we will connect the list of filtered items to `Selection.selectableIds` (so that selection
282
- happens in the filtered list). In addition, we will add datamappings for `Selection.items`
283
- and `Selection.item`:
220
+ ## Example: a Connector maps data onto each facet
221
+
222
+ We've seen how facets are implemented, and how they can be added to a container in a way that allows us to create interactions between them. Since our container contains selection and filtering, we should only allow selection of todos that have not been filtered out. In other words, the list of selectable ids should be derived from the list of filtered todos. Moreover, we need to load the todos and set them as the inputs of the filtering facet. In this example, we show two containers (data and todos) and how to map data between them.
284
223
 
285
224
  ```
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);
225
+ // file: createContainer.ts
226
+ import * from 'ramda' as R;
227
+
228
+ const connectContainer(ctr: any) {
229
+ const data = {
230
+ inputs: { todos: loadTodos() },
231
+ outputs: { filteredTodoById: {} },
232
+ };
233
+
234
+ const todos = {
235
+ selection: new Selection(),
236
+ highlight: new Highlight(),
237
+ filtering: new Filtering(),
312
238
  }
313
- ```
314
239
 
315
- 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:
240
+ const con = createConnector({ data: data, todos: todos });
241
+ const lookUpTodo = (id: string) => outputsCtr.filteredTodoById[id];
317
242
 
243
+ con['todos.filtering'].inputItems = con['inputs'].todos;
244
+ con['outputs'].filteredTodoById = con['todos.filtering'].filteredItems.tf(R.indexBy('id));
245
+ con['todos.selection'].selectableIds = con['outputs'].filteredTodoById.tf(R.keys);
246
+ con['todos.selection'].item = con['todos.selection'].ids.tf(lookUpTodos);
247
+ con['todos.highlight'].item = con['todos.highlight'].id.tf(lookUpTodo);
248
+
249
+ con.connect();
318
250
  ```
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
- }
341
- ```
251
+
252
+ ## Fact: the Connector class maps data between objects
253
+
254
+ The Connector instance allows you to map data from one object onto another object.
255
+ In our example, the `data.inputs.todos` field is mapped onto the `todos.filtering.inputItems` field. What this means is that `Connector` turns `todos.filtering.inputItems` into a `get` property that returns `data.inputs.todos`.
342
256
 
343
257
  ### Logging
344
258
 
345
- By calling `setOptions({logging: true})`, the Skandha library allows you to inspect each facet before and
346
- after calling an operation (remember that an operation is any facet member function decorated with @operation).
347
- This will log Facet members that are decorated with `@data` in a way that looks similar to what you are
348
- used to from Redux:
259
+ By calling `setOptions({logging: true})`, the SkandhaJS library allows you to inspect each facet before and after calling an operation.
260
+ All facet members that are decorated with `@data` (or `@input`, or `@output`) are logged in a way that looks similar to what you are used to from Redux:
349
261
 
350
262
  - before and after each operation, the entire container that holds the facet is logged
351
263
  - all data members of each facet in the container are included in the log
@@ -353,25 +265,14 @@ used to from Redux:
353
265
 
354
266
  ### Making facets observable with Mobx
355
267
 
356
- If you want to render the facets with React then we need to let React know when data changes.
357
- 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:
268
+ If you want to render the facets with React then we need to let React know when data changes. The [skandha-mobx](http://github.com/mnieber/skandha-mobx) library was created for this purpose. It has a `registerCtr` function that turns each `@data` member into a Mobx property that is
269
+ either observable or computed:
361
270
 
362
271
  ```
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
- }
272
+ registerCtr({
273
+ ctr: ctr,
274
+ options: { name: 'Todos'},
275
+ });
375
276
  ```
376
277
 
377
278
  ## Conclusion
@@ -384,5 +285,4 @@ a policy that says that changing the highlight disables editing. By using facets
384
285
  editing-specific code directly in the highlight function (that would obviously be mixing concerns).
385
286
 
386
287
  In my experience, using SkandhaJS leads to code reuse and improved structure. On top of that, it offers a good
387
- debugging experience (yes, you will still have to debug) by logging all changes in the container. Finally,
388
- it works nicely with MobX to render the container contents.
288
+ 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
- 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 { createConnector } from './lib/Connector';
4
+ export { getc, getFacetMemberNames } from './lib/ctr';
5
+ export { data, getDataMemberNames, getLoggedMemberNames, input, log, output, } from './lib/data';
6
+ export { getf } from './lib/facet';
7
+ export { installPolicies } from './lib/install';
8
+ export { getOperationMemberNames, operation } from './lib/operation';
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
- 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 { createConnector } from './lib/Connector';
4
+ export { getc, getFacetMemberNames } from './lib/ctr';
5
+ export { data, getDataMemberNames, getLoggedMemberNames, input, log, output, } from './lib/data';
6
+ export { getf } from './lib/facet';
7
+ export { installPolicies } from './lib/install';
8
+ export { getOperationMemberNames, operation } from './lib/operation';
9
+ export { mapDataToProp } from './lib/patch';
10
+ export { registerCtr } from './lib/registerCtr';
@@ -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
  };
@@ -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
@@ -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,12 +20,9 @@ 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
- export function getCtrClassAdmin(ctrClass) {
23
- return getOrCreate(ctrClass, symbols.admin, function () { return ({}); });
24
- }
25
26
  export function getCtrAdmin(ctr) {
26
27
  return getOrCreate(ctr, symbols.admin, function () { return ({}); });
27
28
  }
@@ -0,0 +1,5 @@
1
+ declare type OptionsT = {
2
+ strict: boolean;
3
+ };
4
+ export declare const createConnector: (obj: any, options?: OptionsT) => any;
5
+ export {};
@@ -0,0 +1,107 @@
1
+ import { mapDataToProp } from './patch';
2
+ function _path(path, obj) {
3
+ var result = obj;
4
+ for (var _i = 0, _a = path.split('.'); _i < _a.length; _i++) {
5
+ var part = _a[_i];
6
+ result = result === null || result === void 0 ? void 0 : result[part];
7
+ if (result === undefined)
8
+ return result;
9
+ }
10
+ return result;
11
+ }
12
+ var Connector = /** @class */ (function () {
13
+ function Connector(obj, options) {
14
+ var _this = this;
15
+ var _a;
16
+ this.entityByKey = {};
17
+ this.getOrCreateEntity = function (key) {
18
+ if (_this.entityByKey[key])
19
+ return _this.entityByKey[key];
20
+ var obj = _path(key, _this.obj);
21
+ var entity = new Proxy(new Entity(obj), {
22
+ get: function (target, key) {
23
+ return target[key] || target.getOrCreateField(key);
24
+ },
25
+ set: function (target, key, value) {
26
+ target.fieldByName[key] = value;
27
+ return true;
28
+ },
29
+ });
30
+ _this.entityByKey[key] = entity;
31
+ return entity;
32
+ };
33
+ this.connect = function () {
34
+ for (var _i = 0, _a = Object.values(_this.entityByKey); _i < _a.length; _i++) {
35
+ var entity = _a[_i];
36
+ _this._connectEntity(entity);
37
+ }
38
+ };
39
+ this._connectEntity = function (entity) {
40
+ var _loop_1 = function (entry) {
41
+ var name_1 = entry[0], field = entry[1];
42
+ if (_this.options.strict && !(name_1 in entity.obj)) {
43
+ throw new Error("Cannot override the field ".concat(name_1, " in ").concat(entity.obj, ".") +
44
+ 'Field not found. Disable strict mode to ignore this error.');
45
+ }
46
+ mapDataToProp(entity.obj, name_1, function () {
47
+ return field.getSource();
48
+ });
49
+ };
50
+ for (var _i = 0, _a = Object.entries(entity.fieldByName); _i < _a.length; _i++) {
51
+ var entry = _a[_i];
52
+ _loop_1(entry);
53
+ }
54
+ };
55
+ this.options = {
56
+ strict: (_a = options === null || options === void 0 ? void 0 : options.strict) !== null && _a !== void 0 ? _a : true,
57
+ };
58
+ this.obj = obj;
59
+ }
60
+ return Connector;
61
+ }());
62
+ export var createConnector = function (obj, options) {
63
+ var con = new Connector(obj, options);
64
+ return new Proxy(con, {
65
+ get: function (target, key) {
66
+ var _a;
67
+ return (_a = target[key]) !== null && _a !== void 0 ? _a : target.getOrCreateEntity(key);
68
+ },
69
+ });
70
+ };
71
+ var Entity = /** @class */ (function () {
72
+ function Entity(obj) {
73
+ var _this = this;
74
+ this.fieldByName = {};
75
+ this.getOrCreateField = function (name) {
76
+ var _a;
77
+ // If we create a new field then we should *not* store it in fieldByName.
78
+ return (_a = _this.fieldByName[name]) !== null && _a !== void 0 ? _a : new Field(_this.obj, name);
79
+ };
80
+ this.obj = obj;
81
+ }
82
+ return Entity;
83
+ }());
84
+ var Field = /** @class */ (function () {
85
+ function Field(obj, name) {
86
+ var _this = this;
87
+ this.transforms = [];
88
+ this.getSource = function () {
89
+ if (!(_this.name in _this.obj)) {
90
+ throw new Error("Field ".concat(_this.name, " does not exist on object ").concat(_this.obj));
91
+ }
92
+ var result = _this.obj[_this.name];
93
+ for (var _i = 0, _a = _this.transforms; _i < _a.length; _i++) {
94
+ var tf = _a[_i];
95
+ result = tf(result);
96
+ }
97
+ return result;
98
+ };
99
+ this.obj = obj;
100
+ this.name = name;
101
+ }
102
+ Field.prototype.tf = function (transform) {
103
+ this.transforms.push(transform);
104
+ return this;
105
+ };
106
+ return Field;
107
+ }());
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,3 @@
1
- import { ClassT, GetterT, ClassMemberT } from "..";
2
- export declare function facet(facetHost: any, facetMember: any, descriptor?: undefined): undefined;
3
- export declare function registerFacets(ctr: any, options: {
4
- name?: string;
5
- members?: string[];
6
- ctrState?: Function;
7
- }): void;
8
- export declare function getf(facetClass: ClassT, ctr?: any): any;
9
- export declare function getm<T = any>(classMember: ClassMemberT): GetterT<T>;
1
+ import { ClassT } from '..';
2
+ export declare function facetClassName(facetClass: ClassT): any;
3
+ export declare function getf(facetClass: ClassT | string, ctr?: any): any;
package/lib/facet.js CHANGED
@@ -1,52 +1,17 @@
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";
5
- export function facet(facetHost, facetMember, descriptor) {
6
- var _a;
7
- if (descriptor === void 0) { descriptor = undefined; }
8
- var ctrClassAdmin = getCtrClassAdmin(facetHost.constructor);
9
- ctrClassAdmin.facetMembers = (_a = ctrClassAdmin.facetMembers) !== null && _a !== void 0 ? _a : [];
10
- ctrClassAdmin.facetMembers.push(facetMember);
11
- return descriptor;
12
- }
13
- export function registerFacets(ctr, options) {
14
- var _a, _b, _c, _d, _e;
15
- var ctrName = (_a = options.name) !== null && _a !== void 0 ? _a : ctr.constructor.name;
16
- var ctrAdmin = getCtrAdmin(ctr);
17
- ctrAdmin.facetMembers = (_b = ctrAdmin.facetMembers) !== null && _b !== void 0 ? _b : [];
18
- ctrAdmin.facetByFacetClassName = (_c = ctrAdmin.facetByFacetClassName) !== null && _c !== void 0 ? _c : {};
19
- ctrAdmin.ctrStateOverride = options.ctrState;
20
- ((_e = (_d = options.members) !== null && _d !== void 0 ? _d : getCtrClassAdmin(ctr.constructor).facetMembers) !== null && _e !== void 0 ? _e : Object.getOwnPropertyNames(ctr)).forEach(function (member) {
21
- ctrAdmin.facetMembers.push(member);
22
- var facet = ctr[member];
23
- setCtr(facet, ctr);
24
- var facetAdmin = getFacetAdmin(facet);
25
- facetAdmin.logName = ctrName + "/" + facet.constructor.name;
26
- var className = facet.constructor.name;
27
- if (ctrAdmin.facetByFacetClassName[className] !== undefined) {
28
- console.error("Two facets of same type " + className + " in container " + ctrName);
29
- }
30
- ctrAdmin.facetByFacetClassName[className] = facet;
31
- if (skandhaOptions.logging) {
32
- console.log("%c Ctr initialized", "color: gray", getCtrState(ctr));
33
- }
34
- });
1
+ import { getCtrAdmin } from '../internal/utils';
2
+ export function facetClassName(facetClass) {
3
+ return facetClass.className ? facetClass.className() : facetClass.name;
35
4
  }
36
5
  export function getf(facetClass, ctr) {
37
6
  var _a;
38
7
  if (!ctr)
39
8
  return function (ctr) { return getf(facetClass, ctr); };
40
- var facet = (_a = getCtrAdmin(ctr).facetByFacetClassName) === null || _a === void 0 ? void 0 : _a[facetClass.name];
9
+ var facet = typeof facetClass === 'string'
10
+ ? ctr[facetClass]
11
+ : (_a = getCtrAdmin(ctr).facetByFacetClassName) === null || _a === void 0 ? void 0 : _a[facetClassName(facetClass)];
41
12
  if (!facet) {
42
- console.error("No facet " + facetClass.name + " in container " + ctr.constructor.name);
13
+ var facetName = facetClass === 'string' ? facetClass : facetClassName(facetClass);
14
+ console.error("No facet ".concat(facetName, " in container ").concat(getCtrAdmin(ctr).logName));
43
15
  }
44
16
  return facet;
45
17
  }
46
- export function getm(classMember) {
47
- 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;
50
- f.facetMemberName = classMember[1];
51
- return f;
52
- }
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 @@
1
- import { GetterT, ClassMemberT } 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 | undefined) => (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 { getf } from "./facet";
13
- import { facetLogName } from "../internal/logging";
14
- import { getDataMemberNames } from "./data";
15
1
  export function mapDataToProp(facet, prop, getter) {
16
2
  delete facet[prop];
17
3
  Object.defineProperty(facet, prop, {
@@ -20,66 +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 " + prop + " that wasn't decorated with " +
30
- ("@data, @input or @output in " + facetLogName(facet)));
31
- }
32
- }
33
- try {
34
- delete facet[prop];
35
- }
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.");
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
- var context = {
54
- ctr: toCtr !== null && toCtr !== void 0 ? toCtr : ctr,
55
- getter: getter,
56
- toClassMember: [toFacetClass, toMember],
57
- data: getter(ctr),
58
- };
59
- return transform ? transform(context.data) : context.data;
60
- },
61
- },
62
- _a);
63
- patchFacet(getf(toFacetClass, toCtr !== null && toCtr !== void 0 ? toCtr : ctr), patch);
64
- };
65
- };
66
- export var mapDatasToFacet = function (_a, getters, transform) {
67
- var toFacetClass = _a[0], toMember = _a[1], toCtr = _a[2];
68
- return function (ctr) {
69
- var _a;
70
- var patch = (_a = {},
71
- _a[toMember] = {
72
- get: function () {
73
- var context = {
74
- ctr: toCtr !== null && toCtr !== void 0 ? toCtr : ctr,
75
- getters: getters,
76
- toClassMember: [toFacetClass, toMember],
77
- datas: getters.map(function (getter) { return getter(ctr); }),
78
- };
79
- return transform.apply(void 0, context.datas);
80
- },
81
- },
82
- _a);
83
- patchFacet(getf(toFacetClass, toCtr !== null && toCtr !== void 0 ? toCtr : ctr), patch);
84
- };
85
- };
@@ -0,0 +1,6 @@
1
+ export declare type OptionsT = {
2
+ name: string;
3
+ facets?: 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.facets) !== null && _c !== void 0 ? _c : Object.getOwnPropertyNames(ctr)).forEach(function (facetName) {
14
+ var facet = ctr[facetName];
15
+ setCtr(facet, ctr);
16
+ ctrAdmin.facetMembers.push(facetName);
17
+ if (!facet.constructor.className) {
18
+ console.error("Facet ".concat(facetName, " 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.6.0",
3
+ "version": "0.9.1",
4
4
  "description": "Create data containers with modular behaviours",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/mnieber/skandha.git",