diff --git a/src/platform/web/ui/AvatarView.js b/src/platform/web/ui/AvatarView.js index 30fe761f..f2d94e3b 100644 --- a/src/platform/web/ui/AvatarView.js +++ b/src/platform/web/ui/AvatarView.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseUpdateView} from "./general/BaseUpdateView.js"; +import {BaseUpdateView} from "./general/BaseUpdateView"; import {renderStaticAvatar, renderImg} from "./avatar.js"; /* @@ -66,7 +66,7 @@ export class AvatarView extends BaseUpdateView { this._avatarTitleChanged(); this._root = renderStaticAvatar(this.value, this._size); // takes care of update being called when needed - super.mount(options); + this.subscribeOnMount(options); return this._root; } diff --git a/src/platform/web/ui/general/BaseUpdateView.js b/src/platform/web/ui/general/BaseUpdateView.ts similarity index 63% rename from src/platform/web/ui/general/BaseUpdateView.js rename to src/platform/web/ui/general/BaseUpdateView.ts index 4f346499..623df99a 100644 --- a/src/platform/web/ui/general/BaseUpdateView.js +++ b/src/platform/web/ui/general/BaseUpdateView.ts @@ -1,5 +1,6 @@ /* Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Daniel Fedorin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,40 +15,54 @@ See the License for the specific language governing permissions and limitations under the License. */ -export class BaseUpdateView { - constructor(value) { +import {IMountArgs, ViewNode, UIView} from "./types"; + +export interface IObservableValue { + on?(event: "change", handler: (props?: string[]) => void): void; + off?(event: "change", handler: (props?: string[]) => void): void; +} + +export abstract class BaseUpdateView implements UIView { + protected _value: T + protected _boundUpdateFromValue: ((props?: string[]) => void) | null + + abstract mount(args?: IMountArgs): ViewNode; + abstract root(): ViewNode | undefined; + abstract update(...any); + + constructor(value :T) { this._value = value; // TODO: can avoid this if we adopt the handleEvent pattern in our EventListener this._boundUpdateFromValue = null; } - mount(options) { + subscribeOnMount(options?: IMountArgs): void { const parentProvidesUpdates = options && options.parentProvidesUpdates; if (!parentProvidesUpdates) { this._subscribe(); } } - unmount() { + unmount(): void { this._unsubscribe(); } - get value() { + get value(): T { return this._value; } - _updateFromValue(changedProps) { + _updateFromValue(changedProps?: string[]) { this.update(this._value, changedProps); } - _subscribe() { + _subscribe(): void { if (typeof this._value?.on === "function") { - this._boundUpdateFromValue = this._updateFromValue.bind(this); + this._boundUpdateFromValue = this._updateFromValue.bind(this) as (props?: string[]) => void; this._value.on("change", this._boundUpdateFromValue); } } - _unsubscribe() { + _unsubscribe(): void { if (this._boundUpdateFromValue) { if (typeof this._value.off === "function") { this._value.off("change", this._boundUpdateFromValue); diff --git a/src/platform/web/ui/general/ListView.ts b/src/platform/web/ui/general/ListView.ts index 2bd3b7d3..eaa5ea90 100644 --- a/src/platform/web/ui/general/ListView.ts +++ b/src/platform/web/ui/general/ListView.ts @@ -35,7 +35,7 @@ export class ListView implements UIView { private _list: ObservableList; private _className?: string; private _tagName: string; - private _root?: HTMLElement; + private _root?: Element; private _subscription?: SubscriptionHandle; private _childCreator: (value: T) => V; private _childInstances?: V[]; @@ -56,9 +56,9 @@ export class ListView implements UIView { this._mountArgs = {parentProvidesUpdates}; } - root(): HTMLElement { + root(): Element | undefined { // won't be undefined when called between mount and unmount - return this._root!; + return this._root; } update(attributes: IOptions) { @@ -74,12 +74,12 @@ export class ListView implements UIView { } } - mount(): HTMLElement { + mount(): Element { const attr: {[name: string]: any} = {}; if (this._className) { attr.className = this._className; } - this._root = el(this._tagName, attr) as HTMLElement; + this._root = el(this._tagName, attr); this.loadList(); if (this._onItemClick) { this._root!.addEventListener("click", this); @@ -145,15 +145,15 @@ export class ListView implements UIView { protected onRemove(idx: number, value: T) { const [child] = this._childInstances!.splice(idx, 1); - child.root().remove(); + child.root()!.remove(); child.unmount(); } protected onMove(fromIdx: number, toIdx: number, value: T) { const [child] = this._childInstances!.splice(fromIdx, 1); this._childInstances!.splice(toIdx, 0, child); - child.root().remove(); - insertAt(this._root!, toIdx, child.root()); + child.root()!.remove(); + insertAt(this._root!, toIdx, child.root()! as Element); } protected onUpdate(i: number, value: T, params: any) { @@ -170,7 +170,7 @@ export class ListView implements UIView { this.onRemove(index, value); } else { const [oldChild] = this._childInstances!.splice(index, 1, child); - this._root!.replaceChild(child.mount(this._mountArgs), oldChild.root()); + this._root!.replaceChild(child.mount(this._mountArgs), oldChild.root()!); oldChild.unmount(); } } diff --git a/src/platform/web/ui/general/TemplateView.js b/src/platform/web/ui/general/TemplateView.js index f6c7247f..d0be8c0c 100644 --- a/src/platform/web/ui/general/TemplateView.js +++ b/src/platform/web/ui/general/TemplateView.js @@ -16,7 +16,7 @@ limitations under the License. import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS } from "./html"; import {mountView} from "./utils"; -import {BaseUpdateView} from "./BaseUpdateView.js"; +import {BaseUpdateView} from "./BaseUpdateView"; function objHasFns(obj) { for(const value of Object.values(obj)) { @@ -80,7 +80,7 @@ export class TemplateView extends BaseUpdateView { builder.close(); } // takes care of update being called when needed - super.mount(options); + this.subscribeOnMount(options); this._attach(); return this._root; } diff --git a/src/platform/web/ui/general/types.ts b/src/platform/web/ui/general/types.ts index f8f671c7..f38c8a5e 100644 --- a/src/platform/web/ui/general/types.ts +++ b/src/platform/web/ui/general/types.ts @@ -1,5 +1,6 @@ /* Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 Daniel Fedorin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,12 +16,15 @@ limitations under the License. */ export interface IMountArgs { // if true, the parent will call update() rather than the view updating itself by binding to a data source. - parentProvidesUpdates: boolean + parentProvidesUpdates?: boolean }; +// Comment nodes can be used as temporary placeholders for Elements, like TemplateView does. +export type ViewNode = Element | Comment; + export interface UIView { - mount(args?: IMountArgs): HTMLElement; - root(): HTMLElement; // should only be called between mount() and unmount() + mount(args?: IMountArgs): ViewNode; + root(): ViewNode | undefined; // should only be called between mount() and unmount() unmount(): void; update(...any); // this isn't really standarized yet } diff --git a/src/platform/web/ui/general/utils.ts b/src/platform/web/ui/general/utils.ts index 1e4bbdaa..a3d6e7ac 100644 --- a/src/platform/web/ui/general/utils.ts +++ b/src/platform/web/ui/general/utils.ts @@ -41,7 +41,7 @@ export function errorToDOM(error: Error): HTMLElement { ]) as HTMLElement; } -export function insertAt(parentNode: HTMLElement, idx: number, childNode: HTMLElement): void { +export function insertAt(parentNode: Element, idx: number, childNode: Element): void { const isLast = idx === parentNode.childElementCount; if (isLast) { parentNode.appendChild(childNode);