skandha 0.9.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 +133 -149
- package/lib/Connector.d.ts +3 -3
- package/lib/Connector.js +50 -41
- package/lib/registerCtr.d.ts +1 -1
- package/lib/registerCtr.js +4 -4
- package/package.json +1 -1
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,103 @@ 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
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
## Glossary (technical)
|
|
23
|
+
|
|
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.
|
|
27
|
+
|
|
28
|
+
## Example: a simplified Selection facet
|
|
29
|
+
|
|
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.
|
|
19
31
|
|
|
20
32
|
```
|
|
21
33
|
// file: Selection.ts
|
|
22
34
|
import { input, output, operation } from 'skandha';
|
|
23
|
-
import { host, stub } from 'aspiration';
|
|
24
35
|
|
|
25
36
|
export type SelectionParamsT = { itemId?: string; isShift?: boolean; isCtrl?: boolean; };
|
|
26
37
|
|
|
27
38
|
export class Selection<ValueT = any> {
|
|
28
39
|
static className = () => 'Selection';
|
|
29
40
|
|
|
30
|
-
@input selectableIds: Array<string> =
|
|
41
|
+
@input selectableIds: Array<string> = [];
|
|
31
42
|
@output ids: Array<string> = [];
|
|
32
43
|
@output anchorId?: string;
|
|
33
|
-
@output items: Array<ValueT> =
|
|
44
|
+
@output items: Array<ValueT> = [];
|
|
34
45
|
|
|
35
|
-
@operation
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return (cbs: Selection_selectItem) => {
|
|
39
|
-
cbs.select();
|
|
40
|
-
};
|
|
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.
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
51
|
```
|
|
44
52
|
|
|
45
|
-
## Fact:
|
|
53
|
+
## Fact: facets have @input, @data and @output members
|
|
46
54
|
|
|
47
|
-
In
|
|
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()`.
|
|
48
56
|
|
|
49
|
-
## Fact:
|
|
57
|
+
## Fact: operations are decorated with `@operation`.
|
|
50
58
|
|
|
51
|
-
|
|
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.
|
|
52
61
|
|
|
53
|
-
## Fact:
|
|
62
|
+
## Fact: operations are logged
|
|
54
63
|
|
|
55
|
-
|
|
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.
|
|
56
65
|
|
|
57
|
-
##
|
|
66
|
+
## Example: a container with Selection, Highlight and Filtering
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
export class Selection_selectItem extends Cbs {
|
|
62
|
-
selectionParams: SelectionParamsT = stub();
|
|
63
|
-
select() {}
|
|
64
|
-
}
|
|
65
|
-
```
|
|
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.
|
|
66
70
|
|
|
67
71
|
```
|
|
68
72
|
// file: createContainer.ts
|
|
69
|
-
import {
|
|
73
|
+
import { Selection_selectItem } from 'SelectionCbs';
|
|
70
74
|
|
|
71
75
|
const createContainer() {
|
|
72
76
|
const ctr = {
|
|
73
|
-
selection: new Selection()
|
|
77
|
+
selection: new Selection(),
|
|
78
|
+
highlight: new Highlight(),
|
|
79
|
+
filtering: new Filtering()
|
|
74
80
|
};
|
|
75
81
|
|
|
76
82
|
setCallbacks(ctr.selection, {
|
|
77
83
|
selectItem: {
|
|
78
84
|
select(this: Selection_selectItem): {
|
|
79
85
|
handleSelectItem(ctr.selection, this.selectionParams);
|
|
86
|
+
highlightFollowsSelection(ctr, this.selectionParams);
|
|
80
87
|
},
|
|
81
88
|
}
|
|
82
89
|
});
|
|
83
90
|
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
setCallbacks(ctr.filtering, {
|
|
92
|
+
apply: {
|
|
93
|
+
exit() {
|
|
94
|
+
highlightIsCorrectedOnFilterChange(ctr);
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
registerCtr({ ctr: ctr, options: { name: 'Todos'} });
|
|
86
100
|
return ctr;
|
|
87
101
|
}
|
|
88
102
|
```
|
|
89
103
|
|
|
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
|
|
104
|
+
```
|
|
105
|
+
// file: highlightFollowsSelection.ts
|
|
106
|
+
const highlightFollowsSelection = (ctr: any, selectionParams: SelectionParamsT) => {
|
|
107
|
+
if (!selectionParams.isShift && !selectionParams.isCtrl) {
|
|
108
|
+
ctr.highlight.highlightItem(selectionParams.itemId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
106
112
|
|
|
107
113
|
```
|
|
108
114
|
// file: handleSelectItem.ts
|
|
@@ -133,145 +139,125 @@ export function handleSelectItem(
|
|
|
133
139
|
}
|
|
134
140
|
```
|
|
135
141
|
|
|
136
|
-
##
|
|
142
|
+
## Note: the todos container has selection, highlight and filtering
|
|
137
143
|
|
|
138
|
-
|
|
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
|
|
139
145
|
|
|
140
|
-
##
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
// file: Highlight.ts
|
|
144
|
-
export class Highlight_highlightItem extends Cbs {
|
|
145
|
-
id: string = stub();
|
|
146
|
-
scrollItemIntoView() {}
|
|
147
|
-
}
|
|
146
|
+
## Fact: callbacks are installed with `setCallbacks`
|
|
148
147
|
|
|
149
|
-
|
|
150
|
-
@data id?: string;
|
|
151
|
-
@data item?: ValueT;
|
|
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.
|
|
152
149
|
|
|
153
|
-
|
|
154
|
-
return (cbs: Highlight_highlightItem) => {
|
|
155
|
-
this.id = id;
|
|
156
|
-
if (cbs.scrollItemIntoView) {
|
|
157
|
-
cbs.scrollItemIntoView();
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
```
|
|
150
|
+
## Fact: callbacks can take care of side effects
|
|
163
151
|
|
|
164
|
-
|
|
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.
|
|
165
153
|
|
|
166
|
-
|
|
154
|
+
## Fact: the callback function can access the callbacks object through `this`.
|
|
167
155
|
|
|
168
|
-
|
|
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`.
|
|
169
157
|
|
|
170
|
-
|
|
171
|
-
// file: createContainer.ts
|
|
172
|
-
const createContainer() {
|
|
173
|
-
const ctr = {
|
|
174
|
-
selection: new Selection(),
|
|
175
|
-
highlight: new Highlight()
|
|
176
|
-
};
|
|
158
|
+
## Fact: the `exit` callback function is called at the end of an operation
|
|
177
159
|
|
|
178
|
-
|
|
179
|
-
selectItem: {
|
|
180
|
-
select(this: Selection_selectItem): {
|
|
181
|
-
handleSelectItem(ctr.selection, this.selectionParams);
|
|
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.
|
|
182
161
|
|
|
183
|
-
|
|
184
|
-
ctr.highlight.highlightItem(this.selectionParams.itemId);
|
|
185
|
-
}
|
|
162
|
+
## Example: a complete Selection facet
|
|
186
163
|
|
|
187
|
-
|
|
188
|
-
// highlightFollowsSelection(ctr, this.selectionParams);
|
|
189
|
-
},
|
|
190
|
-
}
|
|
191
|
-
});
|
|
164
|
+
It's time now to return to the Selection facet and show how callbacks really work.
|
|
192
165
|
|
|
193
|
-
return ctr;
|
|
194
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';
|
|
195
171
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
Above we see for the first time how SkandhaJS helps you to create co-ordinated behaviours. When the client calls `ctr.selection.selectItem` and the `select` callback is executed, then the callback ensures that the selected item is also highlighted. As the comment in the snippet suggests, we can make the body of the `select` callback more readable by extracting some of the code in a `highlightFollowsSelection` helper function.
|
|
172
|
+
export class Selection<ValueT = any> {
|
|
173
|
+
static className = () => 'Selection';
|
|
199
174
|
|
|
200
|
-
|
|
175
|
+
@input selectableIds: Array<string> = stub();
|
|
176
|
+
@output ids: Array<string> = [];
|
|
177
|
+
@output anchorId?: string;
|
|
178
|
+
@output items: Array<ValueT> = stub();
|
|
201
179
|
|
|
180
|
+
@operation @host(['selectionParams']) selectItem(
|
|
181
|
+
selectionParams: SelectionParamsT
|
|
182
|
+
) {
|
|
183
|
+
return (cbs: Selection_selectItem) => {
|
|
184
|
+
cbs.select();
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
202
188
|
```
|
|
203
|
-
// file: createContainer.ts
|
|
204
|
-
const createContainer() {
|
|
205
|
-
const ctr = {
|
|
206
|
-
selection: new Selection(),
|
|
207
|
-
highlight: new Highlight(),
|
|
208
|
-
filtering: new Filtering()
|
|
209
|
-
};
|
|
210
189
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
handleSelectItem(ctr.selection, this.selectionParams);
|
|
215
|
-
highlightFollowsSelection(ctr, this.selectionParams);
|
|
216
|
-
},
|
|
217
|
-
}
|
|
218
|
-
});
|
|
190
|
+
```
|
|
191
|
+
// file: SelectionCbs.ts
|
|
192
|
+
import { stub } from 'aspiration';
|
|
219
193
|
|
|
220
|
-
|
|
221
|
-
apply: {
|
|
222
|
-
exit() {
|
|
223
|
-
highlightIsCorrectedOnFilterChange(ctr);
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
});
|
|
194
|
+
export type SelectionParamsT = { itemId?: string; isShift?: boolean; isCtrl?: boolean; };
|
|
227
195
|
|
|
228
|
-
|
|
196
|
+
export class Selection_selectItem extends Cbs {
|
|
197
|
+
selectionParams: SelectionParamsT = stub();
|
|
198
|
+
select() {}
|
|
229
199
|
}
|
|
230
200
|
```
|
|
231
201
|
|
|
232
|
-
## Fact: the `
|
|
202
|
+
## Fact: the `@host` decorator is used to enable callbacks in an operation.
|
|
203
|
+
|
|
204
|
+
The `@host` decorator comes from the `Aspiration` library. When you call a function `f` that is decorated with `@host` then the following happens:
|
|
205
|
+
|
|
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)`.
|
|
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
|
|
233
217
|
|
|
234
|
-
|
|
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.
|
|
235
219
|
|
|
236
|
-
##
|
|
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.
|
|
237
223
|
|
|
238
224
|
```
|
|
239
225
|
// file: createContainer.ts
|
|
240
226
|
import * from 'ramda' as R;
|
|
241
227
|
|
|
242
228
|
const connectContainer(ctr: any) {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
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(),
|
|
238
|
+
}
|
|
253
239
|
|
|
254
|
-
const
|
|
240
|
+
const con = createConnector({ data: data, todos: todos });
|
|
241
|
+
const lookUpTodo = (id: string) => outputsCtr.filteredTodoById[id];
|
|
255
242
|
|
|
256
|
-
con.filtering.inputItems = con
|
|
257
|
-
con
|
|
258
|
-
con.selection.selectableIds = con
|
|
259
|
-
con.selection.item = con.selection.ids.tf(lookUpTodos);
|
|
260
|
-
con.highlight.item = con.highlight.id.tf(lookUpTodo);
|
|
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);
|
|
261
248
|
|
|
262
249
|
con.connect();
|
|
263
250
|
```
|
|
264
251
|
|
|
265
252
|
## Fact: the Connector class maps data between objects
|
|
266
253
|
|
|
267
|
-
The Connector instance allows you to map data from one object onto another object.
|
|
268
|
-
|
|
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`.
|
|
269
256
|
|
|
270
257
|
### Logging
|
|
271
258
|
|
|
272
|
-
By calling `setOptions({logging: true})`, the SkandhaJS library allows you to inspect each facet before and
|
|
273
|
-
|
|
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:
|
|
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:
|
|
275
261
|
|
|
276
262
|
- before and after each operation, the entire container that holds the facet is logged
|
|
277
263
|
- all data members of each facet in the container are included in the log
|
|
@@ -279,15 +265,13 @@ This will log Facet members that are decorated with `@data` (or `@input`, or `@o
|
|
|
279
265
|
|
|
280
266
|
### Making facets observable with Mobx
|
|
281
267
|
|
|
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
|
|
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
|
|
285
269
|
either observable or computed:
|
|
286
270
|
|
|
287
271
|
```
|
|
288
272
|
registerCtr({
|
|
289
273
|
ctr: ctr,
|
|
290
|
-
options: { name: '
|
|
274
|
+
options: { name: 'Todos'},
|
|
291
275
|
});
|
|
292
276
|
```
|
|
293
277
|
|
package/lib/Connector.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
declare type
|
|
2
|
-
|
|
1
|
+
declare type OptionsT = {
|
|
2
|
+
strict: boolean;
|
|
3
3
|
};
|
|
4
|
-
export declare const createConnector: (
|
|
4
|
+
export declare const createConnector: (obj: any, options?: OptionsT) => any;
|
|
5
5
|
export {};
|
package/lib/Connector.js
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
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
|
+
}
|
|
2
12
|
var Connector = /** @class */ (function () {
|
|
3
|
-
function Connector(
|
|
13
|
+
function Connector(obj, options) {
|
|
4
14
|
var _this = this;
|
|
5
15
|
var _a;
|
|
6
|
-
this.
|
|
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
|
+
};
|
|
7
33
|
this.connect = function () {
|
|
8
|
-
for (var _i = 0, _a = Object.values(_this.
|
|
34
|
+
for (var _i = 0, _a = Object.values(_this.entityByKey); _i < _a.length; _i++) {
|
|
9
35
|
var entity = _a[_i];
|
|
10
36
|
_this._connectEntity(entity);
|
|
11
37
|
}
|
|
@@ -13,12 +39,11 @@ var Connector = /** @class */ (function () {
|
|
|
13
39
|
this._connectEntity = function (entity) {
|
|
14
40
|
var _loop_1 = function (entry) {
|
|
15
41
|
var name_1 = entry[0], field = entry[1];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
throw new Error("Cannot override the field ".concat(name_1, " in ").concat(obj, ".") +
|
|
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, ".") +
|
|
19
44
|
'Field not found. Disable strict mode to ignore this error.');
|
|
20
45
|
}
|
|
21
|
-
mapDataToProp(obj, name_1, function () {
|
|
46
|
+
mapDataToProp(entity.obj, name_1, function () {
|
|
22
47
|
return field.getSource();
|
|
23
48
|
});
|
|
24
49
|
};
|
|
@@ -30,64 +55,48 @@ var Connector = /** @class */ (function () {
|
|
|
30
55
|
this.options = {
|
|
31
56
|
strict: (_a = options === null || options === void 0 ? void 0 : options.strict) !== null && _a !== void 0 ? _a : true,
|
|
32
57
|
};
|
|
33
|
-
|
|
34
|
-
var entry = _b[_i];
|
|
35
|
-
var name_2 = entry[0], getObj = entry[1];
|
|
36
|
-
this.entityByName[name_2] = new Entity(getObj);
|
|
37
|
-
}
|
|
58
|
+
this.obj = obj;
|
|
38
59
|
}
|
|
39
60
|
return Connector;
|
|
40
61
|
}());
|
|
41
|
-
export var createConnector = function (
|
|
42
|
-
var con = new Connector(
|
|
62
|
+
export var createConnector = function (obj, options) {
|
|
63
|
+
var con = new Connector(obj, options);
|
|
43
64
|
return new Proxy(con, {
|
|
44
65
|
get: function (target, key) {
|
|
45
|
-
|
|
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];
|
|
66
|
+
var _a;
|
|
67
|
+
return (_a = target[key]) !== null && _a !== void 0 ? _a : target.getOrCreateEntity(key);
|
|
58
68
|
},
|
|
59
69
|
});
|
|
60
70
|
};
|
|
61
71
|
var Entity = /** @class */ (function () {
|
|
62
|
-
function Entity(
|
|
72
|
+
function Entity(obj) {
|
|
63
73
|
var _this = this;
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
71
81
|
}
|
|
72
82
|
return Entity;
|
|
73
83
|
}());
|
|
74
84
|
var Field = /** @class */ (function () {
|
|
75
|
-
function Field(
|
|
85
|
+
function Field(obj, name) {
|
|
76
86
|
var _this = this;
|
|
77
87
|
this.transforms = [];
|
|
78
88
|
this.getSource = function () {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
throw new Error("Field ".concat(_this.name, " does not exist on object ").concat(obj));
|
|
89
|
+
if (!(_this.name in _this.obj)) {
|
|
90
|
+
throw new Error("Field ".concat(_this.name, " does not exist on object ").concat(_this.obj));
|
|
82
91
|
}
|
|
83
|
-
var result = obj[_this.name];
|
|
92
|
+
var result = _this.obj[_this.name];
|
|
84
93
|
for (var _i = 0, _a = _this.transforms; _i < _a.length; _i++) {
|
|
85
94
|
var tf = _a[_i];
|
|
86
95
|
result = tf(result);
|
|
87
96
|
}
|
|
88
97
|
return result;
|
|
89
98
|
};
|
|
90
|
-
this.
|
|
99
|
+
this.obj = obj;
|
|
91
100
|
this.name = name;
|
|
92
101
|
}
|
|
93
102
|
Field.prototype.tf = function (transform) {
|
package/lib/registerCtr.d.ts
CHANGED
package/lib/registerCtr.js
CHANGED
|
@@ -10,12 +10,12 @@ 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 ".concat(
|
|
18
|
+
console.error("Facet ".concat(facetName, " of container ").concat(ctrAdmin.logName, " ") +
|
|
19
19
|
"does not have a static className function");
|
|
20
20
|
}
|
|
21
21
|
var className = facetClassName(facet.constructor);
|