skandha 0.9.1 → 0.10.2
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 +152 -131
- package/index.d.ts +2 -3
- package/index.js +2 -3
- package/internal/options.d.ts +3 -0
- package/internal/options.js +2 -0
- package/lib/patch.d.ts +3 -1
- package/lib/patch.js +13 -0
- package/package.json +1 -1
- package/lib/Connector.d.ts +0 -5
- package/lib/Connector.js +0 -107
package/README.md
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
# SkandhaJS
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Introduction
|
|
4
4
|
|
|
5
|
-
The goal of SkandhaJS is to provide data containers that have reusable behaviors
|
|
5
|
+
The goal of SkandhaJS is to provide data containers that have reusable behaviors. These behaviours are implemented in classes called "facets". Examples of facets are selection, highlighting, filtering, drag-and-drop, etc. By connecting these facets, you can create interactions between different behaviours.
|
|
6
|
+
|
|
7
|
+
Every facet provides an interface that supports a particular behaviour. This interface is used throughout the application, and ensures that the basic mechanism (e.g. for doing selection) is the same everywhere. The details of the behaviour depend on callback functions that the programmer provides:
|
|
8
|
+
|
|
9
|
+
1. Callbacks decide what exactly happens when an item is selected, highlighted, deleted etc.
|
|
10
|
+
2. Callbacks implement interactions. For example: when selecting an item, also higlight it.
|
|
11
|
+
|
|
12
|
+
The second point is important for limiting the mixing of concerns that happens when you are combining multiple behaviours. This mixing of concerns is unavoidable, but should be limited to specific places that can be easily inspected.
|
|
13
|
+
|
|
14
|
+
As a brief illustration, consider the example of selecting a todo in a list of todos:
|
|
15
|
+
|
|
16
|
+
1. The application creates a local state to store todos. This state contains a `todoSelection` facet.
|
|
17
|
+
2. When the user clicks on a todo, the todo-list component calls `todoSelection.selectItem(todo.id, shift, ctrl)`.
|
|
18
|
+
3. The `aspiration` library intercepts the call in order to prepare a so-called `callbackMap`.
|
|
19
|
+
4. Next, the `todoSelection.selectItem` function runs. It uses the `select` function from the `callbackMap` to do its work.
|
|
20
|
+
5. The `select` callback performs the selection and a side-effect: it highlights the item as well.
|
|
21
|
+
|
|
22
|
+
## Benefits
|
|
23
|
+
|
|
24
|
+
There are three ways in which facets help you to write generic code:
|
|
25
|
+
|
|
26
|
+
1. They can be reused in any container, regardless of what data-type is stored in the container. This means that selection, filtering and drag-and-drop work the same everywhere in the application.
|
|
27
|
+
2. They allow you to capture the interaction between behaviours in reusable functions.
|
|
28
|
+
3. They allow you to handle user-interaction in UI components in a uniform way. All UI components that receive a selection facet will use the same interface to make a selection (and in theory the component can remain agnostic of the data-type of the selected items).
|
|
6
29
|
|
|
7
30
|
## Links
|
|
8
31
|
|
|
@@ -14,90 +37,127 @@ The goal of SkandhaJS is to provide data containers that have reusable behaviors
|
|
|
14
37
|
|
|
15
38
|
## Glossary (conceptual)
|
|
16
39
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
40
|
+
- callback function: a function that is called by an operation to help implement that operation
|
|
41
|
+
- callbackMap object: a collection of callback functions that is used by an operation.
|
|
42
|
+
- container: an object that contains data and facets. Typically, these facets are made to work together using callback functions.
|
|
19
43
|
- facet: a class that implements a reusable behaviour, such as selection.
|
|
20
44
|
- operation: a member function of a facet that changes the data of that facet.
|
|
21
45
|
|
|
22
46
|
## Glossary (technical)
|
|
23
47
|
|
|
24
48
|
- aspiration: a library that implements a form of Aspect Oriented Programming using callback functions.
|
|
49
|
+
- mapDataToProps: a helper function that maps data from one object to another. It's used to map data between facets.
|
|
25
50
|
- registerCtr: the function that tells SkandhaJS which containers and facets exist.
|
|
26
|
-
-
|
|
51
|
+
- setCallbackMap: the function that installs the callbackMap objects that are used by a facet.
|
|
27
52
|
|
|
28
|
-
## Example: a
|
|
53
|
+
## Example: a Selection facet
|
|
29
54
|
|
|
30
|
-
In Skandha,
|
|
55
|
+
In Skandha, a behaviour such as selection is implemented in a "facet" class. The purpose of the selection facet is to store (and manipulate) the meta-data related to selection. In this example, we will show a simplified Selection facet class.
|
|
31
56
|
|
|
32
57
|
```
|
|
33
58
|
// file: Selection.ts
|
|
34
59
|
import { input, output, operation } from 'skandha';
|
|
35
|
-
|
|
36
|
-
|
|
60
|
+
import { host, stub } from 'aspiration';
|
|
61
|
+
import { Selection_selectItem, SelectionParamsT } from 'SelectionCbs';
|
|
37
62
|
|
|
38
63
|
export class Selection<ValueT = any> {
|
|
39
64
|
static className = () => 'Selection';
|
|
40
65
|
|
|
41
|
-
@input selectableIds: Array<string> =
|
|
66
|
+
@input selectableIds: Array<string> = stub();
|
|
42
67
|
@output ids: Array<string> = [];
|
|
43
68
|
@output anchorId?: string;
|
|
44
|
-
@output items: Array<ValueT> =
|
|
69
|
+
@output items: Array<ValueT> = stub();
|
|
45
70
|
|
|
46
|
-
@operation
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
@operation @host(['selectionParams']) selectItem(
|
|
72
|
+
selectionParams: SelectionParamsT
|
|
73
|
+
) {
|
|
74
|
+
const cbs = getCallbacks<Selection_selectItem>(this);
|
|
75
|
+
cbs.select();
|
|
49
76
|
}
|
|
50
77
|
}
|
|
51
78
|
```
|
|
52
79
|
|
|
53
|
-
|
|
80
|
+
```
|
|
81
|
+
// file: SelectionCbs.ts
|
|
82
|
+
import { stub } from 'aspiration';
|
|
54
83
|
|
|
55
|
-
|
|
84
|
+
export type SelectionParamsT = { itemId?: string; isShift?: boolean; isCtrl?: boolean; };
|
|
56
85
|
|
|
57
|
-
|
|
86
|
+
export class Selection_selectItem extends Cbs {
|
|
87
|
+
selectionParams: SelectionParamsT = stub();
|
|
88
|
+
select() {}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Facets have @input, @data and @output members
|
|
93
|
+
|
|
94
|
+
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. The `@input` decorator marks this field as 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. These decorators help with logging, and they also make it possible to bind the facets to state-management solutions such as MobX.
|
|
95
|
+
|
|
96
|
+
## Operations are decorated with `@operation`.
|
|
97
|
+
|
|
98
|
+
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. As the comment mentions, `selectItem()` calls a callback function to do the actual work. This design leaves the selection method (e.g. what should happen if shift is pressed?) up to the client, which makes it possible to support different use-cases flexibly.
|
|
99
|
+
|
|
100
|
+
## Operations are logged
|
|
101
|
+
|
|
102
|
+
As a side effect of calling an operation, the entire container is logged to the console (if logging is enabled); 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.
|
|
103
|
+
|
|
104
|
+
## The `@host` decorator is used to enable callbacks in an operation.
|
|
105
|
+
|
|
106
|
+
The `@host` decorator comes from the `Aspiration` library. When you call a function `f` that is decorated with `@host` then the following happens:
|
|
107
|
+
|
|
108
|
+
- Aspiration looks up the callbackMap object (that was installed with `setCallbackMap()`) for the given operation;
|
|
109
|
+
- it copies the operation arguments to the callbackMap object, so that callback functions can access these arguments;
|
|
110
|
+
- if calls `f(arguments)`;
|
|
111
|
+
- it restores the callbackMap object to its previous state. This ensures that nested calls work correctly.
|
|
112
|
+
|
|
113
|
+
If the original function `f` wants to access the callbackMap object then it uses the `getCallbacks` helper function. Note that the client code calls `f` in the usual way; it remains agnostic of the fact that callbacks are used to help implement `f`.
|
|
58
114
|
|
|
59
|
-
|
|
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.
|
|
115
|
+
## Callback functions have access to all operation arguments
|
|
61
116
|
|
|
62
|
-
|
|
117
|
+
Each callback function of the callbackMap object has access to all arguments of the operation. In our example, the argument of the `selectItem` operation is named `selectionParams`. Therefore, a `selectionParams` field also exists in the `Selection_selectItem` class that stores the callbackMap. We must manually pass the list of argument names to the `@host` decorator, so that SkandhaJS can copy these values (unfortunately, introspection in typescript is not good enough to automate this).
|
|
63
118
|
|
|
64
|
-
|
|
119
|
+
## Facets have a static `className()` function
|
|
120
|
+
|
|
121
|
+
To allow SkandhaJS to log the container, you need to add the `className()` function to each facet class, so that Skandha knows the name of the facet.
|
|
65
122
|
|
|
66
123
|
## Example: a container with Selection, Highlight and Filtering
|
|
67
124
|
|
|
68
125
|
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.
|
|
126
|
+
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). Note that in this example, we're not yet storing any data in the container (this will come later).
|
|
70
127
|
|
|
71
128
|
```
|
|
72
129
|
// file: createContainer.ts
|
|
73
130
|
import { Selection_selectItem } from 'SelectionCbs';
|
|
131
|
+
import * as R from 'ramda';
|
|
74
132
|
|
|
75
133
|
const createContainer() {
|
|
76
|
-
const
|
|
134
|
+
const todosCtr = {
|
|
77
135
|
selection: new Selection(),
|
|
78
136
|
highlight: new Highlight(),
|
|
79
137
|
filtering: new Filtering()
|
|
80
138
|
};
|
|
81
139
|
|
|
82
|
-
|
|
140
|
+
setCallbackMap(todosCtr.selection, {
|
|
83
141
|
selectItem: {
|
|
84
142
|
select(this: Selection_selectItem): {
|
|
85
|
-
handleSelectItem(
|
|
86
|
-
highlightFollowsSelection(
|
|
143
|
+
handleSelectItem(todosCtr.selection, this.selectionParams);
|
|
144
|
+
highlightFollowsSelection(todosCtr, this.selectionParams);
|
|
87
145
|
},
|
|
88
146
|
}
|
|
89
147
|
});
|
|
90
148
|
|
|
91
|
-
|
|
149
|
+
setCallbackMap(todosCtr.highlight, {});
|
|
150
|
+
|
|
151
|
+
setCallbackMap(todosCtr.filtering, {
|
|
92
152
|
apply: {
|
|
93
153
|
exit() {
|
|
94
|
-
highlightIsCorrectedOnFilterChange(
|
|
154
|
+
highlightIsCorrectedOnFilterChange(todosCtr);
|
|
95
155
|
},
|
|
96
156
|
},
|
|
97
157
|
});
|
|
98
158
|
|
|
99
|
-
registerCtr({ ctr:
|
|
100
|
-
return
|
|
159
|
+
registerCtr({ ctr: todosCtr, options: { name: 'Todos'} });
|
|
160
|
+
return todosCtr;
|
|
101
161
|
}
|
|
102
162
|
```
|
|
103
163
|
|
|
@@ -139,141 +199,103 @@ export function handleSelectItem(
|
|
|
139
199
|
}
|
|
140
200
|
```
|
|
141
201
|
|
|
142
|
-
##
|
|
202
|
+
## Callbacks are installed with `setCallbackMap()`
|
|
143
203
|
|
|
144
|
-
|
|
204
|
+
We call `setCallbackMap()` to install a callbackMap 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.
|
|
145
205
|
|
|
146
|
-
##
|
|
206
|
+
## Callbacks can take care of side effects
|
|
147
207
|
|
|
148
|
-
|
|
208
|
+
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. Keeping the code of `setCallbackMap()` short also helps the reader to understand which interactions take place. This is vital, because we are purposely mixing concerns in this part of the code, which means that there is a higher risk of confusing the reader.
|
|
149
209
|
|
|
150
|
-
##
|
|
210
|
+
## The callback function can access the callbackMap object through `this`.
|
|
151
211
|
|
|
152
|
-
|
|
212
|
+
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`.
|
|
153
213
|
|
|
154
|
-
##
|
|
214
|
+
## The `exit()` callback function is called at the end of an operation
|
|
155
215
|
|
|
156
|
-
|
|
216
|
+
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 automatically at the beginning.
|
|
157
217
|
|
|
158
|
-
##
|
|
218
|
+
## The `registerCtr()` allows SkandhaJS to introspect the container
|
|
159
219
|
|
|
160
|
-
|
|
220
|
+
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.
|
|
161
221
|
|
|
162
|
-
## Example:
|
|
222
|
+
## Example: `mapDataToProps()` maps data onto each facet
|
|
163
223
|
|
|
164
|
-
|
|
224
|
+
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. The next step is to map data between facets. Such mappings are necessary when the input data for one facet is based on the output data of another facet. For example, we should only allow selection of todos that have not been filtered out. In this example, we will see how we can create such a mapping. Moreover, we will add code to load the todos and set them as the inputs of the filtering facet.
|
|
165
225
|
|
|
166
226
|
```
|
|
167
|
-
// file:
|
|
168
|
-
import
|
|
169
|
-
import {
|
|
170
|
-
import { Selection_selectItem, SelectionParamsT } from 'SelectionCbs';
|
|
227
|
+
// file: createContainer.ts
|
|
228
|
+
import * from 'ramda' as R;
|
|
229
|
+
import { mapDataToProps, pmap } from 'skandha';
|
|
171
230
|
|
|
172
|
-
|
|
173
|
-
static className = () => '
|
|
231
|
+
class TodosData {
|
|
232
|
+
static className = () => 'Inputs';
|
|
174
233
|
|
|
175
|
-
@
|
|
176
|
-
@
|
|
177
|
-
@output anchorId?: string;
|
|
178
|
-
@output items: Array<ValueT> = stub();
|
|
234
|
+
@data todos: TodoT[] = [];
|
|
235
|
+
@data filteredTodos: TodoT[] = [];
|
|
179
236
|
|
|
180
|
-
@
|
|
181
|
-
|
|
182
|
-
) {
|
|
183
|
-
return (cbs: Selection_selectItem) => {
|
|
184
|
-
cbs.select();
|
|
185
|
-
};
|
|
237
|
+
@data get todoById() {
|
|
238
|
+
return R.indexBy(R.prop('id'), todosCtr);
|
|
186
239
|
}
|
|
187
240
|
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
// file: SelectionCbs.ts
|
|
192
|
-
import { stub } from 'aspiration';
|
|
193
|
-
|
|
194
|
-
export type SelectionParamsT = { itemId?: string; isShift?: boolean; isCtrl?: boolean; };
|
|
195
|
-
|
|
196
|
-
export class Selection_selectItem extends Cbs {
|
|
197
|
-
selectionParams: SelectionParamsT = stub();
|
|
198
|
-
select() {}
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Fact: the `@host` decorator is used to enable callbacks in an operation.
|
|
203
241
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
- it copies the operation arguments to the callbacks object (so that callback functions can access these arguments)
|
|
208
|
-
- if calls `f(arguments)(callbacksObject)`.
|
|
209
|
-
|
|
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`.
|
|
211
|
-
|
|
212
|
-
## Fact: callback functions have access to all operation arguments
|
|
213
|
-
|
|
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`).
|
|
215
|
-
|
|
216
|
-
## Fact: facets have a static `className()` function
|
|
217
|
-
|
|
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.
|
|
219
|
-
|
|
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.
|
|
223
|
-
|
|
224
|
-
```
|
|
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 = {
|
|
242
|
+
const createContainer = () => {
|
|
243
|
+
const todosCtr = {
|
|
244
|
+
data: new TodosData(),
|
|
235
245
|
selection: new Selection(),
|
|
236
246
|
highlight: new Highlight(),
|
|
237
247
|
filtering: new Filtering(),
|
|
238
248
|
}
|
|
249
|
+
todosCtr.data.todos = loadTodos();
|
|
250
|
+
|
|
251
|
+
const getTodoById = (id?: string) => id ? todosCtr.data.todoById[id] : undefined;
|
|
252
|
+
|
|
253
|
+
mapDataToProps(
|
|
254
|
+
pmap(
|
|
255
|
+
[todosCtr.filtering, 'inputItems'],
|
|
256
|
+
() => todosCtr.data.todos
|
|
257
|
+
),
|
|
258
|
+
pmap(
|
|
259
|
+
[todosCtr.data, 'filteredTodos'],
|
|
260
|
+
() => todosCtr.filtering.filteredItems
|
|
261
|
+
),
|
|
262
|
+
pmap(
|
|
263
|
+
[todosCtr.selection, 'selectableIds'],
|
|
264
|
+
() => R.map(R.prop('id'), todosCtr.data.filteredTodos)
|
|
265
|
+
),
|
|
266
|
+
pmap(
|
|
267
|
+
[todosCtr.selection, 'items'],
|
|
268
|
+
() => R.map(getTodoById, todosCtr.selection.ids)
|
|
269
|
+
),
|
|
270
|
+
pmap(
|
|
271
|
+
[todosCtr.highlight, 'item'],
|
|
272
|
+
() => getTodoById(todosCtr.highlight.id)
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
}
|
|
239
276
|
|
|
240
|
-
|
|
241
|
-
|
|
277
|
+
setOptions({logging: true});
|
|
278
|
+
```
|
|
242
279
|
|
|
243
|
-
|
|
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);
|
|
280
|
+
## The `mapDataToProps()` function maps data between objects
|
|
248
281
|
|
|
249
|
-
|
|
250
|
-
```
|
|
282
|
+
The `mapDataToProp()` function takes a container, field name and function and installs a `get` property (on the given container, using the given field name) 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.
|
|
251
283
|
|
|
252
|
-
##
|
|
284
|
+
## The `pmap()` function adds type-checking
|
|
253
285
|
|
|
254
|
-
The
|
|
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`.
|
|
286
|
+
The `pmap` function does not really do anything at run-time, but it adds type-checking at compile-time. It 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).
|
|
256
287
|
|
|
257
|
-
|
|
288
|
+
## Logging can be enabled using `setOptions()`
|
|
258
289
|
|
|
259
|
-
|
|
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:
|
|
290
|
+
If you call `setOptions({logging: true})` then the SkandhaJS library logs each facet before and after each 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:
|
|
261
291
|
|
|
262
292
|
- before and after each operation, the entire container that holds the facet is logged
|
|
263
293
|
- all data members of each facet in the container are included in the log
|
|
264
294
|
- log entries are nested so that you can see how operation calls are nested
|
|
265
295
|
|
|
266
|
-
|
|
267
|
-
|
|
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:
|
|
296
|
+
## The `registerCtr()` function from facets-mobx makes facets observable
|
|
270
297
|
|
|
271
|
-
|
|
272
|
-
registerCtr({
|
|
273
|
-
ctr: ctr,
|
|
274
|
-
options: { name: 'Todos'},
|
|
275
|
-
});
|
|
276
|
-
```
|
|
298
|
+
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 drop-in replacement version of the normal `registerCtr()` function turns each `@data` member into a Mobx property that is either observable or computed.
|
|
277
299
|
|
|
278
300
|
## Conclusion
|
|
279
301
|
|
|
@@ -284,5 +306,4 @@ separating concerns in a clear and understandable way. We could add a `Editing`
|
|
|
284
306
|
a policy that says that changing the highlight disables editing. By using facets, we will not be tempted to put
|
|
285
307
|
editing-specific code directly in the highlight function (that would obviously be mixing concerns).
|
|
286
308
|
|
|
287
|
-
In my experience, using SkandhaJS leads to code reuse and improved structure. On top of that, it offers a good
|
|
288
|
-
debugging experience by logging all changes in the container. Finally, it works nicely with MobX to render the container contents.
|
|
309
|
+
In my experience, using SkandhaJS leads to code reuse and improved structure. On top of that, it offers a good 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,11 @@
|
|
|
1
1
|
export { getCtrState } from './internal/logging';
|
|
2
|
-
export { getOptions, setOptions } from './internal/options';
|
|
3
|
-
export { createConnector } from './lib/Connector';
|
|
2
|
+
export { decorateCb, getOptions, setOptions } from './internal/options';
|
|
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
|
-
export { getOptions, setOptions } from './internal/options';
|
|
3
|
-
export { createConnector } from './lib/Connector';
|
|
2
|
+
export { decorateCb, getOptions, setOptions } from './internal/options';
|
|
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/options.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export declare const options: {
|
|
2
2
|
logging: boolean;
|
|
3
3
|
formatObject: (x: any) => any;
|
|
4
|
+
decorateCb: (x: any) => any;
|
|
4
5
|
};
|
|
5
6
|
export declare const getOptions: () => {
|
|
6
7
|
logging: boolean;
|
|
7
8
|
formatObject: (x: any) => any;
|
|
9
|
+
decorateCb: (x: any) => any;
|
|
8
10
|
};
|
|
9
11
|
export declare const setOptions: (moreOptions: any) => void;
|
|
12
|
+
export declare const decorateCb: (x: any) => any;
|
package/internal/options.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export var options = {
|
|
2
2
|
logging: false,
|
|
3
3
|
formatObject: function (x) { return x; },
|
|
4
|
+
decorateCb: function (x) { return x; },
|
|
4
5
|
};
|
|
5
6
|
export var getOptions = function () { return options; };
|
|
6
7
|
export var setOptions = function (moreOptions) {
|
|
7
8
|
Object.assign(options, moreOptions);
|
|
8
9
|
};
|
|
10
|
+
export var decorateCb = function (x) { return options.decorateCb(x); };
|
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/package.json
CHANGED
package/lib/Connector.d.ts
DELETED
package/lib/Connector.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
}());
|