use ViewModel super class for all view models that need binding

This commit is contained in:
Bruno Windels 2020-05-04 19:23:11 +02:00
parent d91ab5355c
commit cc87e35f23
9 changed files with 85 additions and 67 deletions

View file

@ -1,9 +1,9 @@
import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionViewModel} from "./session/SessionViewModel.js";
import {LoginViewModel} from "./LoginViewModel.js"; import {LoginViewModel} from "./LoginViewModel.js";
import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
import {EventEmitter} from "../utils/EventEmitter.js"; import {ViewModel} from "./ViewModel.js";
export class BrawlViewModel extends EventEmitter { export class BrawlViewModel extends ViewModel {
constructor({createSessionContainer, sessionInfoStorage, storageFactory, clock}) { constructor({createSessionContainer, sessionInfoStorage, storageFactory, clock}) {
super(); super();
this._createSessionContainer = createSessionContainer; this._createSessionContainer = createSessionContainer;
@ -32,7 +32,7 @@ export class BrawlViewModel extends EventEmitter {
if (sessionContainer) { if (sessionContainer) {
this._setSection(() => { this._setSection(() => {
this._sessionContainer = sessionContainer; this._sessionContainer = sessionContainer;
this._sessionViewModel = new SessionViewModel(sessionContainer); this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer}));
}); });
} else { } else {
// switch between picker and login // switch between picker and login
@ -96,7 +96,7 @@ export class BrawlViewModel extends EventEmitter {
} }
// now set it again // now set it again
setter(); setter();
this.emit("change", "activeSection"); this.emitChange("activeSection");
} }
get error() { return this._error; } get error() { return this._error; }

View file

@ -1,7 +1,7 @@
import {EventEmitter} from "../utils/EventEmitter.js"; import {ViewModel} from "./ViewModel.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
export class LoginViewModel extends EventEmitter { export class LoginViewModel extends ViewModel {
constructor({sessionCallback, defaultHomeServer, createSessionContainer}) { constructor({sessionCallback, defaultHomeServer, createSessionContainer}) {
super(); super();
this._createSessionContainer = createSessionContainer; this._createSessionContainer = createSessionContainer;
@ -10,20 +10,6 @@ export class LoginViewModel extends EventEmitter {
this._loadViewModel = null; this._loadViewModel = null;
} }
// TODO: this will need to support binding
// if any of the expr is a function, assume the function is a binding, and return a binding function ourselves
i18n(parts, ...expr) {
// just concat for now
let result = "";
for (let i = 0; i < parts.length; ++i) {
result = result + parts[i];
if (i < expr.length) {
result = result + expr[i];
}
}
return result;
}
get defaultHomeServer() { return this._defaultHomeServer; } get defaultHomeServer() { return this._defaultHomeServer; }
get loadViewModel() {return this._loadViewModel; } get loadViewModel() {return this._loadViewModel; }
@ -42,14 +28,14 @@ export class LoginViewModel extends EventEmitter {
} else { } else {
// show list of session again // show list of session again
this._loadViewModel = null; this._loadViewModel = null;
this.emit("change", "loadViewModel"); this.emitChange("loadViewModel");
} }
}, },
deleteSessionOnCancel: true, deleteSessionOnCancel: true,
homeserver, homeserver,
}); });
this._loadViewModel.start(); this._loadViewModel.start();
this.emit("change", "loadViewModel"); this.emitChange("loadViewModel");
} }
cancel() { cancel() {

View file

@ -1,8 +1,8 @@
import {EventEmitter} from "../utils/EventEmitter.js";
import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js";
import {SyncStatus} from "../matrix/Sync.js"; import {SyncStatus} from "../matrix/Sync.js";
import {ViewModel} from "./ViewModel.js";
export class SessionLoadViewModel extends EventEmitter { export class SessionLoadViewModel extends ViewModel {
constructor({createAndStartSessionContainer, sessionCallback, homeserver, deleteSessionOnCancel}) { constructor({createAndStartSessionContainer, sessionCallback, homeserver, deleteSessionOnCancel}) {
super(); super();
this._createAndStartSessionContainer = createAndStartSessionContainer; this._createAndStartSessionContainer = createAndStartSessionContainer;
@ -19,10 +19,10 @@ export class SessionLoadViewModel extends EventEmitter {
} }
try { try {
this._loading = true; this._loading = true;
this.emit("change"); this.emitChange();
this._sessionContainer = this._createAndStartSessionContainer(); this._sessionContainer = this._createAndStartSessionContainer();
this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => {
this.emit("change"); this.emitChange();
// wait for initial sync, but not catchup sync // wait for initial sync, but not catchup sync
const isCatchupSync = s === LoadStatus.FirstSync && const isCatchupSync = s === LoadStatus.FirstSync &&
this._sessionContainer.sync.status === SyncStatus.CatchupSync; this._sessionContainer.sync.status === SyncStatus.CatchupSync;
@ -50,7 +50,7 @@ export class SessionLoadViewModel extends EventEmitter {
this._error = err; this._error = err;
} finally { } finally {
this._loading = false; this._loading = false;
this.emit("change"); this.emitChange();
} }
} }
@ -72,7 +72,7 @@ export class SessionLoadViewModel extends EventEmitter {
this._sessionCallback(); this._sessionCallback();
} catch (err) { } catch (err) {
this._error = err; this._error = err;
this.emit("change"); this.emitChange();
} }
} }

View file

@ -1,8 +1,8 @@
import {SortedArray} from "../observable/index.js"; import {SortedArray} from "../observable/index.js";
import {EventEmitter} from "../utils/EventEmitter.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {ViewModel} from "./ViewModel.js";
class SessionItemViewModel extends EventEmitter { class SessionItemViewModel extends ViewModel {
constructor(sessionInfo, pickerVM) { constructor(sessionInfo, pickerVM) {
super(); super();
this._pickerVM = pickerVM; this._pickerVM = pickerVM;
@ -19,31 +19,31 @@ class SessionItemViewModel extends EventEmitter {
async delete() { async delete() {
this._isDeleting = true; this._isDeleting = true;
this.emit("change", "isDeleting"); this.emitChange("isDeleting");
try { try {
await this._pickerVM.delete(this.id); await this._pickerVM.delete(this.id);
} catch(err) { } catch(err) {
this._error = err; this._error = err;
console.error(err); console.error(err);
this.emit("change", "error"); this.emitChange("error");
} finally { } finally {
this._isDeleting = false; this._isDeleting = false;
this.emit("change", "isDeleting"); this.emitChange("isDeleting");
} }
} }
async clear() { async clear() {
this._isClearing = true; this._isClearing = true;
this.emit("change"); this.emitChange();
try { try {
await this._pickerVM.clear(this.id); await this._pickerVM.clear(this.id);
} catch(err) { } catch(err) {
this._error = err; this._error = err;
console.error(err); console.error(err);
this.emit("change", "error"); this.emitChange("error");
} finally { } finally {
this._isClearing = false; this._isClearing = false;
this.emit("change", "isClearing"); this.emitChange("isClearing");
} }
} }
@ -82,7 +82,7 @@ class SessionItemViewModel extends EventEmitter {
const json = JSON.stringify(data, undefined, 2); const json = JSON.stringify(data, undefined, 2);
const blob = new Blob([json], {type: "application/json"}); const blob = new Blob([json], {type: "application/json"});
this._exportDataUrl = URL.createObjectURL(blob); this._exportDataUrl = URL.createObjectURL(blob);
this.emit("change", "exportDataUrl"); this.emitChange("exportDataUrl");
} catch (err) { } catch (err) {
alert(err.message); alert(err.message);
console.error(err); console.error(err);
@ -93,13 +93,13 @@ class SessionItemViewModel extends EventEmitter {
if (this._exportDataUrl) { if (this._exportDataUrl) {
URL.revokeObjectURL(this._exportDataUrl); URL.revokeObjectURL(this._exportDataUrl);
this._exportDataUrl = null; this._exportDataUrl = null;
this.emit("change", "exportDataUrl"); this.emitChange("exportDataUrl");
} }
} }
} }
export class SessionPickerViewModel extends EventEmitter { export class SessionPickerViewModel extends ViewModel {
constructor({storageFactory, sessionInfoStorage, sessionCallback, createSessionContainer}) { constructor({storageFactory, sessionInfoStorage, sessionCallback, createSessionContainer}) {
super(); super();
this._storageFactory = storageFactory; this._storageFactory = storageFactory;
@ -141,12 +141,12 @@ export class SessionPickerViewModel extends EventEmitter {
} else { } else {
// show list of session again // show list of session again
this._loadViewModel = null; this._loadViewModel = null;
this.emit("change", "loadViewModel"); this.emitChange("loadViewModel");
} }
} }
}); });
this._loadViewModel.start(); this._loadViewModel.start();
this.emit("change", "loadViewModel"); this.emitChange("loadViewModel");
} }
} }

View file

@ -2,10 +2,13 @@
// as in some cases it would really be more convenient to have multiple events (like telling the timeline to scroll down) // as in some cases it would really be more convenient to have multiple events (like telling the timeline to scroll down)
// we do need to return a disposable from EventEmitter.on, or at least have a method here to easily track a subscription to an EventEmitter // we do need to return a disposable from EventEmitter.on, or at least have a method here to easily track a subscription to an EventEmitter
export class ViewModel extends ObservableValue { import {EventEmitter} from "../utils/EventEmitter.js";
import {Disposables} from "../utils/Disposables.js";
export class ViewModel extends EventEmitter {
constructor(options) { constructor(options) {
super(); super();
this.disposables = new Disposables(); this.disposables = null;
this._options = options; this._options = options;
} }
@ -14,10 +17,33 @@ export class ViewModel extends ObservableValue {
} }
track(disposable) { track(disposable) {
if (!this.disposables) {
this.disposables = new Disposables();
}
this.disposables.track(disposable); this.disposables.track(disposable);
} }
dispose() { dispose() {
if (this.disposables) {
this.disposables.dispose(); this.disposables.dispose();
} }
}
// TODO: this will need to support binding
// if any of the expr is a function, assume the function is a binding, and return a binding function ourselves
i18n(parts, ...expr) {
// just concat for now
let result = "";
for (let i = 0; i < parts.length; ++i) {
result = result + parts[i];
if (i < expr.length) {
result = result + expr[i];
}
}
return result;
}
emitChange(changedProps) {
this.emit("change", changedProps);
}
} }

View file

@ -1,13 +1,14 @@
import {EventEmitter} from "../../utils/EventEmitter.js";
import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js";
import {RoomViewModel} from "./room/RoomViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js";
import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; import {SyncStatusViewModel} from "./SyncStatusViewModel.js";
import {ViewModel} from "../ViewModel.js";
export class SessionViewModel extends EventEmitter { export class SessionViewModel extends ViewModel {
constructor(sessionContainer) { constructor(options) {
super(); super(options);
const sessionContainer = options.sessionContainer;
this._session = sessionContainer.session; this._session = sessionContainer.session;
this._syncStatusViewModel = new SyncStatusViewModel(sessionContainer.sync); this._syncStatusViewModel = new SyncStatusViewModel(this.childOptions(sessionContainer.sync));
this._currentRoomViewModel = null; this._currentRoomViewModel = null;
const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => { const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => {
return new RoomTileViewModel({ return new RoomTileViewModel({
@ -42,7 +43,7 @@ export class SessionViewModel extends EventEmitter {
if (this._currentRoomViewModel) { if (this._currentRoomViewModel) {
this._currentRoomViewModel.dispose(); this._currentRoomViewModel.dispose();
this._currentRoomViewModel = null; this._currentRoomViewModel = null;
this.emit("change", "currentRoom"); this.emitChange("currentRoom");
} }
} }
@ -50,13 +51,13 @@ export class SessionViewModel extends EventEmitter {
if (this._currentRoomViewModel) { if (this._currentRoomViewModel) {
this._currentRoomViewModel.dispose(); this._currentRoomViewModel.dispose();
} }
this._currentRoomViewModel = new RoomViewModel({ this._currentRoomViewModel = new RoomViewModel(this.childOptions({
room, room,
ownUserId: this._session.user.id, ownUserId: this._session.user.id,
closeCallback: () => this._closeCurrentRoom(), closeCallback: () => this._closeCurrentRoom(),
}); }));
this._currentRoomViewModel.load(); this._currentRoomViewModel.load();
this.emit("change", "currentRoom"); this.emitChange("currentRoom");
} }
} }

View file

@ -1,6 +1,6 @@
import {EventEmitter} from "../../utils/EventEmitter.js"; import {ViewModel} from "../ViewModel.js";
export class SyncStatusViewModel extends EventEmitter { export class SyncStatusViewModel extends ViewModel {
constructor(sync) { constructor(sync) {
super(); super();
this._sync = sync; this._sync = sync;
@ -13,7 +13,7 @@ export class SyncStatusViewModel extends EventEmitter {
} else if (status === "started") { } else if (status === "started") {
this._error = null; this._error = null;
} }
this.emit("change"); this.emitChange();
} }
onFirstSubscriptionAdded(name) { onFirstSubscriptionAdded(name) {
@ -30,7 +30,7 @@ export class SyncStatusViewModel extends EventEmitter {
trySync() { trySync() {
this._sync.start(); this._sync.start();
this.emit("change"); this.emitChange();
} }
get status() { get status() {

View file

@ -1,10 +1,11 @@
import {EventEmitter} from "../../../utils/EventEmitter.js";
import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {TimelineViewModel} from "./timeline/TimelineViewModel.js";
import {avatarInitials} from "../avatar.js"; import {avatarInitials} from "../avatar.js";
import {ViewModel} from "../../ViewModel.js";
export class RoomViewModel extends EventEmitter { export class RoomViewModel extends ViewModel {
constructor({room, ownUserId, closeCallback}) { constructor(options) {
super(); super(options);
const {room, ownUserId, closeCallback} = options;
this._room = room; this._room = room;
this._ownUserId = ownUserId; this._ownUserId = ownUserId;
this._timeline = null; this._timeline = null;
@ -19,12 +20,16 @@ export class RoomViewModel extends EventEmitter {
this._room.on("change", this._onRoomChange); this._room.on("change", this._onRoomChange);
try { try {
this._timeline = await this._room.openTimeline(); this._timeline = await this._room.openTimeline();
this._timelineVM = new TimelineViewModel(this._room, this._timeline, this._ownUserId); this._timelineVM = new TimelineViewModel(this.childOptions({
this.emit("change", "timelineViewModel"); room: this._room,
timeline: this._timeline,
ownUserId: this._ownUserId,
}));
this.emitChange("timelineViewModel");
} catch (err) { } catch (err) {
console.error(`room.openTimeline(): ${err.message}:\n${err.stack}`); console.error(`room.openTimeline(): ${err.message}:\n${err.stack}`);
this._timelineError = err; this._timelineError = err;
this.emit("change", "error"); this.emitChange("error");
} }
} }
@ -43,7 +48,7 @@ export class RoomViewModel extends EventEmitter {
// room doesn't tell us yet which fields changed, // room doesn't tell us yet which fields changed,
// so emit all fields originating from summary // so emit all fields originating from summary
_onRoomChange() { _onRoomChange() {
this.emit("change", "name"); this.emitChange("name");
} }
get name() { get name() {
@ -76,7 +81,7 @@ export class RoomViewModel extends EventEmitter {
console.error(`room.sendMessage(): ${err.message}:\n${err.stack}`); console.error(`room.sendMessage(): ${err.message}:\n${err.stack}`);
this._sendError = err; this._sendError = err;
this._timelineError = null; this._timelineError = null;
this.emit("change", "error"); this.emitChange("error");
return false; return false;
} }
return true; return true;

View file

@ -18,7 +18,7 @@ import {TilesCollection} from "./TilesCollection.js";
import {tilesCreator} from "./tilesCreator.js"; import {tilesCreator} from "./tilesCreator.js";
export class TimelineViewModel { export class TimelineViewModel {
constructor(room, timeline, ownUserId) { constructor({room, timeline, ownUserId}) {
this._timeline = timeline; this._timeline = timeline;
// once we support sending messages we could do // once we support sending messages we could do
// timeline.entries.concat(timeline.pendingEvents) // timeline.entries.concat(timeline.pendingEvents)