skandha 0.9.0 → 0.10.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 +165 -164
- package/index.d.ts +1 -2
- package/index.js +1 -2
- package/internal/utils.js +5 -9
- package/lib/facet.js +1 -1
- package/lib/patch.d.ts +3 -1
- package/lib/patch.js +13 -0
- package/lib/registerCtr.d.ts +1 -1
- package/lib/registerCtr.js +6 -6
- package/package.json +1 -1
- package/lib/Connector.d.ts +0 -5
- package/lib/Connector.js +0 -98
package/README.md
CHANGED
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Rationale
|
|
4
4
|
|
|
5
|
-
The goal of SkandhaJS is to provide data containers that have reusable behaviors, such as
|
|
6
|
-
selection, highlighting, filtering, drag-and-drop, etc. The only requirement for using these containers is
|
|
7
|
-
that every item in the container has a unique id. SkandhaJS allows you to combine behaviors and make them interact properly. For example, when selecting an item you may want the item also to
|
|
8
|
-
be highlighted. As we will see, these interactions are achieved by implementing callback functions.
|
|
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.
|
|
9
6
|
|
|
10
7
|
## Links
|
|
11
8
|
|
|
@@ -15,94 +12,105 @@ be highlighted. As we will see, these interactions are achieved by implementing
|
|
|
15
12
|
- The [aspiration](http://github.com/mnieber/aspiration) library handles aspect oriented programming (AOP)
|
|
16
13
|
- The [example code](http://github.com/mnieber/skandha-sample-app) shown in this README.
|
|
17
14
|
|
|
18
|
-
##
|
|
15
|
+
## Glossary (conceptual)
|
|
16
|
+
|
|
17
|
+
- callback function: a function that is called by an operation to help implement that operation
|
|
18
|
+
- callbacks object: a collection of callback functions that is used by an operation.
|
|
19
|
+
- container: a class that contains multiple facets. Typically, these facets are made to work together using callback functions.
|
|
20
|
+
- facet: a class that implements a reusable behaviour, such as selection.
|
|
21
|
+
- operation: a member function of a facet that changes the data of that facet.
|
|
22
|
+
|
|
23
|
+
## Glossary (technical)
|
|
24
|
+
|
|
25
|
+
- aspiration: a library that implements a form of Aspect Oriented Programming using callback functions.
|
|
26
|
+
- mapDataToProps: a helper function that maps data from one object to another. It's also used to map data between facets.
|
|
27
|
+
- registerCtr: the function that tells SkandhaJS which containers and facets exist.
|
|
28
|
+
- setCallbackMap: the function that installs the callbacks objects that are used by a facet.
|
|
29
|
+
|
|
30
|
+
## Example: a simplified Selection facet
|
|
31
|
+
|
|
32
|
+
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.
|
|
19
33
|
|
|
20
34
|
```
|
|
21
35
|
// file: Selection.ts
|
|
22
36
|
import { input, output, operation } from 'skandha';
|
|
23
|
-
import { host, stub } from 'aspiration';
|
|
24
37
|
|
|
25
38
|
export type SelectionParamsT = { itemId?: string; isShift?: boolean; isCtrl?: boolean; };
|
|
26
39
|
|
|
27
40
|
export class Selection<ValueT = any> {
|
|
28
41
|
static className = () => 'Selection';
|
|
29
42
|
|
|
30
|
-
@input selectableIds: Array<string> =
|
|
43
|
+
@input selectableIds: Array<string> = [];
|
|
31
44
|
@output ids: Array<string> = [];
|
|
32
45
|
@output anchorId?: string;
|
|
33
|
-
@output items: Array<ValueT> =
|
|
46
|
+
@output items: Array<ValueT> = [];
|
|
34
47
|
|
|
35
|
-
@operation
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return (cbs: Selection_selectItem) => {
|
|
39
|
-
cbs.select();
|
|
40
|
-
};
|
|
48
|
+
@operation selectItem(selectionParams: SelectionParamsT) {
|
|
49
|
+
// This function calls a callback function to do the actual work.
|
|
50
|
+
// Details about how this works are omitted for now.
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
```
|
|
44
54
|
|
|
45
|
-
## Fact:
|
|
55
|
+
## Fact: facets have @input, @data and @output members
|
|
46
56
|
|
|
47
|
-
In
|
|
57
|
+
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()`. Fields that are decorated with `@data` are used both as output and input.
|
|
48
58
|
|
|
49
|
-
## Fact:
|
|
59
|
+
## Fact: operations are decorated with `@operation`.
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
The `Selection` facet has a `selectItem()` operation that allows you to select from the selectable ids. It stores its results in the `ids`, `anchorId` and `items` fields.
|
|
62
|
+
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.
|
|
52
63
|
|
|
53
|
-
## Fact:
|
|
64
|
+
## Fact: operations are logged
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
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 (such as filtering, or highlight). The log message contains all @input, @data and @output fields of all facets in the container.
|
|
56
67
|
|
|
57
|
-
##
|
|
68
|
+
## Example: a container with Selection, Highlight and Filtering
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
export class Selection_selectItem extends Cbs {
|
|
62
|
-
selectionParams: SelectionParamsT = stub();
|
|
63
|
-
select() {}
|
|
64
|
-
}
|
|
65
|
-
```
|
|
70
|
+
Now that we have some idea of what a facet is, we will discuss containers.
|
|
71
|
+
In this example we will take a look at a todos container. It uses the `Selection` facet that we saw earlier (and some other facets that we haven't defined yet).
|
|
66
72
|
|
|
67
73
|
```
|
|
68
74
|
// file: createContainer.ts
|
|
69
|
-
import {
|
|
75
|
+
import { Selection_selectItem } from 'SelectionCbs';
|
|
70
76
|
|
|
71
77
|
const createContainer() {
|
|
72
|
-
const
|
|
73
|
-
selection: new Selection()
|
|
78
|
+
const todosCtr = {
|
|
79
|
+
selection: new Selection(),
|
|
80
|
+
highlight: new Highlight(),
|
|
81
|
+
filtering: new Filtering()
|
|
74
82
|
};
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
setCallbackMap(todosCtr.selection, {
|
|
77
85
|
selectItem: {
|
|
78
86
|
select(this: Selection_selectItem): {
|
|
79
|
-
handleSelectItem(
|
|
87
|
+
handleSelectItem(todosCtr.selection, this.selectionParams);
|
|
88
|
+
highlightFollowsSelection(todosCtr, this.selectionParams);
|
|
80
89
|
},
|
|
81
90
|
}
|
|
82
91
|
});
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
setCallbackMap(todosCtr.filtering, {
|
|
94
|
+
apply: {
|
|
95
|
+
exit() {
|
|
96
|
+
highlightIsCorrectedOnFilterChange(todosCtr);
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
registerCtr({ ctr: todosCtr, options: { name: 'Todos'} });
|
|
102
|
+
return todosCtr;
|
|
87
103
|
}
|
|
88
104
|
```
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
## Fact: the registerCtr function registers the container with SkandhaJS
|
|
99
|
-
|
|
100
|
-
The purpose of calling registerCtr on the container is twofold:
|
|
101
|
-
|
|
102
|
-
- it makes it possible to look up a facet by its name or class, e.g. `getf(ctr, Selection)` returns `ctr.selection`.
|
|
103
|
-
- it tells SkandhaJS what facets should be included in log reports. When the client calls any facet function that is decorated with @operation then SkandhaJS will log the entire container before and after the operation was executed. Logging can be enabled using the `setOptions` function.
|
|
104
|
-
|
|
105
|
-
## Snippet: the handleSelectItem functon
|
|
106
|
+
```
|
|
107
|
+
// file: highlightFollowsSelection.ts
|
|
108
|
+
const highlightFollowsSelection = (ctr: any, selectionParams: SelectionParamsT) => {
|
|
109
|
+
if (!selectionParams.isShift && !selectionParams.isCtrl) {
|
|
110
|
+
ctr.highlight.highlightItem(selectionParams.itemId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
106
114
|
|
|
107
115
|
```
|
|
108
116
|
// file: handleSelectItem.ts
|
|
@@ -133,163 +141,156 @@ export function handleSelectItem(
|
|
|
133
141
|
}
|
|
134
142
|
```
|
|
135
143
|
|
|
136
|
-
##
|
|
144
|
+
## Note: the todos container has selection, highlight and filtering
|
|
137
145
|
|
|
138
|
-
|
|
146
|
+
In `createContainer.ts` we see how `createContainer()` creates a `todosCtr` 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. In this case, we only install the `select()` callback function.
|
|
139
147
|
|
|
140
|
-
##
|
|
148
|
+
## Fact: callbacks are installed with `setCallbackMap()`
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
// file: Highlight.ts
|
|
144
|
-
export class Highlight_highlightItem extends Cbs {
|
|
145
|
-
id: string = stub();
|
|
146
|
-
scrollItemIntoView() {}
|
|
147
|
-
}
|
|
150
|
+
We call `setCallbackMap()` to install a callbacks object for each operation of the `Selection` facet. 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 `setCallbackMap()` to be as short and readable as possible, so that it's easy to understand what happens in each operation of each facet.
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
@data id?: string;
|
|
151
|
-
@data item?: ValueT;
|
|
152
|
+
## Fact: callbacks can take care of side effects
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
return (cbs: Highlight_highlightItem) => {
|
|
155
|
-
this.id = id;
|
|
156
|
-
if (cbs.scrollItemIntoView) {
|
|
157
|
-
cbs.scrollItemIntoView();
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
```
|
|
154
|
+
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.
|
|
163
155
|
|
|
164
|
-
## Fact: the
|
|
156
|
+
## Fact: the callback function can access the callbacks object through `this`.
|
|
165
157
|
|
|
166
|
-
|
|
158
|
+
When the clients calls `todosCtr.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`.
|
|
167
159
|
|
|
168
|
-
##
|
|
160
|
+
## Fact: the `exit()` callback function is called at the end of an operation
|
|
169
161
|
|
|
170
|
-
|
|
171
|
-
// file: createContainer.ts
|
|
172
|
-
const createContainer() {
|
|
173
|
-
const ctr = {
|
|
174
|
-
selection: new Selection(),
|
|
175
|
-
highlight: new Highlight()
|
|
176
|
-
};
|
|
162
|
+
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.
|
|
177
163
|
|
|
178
|
-
|
|
179
|
-
selectItem: {
|
|
180
|
-
select(this: Selection_selectItem): {
|
|
181
|
-
handleSelectItem(ctr.selection, this.selectionParams);
|
|
164
|
+
## Fact: the `registerCtr()` allows SkandhaJS to introspect the container
|
|
182
165
|
|
|
183
|
-
|
|
184
|
-
ctr.highlight.highlightItem(this.selectionParams.itemId);
|
|
185
|
-
}
|
|
166
|
+
At the end of `createContainer.ts` we call `registerCtr()` so that SkandhaJS can 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(todosCtr, Selection)` returns `todosCtr.selection`. Second, it informs SkandhaJS how to log all facets of the container
|
|
186
167
|
|
|
187
|
-
|
|
188
|
-
// highlightFollowsSelection(ctr, this.selectionParams);
|
|
189
|
-
},
|
|
190
|
-
}
|
|
191
|
-
});
|
|
168
|
+
## Example: a complete Selection facet
|
|
192
169
|
|
|
193
|
-
|
|
194
|
-
```
|
|
170
|
+
It's time now to return to the Selection facet and show how callbacks really work.
|
|
195
171
|
|
|
196
|
-
|
|
172
|
+
```
|
|
173
|
+
// file: Selection.ts
|
|
174
|
+
import { input, output, operation } from 'skandha';
|
|
175
|
+
import { host, stub } from 'aspiration';
|
|
176
|
+
import { Selection_selectItem, SelectionParamsT } from 'SelectionCbs';
|
|
197
177
|
|
|
198
|
-
|
|
178
|
+
export class Selection<ValueT = any> {
|
|
179
|
+
static className = () => 'Selection';
|
|
199
180
|
|
|
200
|
-
|
|
181
|
+
@input selectableIds: Array<string> = stub();
|
|
182
|
+
@output ids: Array<string> = [];
|
|
183
|
+
@output anchorId?: string;
|
|
184
|
+
@output items: Array<ValueT> = stub();
|
|
201
185
|
|
|
186
|
+
@operation @host(['selectionParams']) selectItem(
|
|
187
|
+
selectionParams: SelectionParamsT
|
|
188
|
+
) {
|
|
189
|
+
return (cbs: Selection_selectItem) => {
|
|
190
|
+
cbs.select();
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
202
194
|
```
|
|
203
|
-
// file: createContainer.ts
|
|
204
|
-
const createContainer() {
|
|
205
|
-
const ctr = {
|
|
206
|
-
selection: new Selection(),
|
|
207
|
-
highlight: new Highlight(),
|
|
208
|
-
filtering: new Filtering()
|
|
209
|
-
};
|
|
210
195
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
handleSelectItem(ctr.selection, this.selectionParams);
|
|
215
|
-
highlightFollowsSelection(ctr, this.selectionParams);
|
|
216
|
-
},
|
|
217
|
-
}
|
|
218
|
-
});
|
|
196
|
+
```
|
|
197
|
+
// file: SelectionCbs.ts
|
|
198
|
+
import { stub } from 'aspiration';
|
|
219
199
|
|
|
220
|
-
|
|
221
|
-
apply: {
|
|
222
|
-
exit() {
|
|
223
|
-
highlightIsCorrectedOnFilterChange(ctr);
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
});
|
|
200
|
+
export type SelectionParamsT = { itemId?: string; isShift?: boolean; isCtrl?: boolean; };
|
|
227
201
|
|
|
228
|
-
|
|
202
|
+
export class Selection_selectItem extends Cbs {
|
|
203
|
+
selectionParams: SelectionParamsT = stub();
|
|
204
|
+
select() {}
|
|
229
205
|
}
|
|
230
206
|
```
|
|
231
207
|
|
|
232
|
-
## Fact: the `
|
|
208
|
+
## Fact: the `@host` decorator is used to enable callbacks in an operation.
|
|
209
|
+
|
|
210
|
+
The `@host` decorator comes from the `Aspiration` library. When you call a function `f` that is decorated with `@host` then the following happens:
|
|
211
|
+
|
|
212
|
+
- Aspiration looks up the callbacks object for the operation (that was installed with `setCallbackMap()`)
|
|
213
|
+
- it copies the operation arguments to the callbacks object (so that callback functions can access these arguments)
|
|
214
|
+
- if calls `f(arguments)(callbacksObject)`.
|
|
215
|
+
|
|
216
|
+
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`.
|
|
217
|
+
|
|
218
|
+
## Fact: callback functions have access to all operation arguments
|
|
233
219
|
|
|
234
|
-
|
|
220
|
+
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`).
|
|
235
221
|
|
|
236
|
-
##
|
|
222
|
+
## Fact: facets have a static `className()` function
|
|
223
|
+
|
|
224
|
+
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.
|
|
225
|
+
|
|
226
|
+
## Example: `mapDataToProps()` maps data onto each facet
|
|
227
|
+
|
|
228
|
+
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. In this example, we will see how we can create such a mapping from the filtering facet to the selection facet. Moreover, we will add code to load the todos and set them as the inputs of the filtering facet.
|
|
237
229
|
|
|
238
230
|
```
|
|
239
231
|
// file: createContainer.ts
|
|
240
232
|
import * from 'ramda' as R;
|
|
233
|
+
import { mapDataToProps, pmap } from 'skandha';
|
|
241
234
|
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
235
|
+
const createContainer = () => {
|
|
236
|
+
const todosCtr = {
|
|
237
|
+
data: {
|
|
238
|
+
todos: loadTodos(),
|
|
239
|
+
filteredTodoById: {},
|
|
240
|
+
},
|
|
241
|
+
selection: new Selection(),
|
|
242
|
+
highlight: new Highlight(),
|
|
243
|
+
filtering: new Filtering(),
|
|
244
|
+
}
|
|
245
245
|
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
const lookUpTodo = (id?: string) => id ? todosCtr.data.filteredTodoById[id] : undefined;
|
|
247
|
+
|
|
248
|
+
mapDataToProps(
|
|
249
|
+
pmap(
|
|
250
|
+
[todosCtr.filtering, 'inputItems'],
|
|
251
|
+
() => todosCtr.data.todos
|
|
252
|
+
),
|
|
253
|
+
pmap(
|
|
254
|
+
[todosCtr.data, 'filteredTodoById'],
|
|
255
|
+
() => R.indexBy('id')(todosCtr.filtering.filteredItems)
|
|
256
|
+
),
|
|
257
|
+
pmap(
|
|
258
|
+
[todosCtr.selection, 'selectableIds'],
|
|
259
|
+
() => R.keys(todosCtr.data.filteredTodoById)
|
|
260
|
+
),
|
|
261
|
+
pmap(
|
|
262
|
+
[todosCtr.selection, 'item'],
|
|
263
|
+
() => R.map(lookUpTodo, todosCtr.selection.ids)
|
|
264
|
+
),
|
|
265
|
+
pmap(
|
|
266
|
+
[todosCtr.highlight, 'item'],
|
|
267
|
+
() => lookUpTodo(todosCtr.highlight.id)
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
}
|
|
253
271
|
|
|
254
|
-
|
|
272
|
+
setOptions({logging: true});
|
|
273
|
+
```
|
|
255
274
|
|
|
256
|
-
|
|
257
|
-
con.outputs.filteredTodoById = con.filtering.filteredItems.tf(R.indexBy('id));
|
|
258
|
-
con.selection.selectableIds = con.outputs.filteredTodoById.tf(R.keys);
|
|
259
|
-
con.selection.item = con.selection.ids.tf(lookUpTodos);
|
|
260
|
-
con.highlight.item = con.highlight.id.tf(lookUpTodo);
|
|
275
|
+
## Fact: the `mapDataToProps()` function maps data between objects
|
|
261
276
|
|
|
262
|
-
|
|
263
|
-
```
|
|
277
|
+
The `mapDataToProp()` function takes a container, field name and function then it turns that field into a `get` property that executes the given function. The `mapDataToProps()` is a small convenience function that calls `mapDataToProp()` on each of its arguments. In our example, we see that the `todosCtr.data.todos` field is mapped onto the `todosCtr.filtering.inputItems` field.
|
|
264
278
|
|
|
265
|
-
## Fact: the
|
|
279
|
+
## Fact: the `pmap()` function adds type-checking
|
|
266
280
|
|
|
267
|
-
The
|
|
268
|
-
The snippet contains three steps. In the first step we declare the objects, such as `selection`, `filtering`, etc. For each object, we specify the getter function that returns the object. In the second step, we declare relations between the different fields of these objects. In this step, we can also declare one or more transformation functions (such as `lookUpTodo`) that should be used in the mapping. In the final step, we call `con.connect` to create the `get` properties that perform the declared mappings.
|
|
281
|
+
The `pmap()` function takes two arguments. The first argument is an array that has a container and a container-member. The second argument is a function. The `pmap` function checks that the output of this function can be stored in the container-member (if not, then a compile-time error results). It returns an array that contains its two arguments (because that is the format that `mapDataToProps` expects). As you can see, `pmap` does not do any useful work at run-time, but it adds type-checking at compile-time.
|
|
269
282
|
|
|
270
|
-
|
|
283
|
+
## Fact: logging can be enabled using `setOptions()`
|
|
271
284
|
|
|
272
|
-
By calling `setOptions({logging: true})`, the SkandhaJS library allows you to inspect each facet before and
|
|
273
|
-
after calling an operation. An operation is any facet member function decorated with @operation.
|
|
274
|
-
This will log Facet members that are decorated with `@data` (or `@input`, or `@output`) in a way that looks similar to what you are used to from Redux:
|
|
285
|
+
By calling `setOptions({logging: true})`, the SkandhaJS library allows you to inspect each facet before and after calling an operation. 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:
|
|
275
286
|
|
|
276
287
|
- before and after each operation, the entire container that holds the facet is logged
|
|
277
288
|
- all data members of each facet in the container are included in the log
|
|
278
289
|
- log entries are nested so that you can see how operation calls are nested
|
|
279
290
|
|
|
280
|
-
|
|
291
|
+
## Fact: the `registerCtr()` function from facets-mobx makes facets observable
|
|
281
292
|
|
|
282
|
-
If you want to render the facets with React then we need to let React know when data changes.
|
|
283
|
-
The [skandha-mobx](http://github.com/mnieber/skandha-mobx) library was created for this purpose.
|
|
284
|
-
It has a `registerCtr` function that turns each `@data` member into a Mobx property that is
|
|
285
|
-
either observable or computed:
|
|
286
|
-
|
|
287
|
-
```
|
|
288
|
-
registerCtr({
|
|
289
|
-
ctr: ctr,
|
|
290
|
-
options: { name: 'TodosCtr'},
|
|
291
|
-
});
|
|
292
|
-
```
|
|
293
|
+
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 can be used instead of the default one from `skandha`. This replacement version of `registerCtr()` function turns each `@data` member into a Mobx property that is either observable or computed.
|
|
293
294
|
|
|
294
295
|
## Conclusion
|
|
295
296
|
|
package/index.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
export { getCtrState } from './internal/logging';
|
|
2
2
|
export { getOptions, setOptions } from './internal/options';
|
|
3
|
-
export { createConnector } from './lib/Connector';
|
|
4
3
|
export { getc, getFacetMemberNames } from './lib/ctr';
|
|
5
4
|
export { data, getDataMemberNames, getLoggedMemberNames, input, log, output, } from './lib/data';
|
|
6
5
|
export { getf } from './lib/facet';
|
|
7
6
|
export { installPolicies } from './lib/install';
|
|
8
7
|
export { getOperationMemberNames, operation } from './lib/operation';
|
|
9
|
-
export { mapDataToProp } from './lib/patch';
|
|
8
|
+
export { mapDataToProp, mapDataToProps, pmap } from './lib/patch';
|
|
10
9
|
export { registerCtr } from './lib/registerCtr';
|
|
11
10
|
export type { OptionsT as RegisterCtrOptionsT } from './lib/registerCtr';
|
|
12
11
|
export declare type ClassT = any;
|
package/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
export { getCtrState } from './internal/logging';
|
|
2
2
|
export { getOptions, setOptions } from './internal/options';
|
|
3
|
-
export { createConnector } from './lib/Connector';
|
|
4
3
|
export { getc, getFacetMemberNames } from './lib/ctr';
|
|
5
4
|
export { data, getDataMemberNames, getLoggedMemberNames, input, log, output, } from './lib/data';
|
|
6
5
|
export { getf } from './lib/facet';
|
|
7
6
|
export { installPolicies } from './lib/install';
|
|
8
7
|
export { getOperationMemberNames, operation } from './lib/operation';
|
|
9
|
-
export { mapDataToProp } from './lib/patch';
|
|
8
|
+
export { mapDataToProp, mapDataToProps, pmap } from './lib/patch';
|
|
10
9
|
export { registerCtr } from './lib/registerCtr';
|
package/internal/utils.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
var __spreadArray = (this && this.__spreadArray) || function (to, from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
ar[i] = from[i];
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
return to.concat(ar || Array.prototype.slice.call(from));
|
|
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;
|
|
9
5
|
};
|
|
10
6
|
import { symbols } from './symbols';
|
|
11
7
|
export function getOrCreate(obj, key, fn) {
|
|
@@ -20,7 +16,7 @@ export var zip = function (arr) {
|
|
|
20
16
|
arrs[_i - 1] = arguments[_i];
|
|
21
17
|
}
|
|
22
18
|
return arr.map(function (val, i) {
|
|
23
|
-
return arrs.reduce(function (a, arr) { return __spreadArray(__spreadArray([], a
|
|
19
|
+
return arrs.reduce(function (a, arr) { return __spreadArray(__spreadArray([], a), [arr[i]]); }, [val]);
|
|
24
20
|
});
|
|
25
21
|
};
|
|
26
22
|
export function getCtrAdmin(ctr) {
|
package/lib/facet.js
CHANGED
|
@@ -11,7 +11,7 @@ export function getf(facetClass, ctr) {
|
|
|
11
11
|
: (_a = getCtrAdmin(ctr).facetByFacetClassName) === null || _a === void 0 ? void 0 : _a[facetClassName(facetClass)];
|
|
12
12
|
if (!facet) {
|
|
13
13
|
var facetName = facetClass === 'string' ? facetClass : facetClassName(facetClass);
|
|
14
|
-
console.error("No facet "
|
|
14
|
+
console.error("No facet " + facetName + " in container " + getCtrAdmin(ctr).logName);
|
|
15
15
|
}
|
|
16
16
|
return facet;
|
|
17
17
|
}
|
package/lib/patch.d.ts
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
export declare function mapDataToProp(facet:
|
|
1
|
+
export declare function mapDataToProp<F, P extends keyof F>(facet: F, prop: P, getter: () => F[P]): void;
|
|
2
|
+
export declare function mapDataToProps(...mappings: any[]): void;
|
|
3
|
+
export declare function pmap<F, P extends keyof F>(member: [F, P], getter: () => F[P]): ([F, P] | (() => F[P]))[];
|
package/lib/patch.js
CHANGED
|
@@ -6,3 +6,16 @@ export function mapDataToProp(facet, prop, getter) {
|
|
|
6
6
|
configurable: true,
|
|
7
7
|
});
|
|
8
8
|
}
|
|
9
|
+
export function mapDataToProps() {
|
|
10
|
+
var mappings = [];
|
|
11
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
12
|
+
mappings[_i] = arguments[_i];
|
|
13
|
+
}
|
|
14
|
+
for (var _a = 0, mappings_1 = mappings; _a < mappings_1.length; _a++) {
|
|
15
|
+
var mapping = mappings_1[_a];
|
|
16
|
+
mapDataToProp(mapping[0][0], mapping[0][1], mapping[1]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function pmap(member, getter) {
|
|
20
|
+
return [member, getter];
|
|
21
|
+
}
|
package/lib/registerCtr.d.ts
CHANGED
package/lib/registerCtr.js
CHANGED
|
@@ -10,18 +10,18 @@ export function registerCtr(ctr, options) {
|
|
|
10
10
|
ctrAdmin.facetMembers = (_a = ctrAdmin.facetMembers) !== null && _a !== void 0 ? _a : [];
|
|
11
11
|
ctrAdmin.facetByFacetClassName = (_b = ctrAdmin.facetByFacetClassName) !== null && _b !== void 0 ? _b : {};
|
|
12
12
|
ctrAdmin.ctrStateOverride = options.ctrState;
|
|
13
|
-
((_c = options.
|
|
14
|
-
var facet = ctr[
|
|
13
|
+
((_c = options.facets) !== null && _c !== void 0 ? _c : Object.getOwnPropertyNames(ctr)).forEach(function (facetName) {
|
|
14
|
+
var facet = ctr[facetName];
|
|
15
15
|
setCtr(facet, ctr);
|
|
16
|
-
ctrAdmin.facetMembers.push(
|
|
16
|
+
ctrAdmin.facetMembers.push(facetName);
|
|
17
17
|
if (!facet.constructor.className) {
|
|
18
|
-
console.error("Facet "
|
|
18
|
+
console.error("Facet " + facetName + " of container " + ctrAdmin.logName + " " +
|
|
19
19
|
"does not have a static className function");
|
|
20
20
|
}
|
|
21
21
|
var className = facetClassName(facet.constructor);
|
|
22
|
-
getFacetAdmin(facet).logName =
|
|
22
|
+
getFacetAdmin(facet).logName = ctrAdmin.logName + "/" + className;
|
|
23
23
|
if (ctrAdmin.facetByFacetClassName[className] !== undefined) {
|
|
24
|
-
console.error("Two facets of same type "
|
|
24
|
+
console.error("Two facets of same type " + className + " in container " + ctrAdmin.logName);
|
|
25
25
|
}
|
|
26
26
|
ctrAdmin.facetByFacetClassName[className] = facet;
|
|
27
27
|
});
|
package/package.json
CHANGED
package/lib/Connector.d.ts
DELETED
package/lib/Connector.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { mapDataToProp } from './patch';
|
|
2
|
-
var Connector = /** @class */ (function () {
|
|
3
|
-
function Connector(entities, options) {
|
|
4
|
-
var _this = this;
|
|
5
|
-
var _a;
|
|
6
|
-
this.entityByName = {};
|
|
7
|
-
this.connect = function () {
|
|
8
|
-
for (var _i = 0, _a = Object.values(_this.entityByName); _i < _a.length; _i++) {
|
|
9
|
-
var entity = _a[_i];
|
|
10
|
-
_this._connectEntity(entity);
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
this._connectEntity = function (entity) {
|
|
14
|
-
var _loop_1 = function (entry) {
|
|
15
|
-
var name_1 = entry[0], field = entry[1];
|
|
16
|
-
var obj = entity.getObj();
|
|
17
|
-
if (_this.options.strict && !(name_1 in obj)) {
|
|
18
|
-
throw new Error("Cannot override the field ".concat(name_1, " in ").concat(obj, ".") +
|
|
19
|
-
'Field not found. Disable strict mode to ignore this error.');
|
|
20
|
-
}
|
|
21
|
-
mapDataToProp(obj, name_1, function () {
|
|
22
|
-
return field.getSource();
|
|
23
|
-
});
|
|
24
|
-
};
|
|
25
|
-
for (var _i = 0, _a = Object.entries(entity.fieldByName); _i < _a.length; _i++) {
|
|
26
|
-
var entry = _a[_i];
|
|
27
|
-
_loop_1(entry);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
this.options = {
|
|
31
|
-
strict: (_a = options === null || options === void 0 ? void 0 : options.strict) !== null && _a !== void 0 ? _a : true,
|
|
32
|
-
};
|
|
33
|
-
for (var _i = 0, _b = Object.entries(entities); _i < _b.length; _i++) {
|
|
34
|
-
var entry = _b[_i];
|
|
35
|
-
var name_2 = entry[0], getObj = entry[1];
|
|
36
|
-
this.entityByName[name_2] = new Entity(getObj);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return Connector;
|
|
40
|
-
}());
|
|
41
|
-
export var createConnector = function (entities) {
|
|
42
|
-
var con = new Connector(entities);
|
|
43
|
-
return new Proxy(con, {
|
|
44
|
-
get: function (target, key) {
|
|
45
|
-
// Code example:
|
|
46
|
-
//
|
|
47
|
-
// const con = createConnector({ foo: () => myFoo });
|
|
48
|
-
//
|
|
49
|
-
// When you call `con.foo` then you
|
|
50
|
-
// receive `con.entityByName.foo.fieldByName`.
|
|
51
|
-
//
|
|
52
|
-
// con.foo.bar = 5; // Equivalent to: con.entityByName.foo.fieldByName.bar = 5
|
|
53
|
-
//
|
|
54
|
-
// When you call `con.connect` then a normal lookup of the `connect`
|
|
55
|
-
// member in `con` is performed.
|
|
56
|
-
var _a, _b;
|
|
57
|
-
return (_b = (_a = target.entityByName[key]) === null || _a === void 0 ? void 0 : _a.fieldByName) !== null && _b !== void 0 ? _b : target[key];
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
var Entity = /** @class */ (function () {
|
|
62
|
-
function Entity(getObj) {
|
|
63
|
-
var _this = this;
|
|
64
|
-
this.getObj = getObj;
|
|
65
|
-
this.fieldByName = new Proxy({}, {
|
|
66
|
-
get: function (target, key) {
|
|
67
|
-
var _a;
|
|
68
|
-
return (_a = target[key]) !== null && _a !== void 0 ? _a : new Field(_this, key);
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
return Entity;
|
|
73
|
-
}());
|
|
74
|
-
var Field = /** @class */ (function () {
|
|
75
|
-
function Field(entity, name) {
|
|
76
|
-
var _this = this;
|
|
77
|
-
this.transforms = [];
|
|
78
|
-
this.getSource = function () {
|
|
79
|
-
var obj = _this.entity.getObj();
|
|
80
|
-
if (!(_this.name in obj)) {
|
|
81
|
-
throw new Error("Field ".concat(_this.name, " does not exist on object ").concat(obj));
|
|
82
|
-
}
|
|
83
|
-
var result = obj[_this.name];
|
|
84
|
-
for (var _i = 0, _a = _this.transforms; _i < _a.length; _i++) {
|
|
85
|
-
var tf = _a[_i];
|
|
86
|
-
result = tf(result);
|
|
87
|
-
}
|
|
88
|
-
return result;
|
|
89
|
-
};
|
|
90
|
-
this.entity = entity;
|
|
91
|
-
this.name = name;
|
|
92
|
-
}
|
|
93
|
-
Field.prototype.tf = function (transform) {
|
|
94
|
-
this.transforms.push(transform);
|
|
95
|
-
return this;
|
|
96
|
-
};
|
|
97
|
-
return Field;
|
|
98
|
-
}());
|