reduce navigation boilerplate

this makes the url router adjust the url when the navigation path is
changed, instead of doing urlRouter.applyUrl() and
urlRouter.history.pushUrl().

This history field and applyUrl method on URLRouter are now private,
as the URLRouter should only be used to generate urls you want to
put in an <a href="..."></a>, anything else should use navigator.push()
This commit is contained in:
Bruno Windels 2020-10-16 12:46:14 +02:00
parent ddf7d01760
commit 788bce7904
7 changed files with 74 additions and 89 deletions

View file

@ -35,13 +35,13 @@ export class RootViewModel extends ViewModel {
this._sessionViewModel = null; this._sessionViewModel = null;
} }
async load(lastUrlHash) { async load() {
this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation()));
this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation()));
this._applyNavigation(lastUrlHash); this._applyNavigation(this.urlRouter.getLastUrl());
} }
async _applyNavigation(restoreHashIfAtDefault) { async _applyNavigation(restoreUrlIfAtDefault) {
const isLogin = this.navigation.observe("login").get(); const isLogin = this.navigation.observe("login").get();
const sessionId = this.navigation.observe("session").get(); const sessionId = this.navigation.observe("session").get();
if (isLogin) { if (isLogin) {
@ -58,30 +58,24 @@ export class RootViewModel extends ViewModel {
} }
} else { } else {
try { try {
let url = restoreHashIfAtDefault; if (restoreUrlIfAtDefault) {
if (!url) { this.urlRouter.pushUrl(restoreUrlIfAtDefault);
// redirect depending on what sessions are already present } else {
const sessionInfos = await this._sessionInfoStorage.getAll(); const sessionInfos = await this._sessionInfoStorage.getAll();
url = this._urlForSessionInfos(sessionInfos); if (sessionInfos.length === 0) {
this.navigation.push("login");
} else if (sessionInfos.length === 1) {
this.navigation.push("session", sessionInfos[0].id);
} else {
this.navigation.push("session");
}
} }
this.urlRouter.history.replaceUrl(url);
this.urlRouter.applyUrl(url);
} catch (err) { } catch (err) {
this._setSection(() => this._error = err); this._setSection(() => this._error = err);
} }
} }
} }
_urlForSessionInfos(sessionInfos) {
if (sessionInfos.length === 0) {
return this.urlRouter.urlForSegment("login");
} else if (sessionInfos.length === 1) {
return this.urlRouter.urlForSegment("session", sessionInfos[0].id);
} else {
return this.urlRouter.urlForSegment("session");
}
}
async _showPicker() { async _showPicker() {
this._setSection(() => { this._setSection(() => {
this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions({ this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions({
@ -102,10 +96,8 @@ export class RootViewModel extends ViewModel {
defaultHomeServer: "https://matrix.org", defaultHomeServer: "https://matrix.org",
createSessionContainer: this._createSessionContainer, createSessionContainer: this._createSessionContainer,
ready: sessionContainer => { ready: sessionContainer => {
const url = this.urlRouter.urlForSegment("session", sessionContainer.sessionId);
this.urlRouter.applyUrl(url);
this.urlRouter.history.replaceUrl(url);
this._showSession(sessionContainer); this._showSession(sessionContainer);
this.navigation.push("session", sessionContainer.sessionId);
}, },
})); }));
}); });

View file

@ -14,19 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {BaseObservableValue} from "../../observable/ObservableValue.js"; import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue.js";
export class Navigation { export class Navigation {
constructor(allowsChild) { constructor(allowsChild) {
this._allowsChild = allowsChild; this._allowsChild = allowsChild;
this._path = new Path([], allowsChild); this._path = new Path([], allowsChild);
this._observables = new Map(); this._observables = new Map();
this._pathObservable = new ObservableValue(this._path);
}
get pathObservable() {
return this._pathObservable;
} }
get path() { get path() {
return this._path; return this._path;
} }
push(type, value = undefined) {
return this.applyPath(this.path.with(new Segment(type, value)));
}
applyPath(path) { applyPath(path) {
// Path is not exported, so you can only create a Path through Navigation, // Path is not exported, so you can only create a Path through Navigation,
// so we assume it respects the allowsChild rules // so we assume it respects the allowsChild rules
@ -45,6 +54,10 @@ export class Navigation {
const observable = this._observables.get(segment.type); const observable = this._observables.get(segment.type);
observable?.emitIfChanged(); observable?.emitIfChanged();
} }
// to observe the whole path having changed
// Since paths are immutable,
// we can just use set here which will compare the references
this._pathObservable.set(this._path);
} }
observe(type) { observe(type) {

View file

@ -14,40 +14,50 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Segment} from "./Navigation.js";
export class URLRouter { export class URLRouter {
constructor({history, navigation, parseUrlPath, stringifyPath}) { constructor({history, navigation, parseUrlPath, stringifyPath}) {
this._subscription = null;
this._history = history; this._history = history;
this._navigation = navigation; this._navigation = navigation;
this._parseUrlPath = parseUrlPath; this._parseUrlPath = parseUrlPath;
this._stringifyPath = stringifyPath; this._stringifyPath = stringifyPath;
this._subscription = null;
this._pathSubscription = null;
} }
attach() { attach() {
this._subscription = this._history.subscribe(url => { this._subscription = this._history.subscribe(url => {
const redirectedUrl = this.applyUrl(url); const redirectedUrl = this._applyUrl(url);
if (redirectedUrl !== url) { if (redirectedUrl !== url) {
this._history.replaceUrl(redirectedUrl); this._history.replaceUrlSilently(redirectedUrl);
}
});
this._applyUrl(this._history.get());
this._pathSubscription = this._navigation.pathObservable.subscribe(path => {
const url = this.urlForPath(path);
if (url !== this._history.get()) {
this._history.pushUrlSilently(url);
} }
}); });
this.applyUrl(this._history.get());
} }
dispose() { dispose() {
this._subscription = this._subscription(); this._subscription = this._subscription();
this._pathSubscription = this._pathSubscription();
} }
applyUrl(url) { _applyUrl(url) {
const urlPath = this._history.urlAsPath(url) const urlPath = this._history.urlAsPath(url)
const navPath = this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path)); const navPath = this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path));
this._navigation.applyPath(navPath); this._navigation.applyPath(navPath);
return this._history.pathAsUrl(this._stringifyPath(navPath)); return this._history.pathAsUrl(this._stringifyPath(navPath));
} }
get history() { pushUrl(url) {
return this._history; this._history.pushUrl(url);
}
getLastUrl() {
return this._history.getLastUrl();
} }
urlForSegments(segments) { urlForSegments(segments) {
@ -70,7 +80,7 @@ export class URLRouter {
} }
urlForPath(path) { urlForPath(path) {
return this.history.pathAsUrl(this._stringifyPath(path)); return this._history.pathAsUrl(this._stringifyPath(path));
} }
openRoomActionUrl(roomId) { openRoomActionUrl(roomId) {
@ -78,26 +88,4 @@ export class URLRouter {
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`; const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
return this._history.pathAsUrl(urlPath); return this._history.pathAsUrl(urlPath);
} }
disableGridUrl() {
let path = this._navigation.path.until("session");
const room = this._navigation.path.get("room");
if (room) {
path = path.with(room);
}
return this.urlForPath(path);
}
enableGridUrl() {
let path = this._navigation.path.until("session");
const room = this._navigation.path.get("room");
if (room) {
path = path.with(this._navigation.segment("rooms", [room.value]));
path = path.with(room);
} else {
path = path.with(this._navigation.segment("rooms", []));
path = path.with(this._navigation.segment("empty-grid-tile", 0));
}
return this.urlForPath(path);
}
} }

View file

@ -83,16 +83,12 @@ export class RoomGridViewModel extends ViewModel {
if (index === this._selectedIndex) { if (index === this._selectedIndex) {
return; return;
} }
let path = this.navigation.path;
const vm = this._viewModels[index]; const vm = this._viewModels[index];
if (vm) { if (vm) {
path = path.with(this.navigation.segment("room", vm.id)); this.navigation.push("room", vm.id);
} else { } else {
path = path.with(this.navigation.segment("empty-grid-tile", index)); this.navigation.push("empty-grid-tile", index);
} }
let url = this.urlRouter.urlForPath(path);
url = this.urlRouter.applyUrl(url);
this.urlRouter.history.pushUrl(url);
} }
/** called from SessionViewModel */ /** called from SessionViewModel */

View file

@ -76,14 +76,25 @@ export class LeftPanelViewModel extends ViewModel {
} }
toggleGrid() { toggleGrid() {
let url;
if (this.gridEnabled) { if (this.gridEnabled) {
url = this.urlRouter.disableGridUrl(); let path = this.navigation.path.until("session");
const room = this.navigation.path.get("room");
if (room) {
path = path.with(room);
}
this.navigation.applyPath(path);
} else { } else {
url = this.urlRouter.enableGridUrl(); let path = this.navigation.path.until("session");
const room = this.navigation.path.get("room");
if (room) {
path = path.with(this.navigation.segment("rooms", [room.value]));
path = path.with(room);
} else {
path = path.with(this.navigation.segment("rooms", []));
path = path.with(this.navigation.segment("empty-grid-tile", 0));
}
this.navigation.applyPath(path);
} }
url = this.urlRouter.applyUrl(url);
this.urlRouter.history.pushUrl(url);
} }
get roomList() { get roomList() {

View file

@ -118,8 +118,7 @@ export async function main(container, paths, legacyExtras) {
} }
const navigation = createNavigation(); const navigation = createNavigation();
const history = new History(); const urlRouter = createRouter({navigation, history: new History()});
const urlRouter = createRouter({navigation, history});
urlRouter.attach(); urlRouter.attach();
const vm = new RootViewModel({ const vm = new RootViewModel({
@ -143,7 +142,7 @@ export async function main(container, paths, legacyExtras) {
navigation navigation
}); });
window.__brawlViewModel = vm; window.__brawlViewModel = vm;
await vm.load(history.getLastUrl()); await vm.load();
// TODO: replace with platform.createAndMountRootView(vm, container); // TODO: replace with platform.createAndMountRootView(vm, container);
const view = new RootView(vm); const view = new RootView(vm);
container.appendChild(view.mount()); container.appendChild(view.mount());

View file

@ -20,14 +20,9 @@ export class History extends BaseObservableValue {
constructor() { constructor() {
super(); super();
this._boundOnHashChange = null; this._boundOnHashChange = null;
this._expectSetEcho = false;
} }
_onHashChange() { _onHashChange() {
if (this._expectSetEcho) {
this._expectSetEcho = false;
return;
}
this.emit(this.get()); this.emit(this.get());
this._storeHash(this.get()); this._storeHash(this.get());
} }
@ -37,28 +32,19 @@ export class History extends BaseObservableValue {
} }
/** does not emit */ /** does not emit */
replaceUrl(url) { replaceUrlSilently(url) {
window.history.replaceState(null, null, url); window.history.replaceState(null, null, url);
this._storeHash(url); this._storeHash(url);
} }
/** does not emit */ /** does not emit */
pushUrl(url) { pushUrlSilently(url) {
window.history.pushState(null, null, url); window.history.pushState(null, null, url);
this._storeHash(url); this._storeHash(url);
// const hash = this.urlAsPath(url); }
// // important to check before we expect an echo
// // as setting the hash to it's current value doesn't pushUrl(url) {
// // trigger onhashchange document.location.hash = url;
// if (hash === document.location.hash) {
// return;
// }
// // this operation is silent,
// // so avoid emitting on echo hashchange event
// if (this._boundOnHashChange) {
// this._expectSetEcho = true;
// }
// document.location.hash = hash;
} }
urlAsPath(url) { urlAsPath(url) {