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 +190 -290
- package/index.d.ts +11 -11
- package/index.js +10 -8
- package/internal/logging.d.ts +2 -1
- package/internal/logging.js +15 -14
- package/internal/symbols.js +1 -1
- package/internal/utils.d.ts +0 -1
- package/internal/utils.js +10 -9
- package/lib/Connector.d.ts +5 -0
- package/lib/Connector.js +107 -0
- package/lib/ctr.js +1 -1
- package/lib/data.js +1 -1
- package/lib/facet.d.ts +3 -9
- package/lib/facet.js +8 -43
- package/lib/operation.js +4 -4
- package/lib/patch.d.ts +0 -4
- package/lib/patch.js +0 -77
- package/lib/registerCtr.d.ts +6 -0
- package/lib/registerCtr.js +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,27 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Rationale
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
15
|
+
## Glossary (conceptual)
|
|
35
16
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
##
|
|
22
|
+
## Glossary (technical)
|
|
42
23
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
28
|
+
## Example: a simplified Selection facet
|
|
48
29
|
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
## Fact: facets have @input, @data and @output members
|
|
65
54
|
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
@data todoById: TodoByIdT = {};
|
|
70
|
-
}
|
|
57
|
+
## Fact: operations are decorated with `@operation`.
|
|
71
58
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
Clients should use only these fields.
|
|
66
|
+
## Example: a container with Selection, Highlight and Filtering
|
|
89
67
|
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
setCallbacks(ctr.filtering, {
|
|
92
|
+
apply: {
|
|
93
|
+
exit() {
|
|
94
|
+
highlightIsCorrectedOnFilterChange(ctr);
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
registerCtr({ ctr: ctr, options: { name: 'Todos'} });
|
|
100
|
+
return ctr;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
102
103
|
|
|
103
104
|
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
142
|
+
## Note: the todos container has selection, highlight and filtering
|
|
127
143
|
|
|
128
|
-
|
|
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
|
-
|
|
146
|
+
## Fact: callbacks are installed with `setCallbacks`
|
|
136
147
|
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
@data isEnabled: boolean = false;
|
|
155
|
-
@data filter: FilterT = () => [];
|
|
154
|
+
## Fact: the callback function can access the callbacks object through `this`.
|
|
156
155
|
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
constructor() {
|
|
197
|
-
registerFacets(this);
|
|
198
|
-
this._setCallbacks();
|
|
199
|
-
}
|
|
191
|
+
// file: SelectionCbs.ts
|
|
192
|
+
import { stub } from 'aspiration';
|
|
200
193
|
|
|
201
|
-
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
202
|
+
## Fact: the `@host` decorator is used to enable callbacks in an operation.
|
|
236
203
|
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
## Fact: facets have a static `className()` function
|
|
272
217
|
|
|
273
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
and
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
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 {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
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 {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
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';
|
package/internal/logging.d.ts
CHANGED
|
@@ -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
|
|
5
|
+
export declare function defaultGetCtrState(ctr: any): any;
|
|
6
|
+
export declare function getCtrState(ctr: any): any;
|
package/internal/logging.js
CHANGED
|
@@ -9,13 +9,13 @@ var __assign = (this && this.__assign) || function () {
|
|
|
9
9
|
};
|
|
10
10
|
return __assign.apply(this, arguments);
|
|
11
11
|
};
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { options } from
|
|
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] +
|
|
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) +
|
|
37
|
+
var label = facetLogName(facet) + '.' + operationName;
|
|
41
38
|
if (start) {
|
|
42
39
|
console.group(label);
|
|
43
|
-
console.log(
|
|
44
|
-
console.log(
|
|
40
|
+
console.log('%c args: ', 'color: gray', args);
|
|
41
|
+
console.log('%c state', 'color: gray', getState());
|
|
45
42
|
}
|
|
46
43
|
else {
|
|
47
|
-
console.log(
|
|
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
|
|
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
|
+
}
|
package/internal/symbols.js
CHANGED
package/internal/utils.d.ts
CHANGED
|
@@ -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,
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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
|
}
|
package/lib/Connector.js
ADDED
|
@@ -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
package/lib/data.js
CHANGED
package/lib/facet.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { ClassT
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
38
|
-
import {
|
|
39
|
-
import { getFacetClassAdmin } from
|
|
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 ===
|
|
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,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
|
+
}
|