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 {LoginViewModel} from "./LoginViewModel.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}) {
super();
this._createSessionContainer = createSessionContainer;
@ -32,7 +32,7 @@ export class BrawlViewModel extends EventEmitter {
if (sessionContainer) {
this._setSection(() => {
this._sessionContainer = sessionContainer;
this._sessionViewModel = new SessionViewModel(sessionContainer);
this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer}));
});
} else {
// switch between picker and login
@ -96,7 +96,7 @@ export class BrawlViewModel extends EventEmitter {
}
// now set it again
setter();
this.emit("change", "activeSection");
this.emitChange("activeSection");
}
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";
export class LoginViewModel extends EventEmitter {
export class LoginViewModel extends ViewModel {
constructor({sessionCallback, defaultHomeServer, createSessionContainer}) {
super();
this._createSessionContainer = createSessionContainer;
@ -10,20 +10,6 @@ export class LoginViewModel extends EventEmitter {
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 loadViewModel() {return this._loadViewModel; }
@ -42,14 +28,14 @@ export class LoginViewModel extends EventEmitter {
} else {
// show list of session again
this._loadViewModel = null;
this.emit("change", "loadViewModel");
this.emitChange("loadViewModel");
}
},
deleteSessionOnCancel: true,
homeserver,
});
this._loadViewModel.start();
this.emit("change", "loadViewModel");
this.emitChange("loadViewModel");
}
cancel() {

View file

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

View file

@ -1,8 +1,8 @@
import {SortedArray} from "../observable/index.js";
import {EventEmitter} from "../utils/EventEmitter.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {ViewModel} from "./ViewModel.js";
class SessionItemViewModel extends EventEmitter {
class SessionItemViewModel extends ViewModel {
constructor(sessionInfo, pickerVM) {
super();
this._pickerVM = pickerVM;
@ -19,31 +19,31 @@ class SessionItemViewModel extends EventEmitter {
async delete() {
this._isDeleting = true;
this.emit("change", "isDeleting");
this.emitChange("isDeleting");
try {
await this._pickerVM.delete(this.id);
} catch(err) {
this._error = err;
console.error(err);
this.emit("change", "error");
this.emitChange("error");
} finally {
this._isDeleting = false;
this.emit("change", "isDeleting");
this.emitChange("isDeleting");
}
}
async clear() {
this._isClearing = true;
this.emit("change");
this.emitChange();
try {
await this._pickerVM.clear(this.id);
} catch(err) {
this._error = err;
console.error(err);
this.emit("change", "error");
this.emitChange("error");
} finally {
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 blob = new Blob([json], {type: "application/json"});
this._exportDataUrl = URL.createObjectURL(blob);
this.emit("change", "exportDataUrl");
this.emitChange("exportDataUrl");
} catch (err) {
alert(err.message);
console.error(err);
@ -93,13 +93,13 @@ class SessionItemViewModel extends EventEmitter {
if (this._exportDataUrl) {
URL.revokeObjectURL(this._exportDataUrl);
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}) {
super();
this._storageFactory = storageFactory;
@ -141,12 +141,12 @@ export class SessionPickerViewModel extends EventEmitter {
} else {
// show list of session again
this._loadViewModel = null;
this.emit("change", "loadViewModel");
this.emitChange("loadViewModel");
}
}
});
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)
// 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) {
super();
this.disposables = new Disposables();
this.disposables = null;
this._options = options;
}
@ -14,10 +17,33 @@ export class ViewModel extends ObservableValue {
}
track(disposable) {
if (!this.disposables) {
this.disposables = new Disposables();
}
this.disposables.track(disposable);
}
dispose() {
if (this.disposables) {
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 {RoomViewModel} from "./room/RoomViewModel.js";
import {SyncStatusViewModel} from "./SyncStatusViewModel.js";
import {ViewModel} from "../ViewModel.js";
export class SessionViewModel extends EventEmitter {
constructor(sessionContainer) {
super();
export class SessionViewModel extends ViewModel {
constructor(options) {
super(options);
const sessionContainer = options.sessionContainer;
this._session = sessionContainer.session;
this._syncStatusViewModel = new SyncStatusViewModel(sessionContainer.sync);
this._syncStatusViewModel = new SyncStatusViewModel(this.childOptions(sessionContainer.sync));
this._currentRoomViewModel = null;
const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => {
return new RoomTileViewModel({
@ -42,7 +43,7 @@ export class SessionViewModel extends EventEmitter {
if (this._currentRoomViewModel) {
this._currentRoomViewModel.dispose();
this._currentRoomViewModel = null;
this.emit("change", "currentRoom");
this.emitChange("currentRoom");
}
}
@ -50,13 +51,13 @@ export class SessionViewModel extends EventEmitter {
if (this._currentRoomViewModel) {
this._currentRoomViewModel.dispose();
}
this._currentRoomViewModel = new RoomViewModel({
this._currentRoomViewModel = new RoomViewModel(this.childOptions({
room,
ownUserId: this._session.user.id,
closeCallback: () => this._closeCurrentRoom(),
});
}));
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) {
super();
this._sync = sync;
@ -13,7 +13,7 @@ export class SyncStatusViewModel extends EventEmitter {
} else if (status === "started") {
this._error = null;
}
this.emit("change");
this.emitChange();
}
onFirstSubscriptionAdded(name) {
@ -30,7 +30,7 @@ export class SyncStatusViewModel extends EventEmitter {
trySync() {
this._sync.start();
this.emit("change");
this.emitChange();
}
get status() {

View file

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

View file

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