/
lit_module.ts
189 lines (161 loc) · 6.71 KB
/
lit_module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// tslint:disable:no-new-decorators
import {html, TemplateResult} from 'lit';
import {property} from 'lit/decorators.js';
import {computed, observable} from 'mobx';
import {ReactiveElement} from '../lib/elements';
import {LitModuleClass, ModelInfoMap, SCROLL_SYNC_CSS_CLASS, Spec} from '../lib/types';
import {ApiService, AppState, SelectionService} from '../services/services';
import {app} from './app';
/**
* An interface describing the LitWidget element that contains the LitModule.
*/
export interface ParentWidgetElement {
isLoading: boolean;
}
type IsLoadingFn = (isLoading: boolean) => void;
type OnScrollFn = (scrollTop: number, scrollLeft: number) => void;
/**
* The base class from which all Lit Module classes extends, in order to have
* type safety for dynamically creating modules. Derives from MobxLitElement for
* automatic reactive rendering. Provides a few helper methods for setting up
* explicit mobx reactions with automatic disposal upon component disconnect.
*/
export abstract class LitModule extends ReactiveElement {
/**
* A callback used to set the loading status of the parent widget component.
*/
@property({type: Object}) setIsLoading: IsLoadingFn = (status: boolean) => {};
/**
* A callback used to keep scrolling syncronized between duplicated instances
* of a module. Only used if the class defined by SCROLL_SYNC_CSS_CLASS is
* used in an element in the module. Otherwise scrolling is syncronized using
* the outer container that contains the module.
*/
@property({type: Object}) onSyncScroll: OnScrollFn|null = null;
// Name of this module, to show in the UI.
static title: string = '';
/**
* Information about this module that displays on hover.
*/
static infoMarkdown = '';
// Number of columns of the 12 column horizontal layout.
static numCols: number = 4;
// Whether to collapse this module by default.
static collapseByDefault: boolean = false;
// If true, duplicate this module in example comparison mode.
static duplicateForExampleComparison: boolean = false;
// If true, duplicate this module when running with more than one model.
static duplicateForModelComparison: boolean = true;
// If true, duplicate this module as rows, instead of columns.
static duplicateAsRow: boolean = false;
// Template function. Should return HTML to create this element in the DOM.
static template:
(model: string, selectionServiceIndex: number,
shouldReact: number) => TemplateResult = () => html``;
@observable @property({type: String}) model = '';
@observable @property({type: Number}) selectionServiceIndex = 0;
// tslint:disable:no-any
@observable
protected readonly latestLoadPromises = new Map<string, Promise<any>>();
// tslint:enable:no-any
protected readonly apiService = app.getService(ApiService);
protected readonly appState = app.getService(AppState);
@computed
protected get selectionService() {
return app.getServiceArray(SelectionService)[this.selectionServiceIndex];
}
override updated() {
// If the class defined by SCROLL_SYNC_CSS_CLASS is used in the module then
// set its onscroll callback to propagate to the parent widget.
// There is no need to use this class if a module scrolls through the
// normal mechanism of its parent container div from the LitWidget element
// that wraps modules. But if a module doesn't scroll using that parent
// container, but through some element internal to the module, then using
// this class on that element will allow for scrolling to be syncronized
// across duplicated modules of this type.
const scrollElems = this.shadowRoot!.querySelectorAll<HTMLElement>(
`.${SCROLL_SYNC_CSS_CLASS}`);
for (const elem of scrollElems) {
// The "proper" way to do this is with events, but there is some weirdness
// with re-raising the event to properly cross the shadow DOM boundary
// that leads to very laggy scrolling behavior.
// This direct, imperative callback is much, much smoother.
elem.onscroll = () => {
this.onSyncScroll?.(elem.scrollTop, elem.scrollLeft);
};
}
}
/**
* Base module render function - not to be overridden by clients.
*
* This render function will call the renderImpl method if the module is set
* to react, otherwise it will not render anything.
*
* Any client overridding this method will not get the standard behavior of
* pausing rendering when a module is set to not react. This may cause issues
* as the module will still have reactions paused in this case. Therefore,
* clients should avoid overidding this method and instead they should
* implement renderImpl.
*/
override render() {
// If the module is not reactive, then do not render anything.
if (this.shouldReact === 0) {
return;
}
return this.renderImpl();
}
/**
* Render function for each LIT module to override.
*
* Only called if the module is reactive, meaning it is visibly on-screen.
* Clients should override this method as opposed to the base render() method.
*/
protected renderImpl(): unknown {
return html``;
}
/**
* A helper method for wrapping async API calls in machinery that a)
* automatically sets the loading state of the parent widget container and
* b) ensures that the function only returns the value for the latest async
* call, and null otherwise;
*/
async loadLatest<T>(key: string, promise: Promise<T>): Promise<T|null> {
this.latestLoadPromises.set(key, promise);
this.setIsLoading(true);
const result = await promise;
if (this.latestLoadPromises.get(key) === promise) {
this.setIsLoading(false);
this.latestLoadPromises.delete(key);
return result;
}
return null;
}
/**
* Decide if this module should be displayed, based on the current model(s)
* and dataset.
*/
static shouldDisplayModule(modelSpecs: ModelInfoMap, datasetSpec: Spec) {
return true;
}
}
/**
* A type representing the constructor / class of a LitModule, extended with the
* static properties that need to be defined on a LitModule.
*/
export type LitModuleType = typeof LitModule&LitModuleClass;