Skip to content

Commit

Permalink
Introduce Model and refactor of Widget & App
Browse files Browse the repository at this point in the history
  • Loading branch information
lahmatiy committed Jul 17, 2024
1 parent 7205c82 commit 179a5e0
Show file tree
Hide file tree
Showing 10 changed files with 934 additions and 488 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## next

- Introduced the `Model` class as a base for `Widget` and `App`:
- Added a new `setup` option for configuring model-related aspects during initialization (immutable during the lifecycle), such as object markers, additional query methods, etc.
- Implements `loadDataFrom*` methods
- Added handling of empty payload on data loading (raise an error "Empty payload")
- Added `props` options for a view definition, a function (or a string as a jora query) `(data, { props, context}) => any` which should return a normalized props
- Added additional block into inspector popup to display normalized props when `view.options.props` is specified
Expand Down
4 changes: 3 additions & 1 deletion src/lib.js → src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { version } from './version.js';
import { Widget, App } from './main/index.js';
import { Model, Widget, App } from './main/index.js';
import * as views from './views/index.js';
import * as pages from './pages/index.js';
import inspector from './extensions/inspector.js';
Expand All @@ -10,8 +10,10 @@ import { buttons as navButtons } from './nav/index.js';
import jsonxl from './core/encodings/jsonxl.js';
import utils from './core/utils/index.js';

export type * from './main/index.js';
export {
version,
Model,
Widget,
App,
views,
Expand Down
93 changes: 48 additions & 45 deletions src/main/app.js → src/main/app.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
/* eslint-env browser */

import { Widget } from './widget.js';
import upload from '../extensions/upload.js';
import { SetDataProgressOptions, Widget, WidgetEvents, WidgetOptions } from './widget.js';
import upload, { UploadOptions } from '../extensions/upload.js';
import embed from '../extensions/embed-client.js';
import router from '../extensions/router.js';
import { createElement } from '../core/utils/dom.js';
import Progressbar from '../core/utils/progressbar.js';
import Progressbar, { ProgressbarOptions, loadStages } from '../core/utils/progressbar.js';
import * as navButtons from '../nav/buttons.js';
import {
loadDataFromStream,
loadDataFromFile,
loadDataFromEvent,
loadDataFromUrl,
syncLoaderWithProgressbar
} from '../core/utils/load-data.js';
import { syncLoaderWithProgressbar } from '../core/utils/load-data.js';
import { LoadDataBaseOptions, LoadDataState } from '../core/utils/load-data.types.js';

const coalesceOption = (value, fallback) => value !== undefined ? value : fallback;
const mixinEncodings = (host, options) => ({
...options,
encodings: Array.isArray(options?.encodings)
? [...options.encodings, ...host.encodings]
: host.encodings
});

export class App extends Widget {
constructor(options = {}) {

export type AppLoadingState = 'init' | 'error' | 'success';
export type AppLoadingStateOptions<T> =
T extends 'init' ? { progressbar: Progressbar } :
T extends 'error' ? { error: Error & { renderContent?: any }, progressbar: Progressbar } :
undefined;

export interface AppEvents extends WidgetEvents {
startLoadData: [subscribe: Parameters<Progressbar['subscribe']>];
}
export interface AppOptions<T = Widget> extends WidgetOptions<T> {
mode: 'modelfree';
router: boolean;
upload: UploadOptions
embed: boolean;
}
type AppOptionsBind = AppOptions; // to fix: Type parameter 'Options' has a circular default.

export class App<
Options extends AppOptions = AppOptionsBind,
Events extends AppEvents = AppEvents
> extends Widget<Options, Events> {
mode: string | undefined;
_defaultPageId: string | undefined;
declare dom: Widget['dom'] & {
loadingOverlay: HTMLElement;
};

constructor(options: Partial<Options> = {}) {
const extensions = options.extensions ? [options.extensions] : [];

extensions.push(navButtons.darkmodeToggle);
Expand Down Expand Up @@ -62,20 +77,21 @@ export class App extends Widget {
this.mode = this.options.mode;
}

setLoadingState(state, { error, progressbar } = {}) {
setLoadingState<S extends AppLoadingState>(state: S, options?: AppLoadingStateOptions<S>) {
const loadingOverlayEl = this.dom.loadingOverlay;
const { progressbar } = options || {};

switch (state) {
case 'init': {
loadingOverlayEl.classList.remove('error', 'done');

// if progressbar already has parent element -> do nothing
if (progressbar.el.parentNode) {
if (progressbar?.el.parentNode) {
return;
}

loadingOverlayEl.innerHTML = '';
loadingOverlayEl.append(progressbar.el);
loadingOverlayEl.append(progressbar?.el || '');
loadingOverlayEl.classList.add('init');
requestAnimationFrame(() => loadingOverlayEl.classList.remove('init'));

Expand All @@ -89,6 +105,8 @@ export class App extends Widget {
}

case 'error': {
const error = (options as AppLoadingStateOptions<'error'>)?.error;

loadingOverlayEl.classList.add('error');
loadingOverlayEl.innerHTML = '';

Expand Down Expand Up @@ -133,7 +151,7 @@ export class App extends Widget {
}
}

async setDataProgress(data, context, options) {
async setDataProgress(data: unknown, context: unknown, options?: SetDataProgressOptions) {
const dataset = options?.dataset;
const progressbar = options?.progressbar || this.progressbar({ title: 'Set data' });

Expand All @@ -146,7 +164,7 @@ export class App extends Widget {
}
}

progressbar(options) {
progressbar(options: ProgressbarOptions & { title?: string }) {
return new Progressbar({
delay: 200,
domReady: this.dom.ready,
Expand All @@ -159,8 +177,8 @@ export class App extends Widget {
});
}

trackLoadDataProgress(loader) {
const progressbar = this.progressbar({ title: loader.title });
async trackLoadDataProgress(loader: LoadDataState) {
const progressbar = this.progressbar({ title: loadStages[loader.state.value.stage].title });

this.setLoadingState('init', { progressbar });
this.emit('startLoadData', progressbar.subscribe.bind(progressbar));
Expand All @@ -170,38 +188,23 @@ export class App extends Widget {
error => this.setLoadingState('error', { error, progressbar })
);

return loader.result;
await loader.result;
}

loadDataFromStream(stream, options) {
return this.trackLoadDataProgress(loadDataFromStream(
stream,
mixinEncodings(this, typeof options === 'number' ? { size: options } : options)
));
}

loadDataFromEvent(event, options) {
loadDataFromEvent(event: DragEvent | InputEvent, options?: LoadDataBaseOptions) {
if (this.options.mode === 'modelfree' && this.defaultPageId !== this.discoveryPageId) {
this._defaultPageId = this.defaultPageId;
this.defaultPageId = this.discoveryPageId;
this.setPageHash(this.pageHash, true);
this.cancelScheduledRender();
}

return this.trackLoadDataProgress(loadDataFromEvent(event, mixinEncodings(this, options)));
}

loadDataFromFile(file, options) {
return this.trackLoadDataProgress(loadDataFromFile(file, mixinEncodings(this, options)));
}

loadDataFromUrl(url, options) {
return this.trackLoadDataProgress(loadDataFromUrl(url, mixinEncodings(this, options)));
return super.loadDataFromEvent(event, options);
}

unloadData() {
if (this.hasDatasets() && this.options.mode === 'modelfree' && this._defaultPageId !== this.defaultPageId) {
this.defaultPageId = this._defaultPageId;
this.defaultPageId = this._defaultPageId as string;
this.setPageHash(this.pageHash, true);
this.cancelScheduledRender();
}
Expand Down
168 changes: 0 additions & 168 deletions src/main/data-extension-api.js

This file was deleted.

2 changes: 0 additions & 2 deletions src/main/index.js

This file was deleted.

3 changes: 3 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './model.js';
export * from './widget.js';
export { App } from './app.js';
Loading

0 comments on commit 179a5e0

Please sign in to comment.