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;
}
async load(lastUrlHash) {
async load() {
this.track(this.navigation.observe("login").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 sessionId = this.navigation.observe("session").get();
if (isLogin) {
@ -58,30 +58,24 @@ export class RootViewModel extends ViewModel {
}
} else {
try {
let url = restoreHashIfAtDefault;
if (!url) {
// redirect depending on what sessions are already present
if (restoreUrlIfAtDefault) {
this.urlRouter.pushUrl(restoreUrlIfAtDefault);
} else {
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) {
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() {
this._setSection(() => {
this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions({
@ -102,10 +96,8 @@ export class RootViewModel extends ViewModel {
defaultHomeServer: "https://matrix.org",
createSessionContainer: this._createSessionContainer,
ready: sessionContainer => {
const url = this.urlRouter.urlForSegment("session", sessionContainer.sessionId);
this.urlRouter.applyUrl(url);
this.urlRouter.history.replaceUrl(url);
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.
*/
import {BaseObservableValue} from "../../observable/ObservableValue.js";
import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue.js";
export class Navigation {
constructor(allowsChild) {
this._allowsChild = allowsChild;
this._path = new Path([], allowsChild);
this._observables = new Map();
this._pathObservable = new ObservableValue(this._path);
}
get pathObservable() {
return this._pathObservable;
}
get path() {
return this._path;
}
push(type, value = undefined) {
return this.applyPath(this.path.with(new Segment(type, value)));
}
applyPath(path) {
// Path is not exported, so you can only create a Path through Navigation,
// so we assume it respects the allowsChild rules
@ -45,6 +54,10 @@ export class Navigation {
const observable = this._observables.get(segment.type);
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) {

View File

@ -14,40 +14,50 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {Segment} from "./Navigation.js";
export class URLRouter {
constructor({history, navigation, parseUrlPath, stringifyPath}) {
this._subscription = null;
this._history = history;
this._navigation = navigation;
this._parseUrlPath = parseUrlPath;
this._stringifyPath = stringifyPath;
this._subscription = null;
this._pathSubscription = null;
}
attach() {
this._subscription = this._history.subscribe(url => {
const redirectedUrl = this.applyUrl(url);
const redirectedUrl = this._applyUrl(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() {
this._subscription = this._subscription();
this._pathSubscription = this._pathSubscription();
}
applyUrl(url) {
_applyUrl(url) {
const urlPath = this._history.urlAsPath(url)
const navPath = this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path));
this._navigation.applyPath(navPath);
return this._history.pathAsUrl(this._stringifyPath(navPath));
}
get history() {
return this._history;
pushUrl(url) {
this._history.pushUrl(url);
}
getLastUrl() {
return this._history.getLastUrl();
}
urlForSegments(segments) {
@ -70,7 +80,7 @@ export class URLRouter {
}
urlForPath(path) {
return this.history.pathAsUrl(this._stringifyPath(path));
return this._history.pathAsUrl(this._stringifyPath(path));
}
openRoomActionUrl(roomId) {
@ -78,26 +88,4 @@ export class URLRouter {
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
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) {
return;
}
let path = this.navigation.path;
const vm = this._viewModels[index];
if (vm) {
path = path.with(this.navigation.segment("room", vm.id));
this.navigation.push("room", vm.id);
} 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 */

View File

@ -76,14 +76,25 @@ export class LeftPanelViewModel extends ViewModel {
}
toggleGrid() {
let url;
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 {
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() {

View File

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

View File

@ -20,14 +20,9 @@ export class History extends BaseObservableValue {
constructor() {
super();
this._boundOnHashChange = null;
this._expectSetEcho = false;
}
_onHashChange() {
if (this._expectSetEcho) {
this._expectSetEcho = false;
return;
}
this.emit(this.get());
this._storeHash(this.get());
}
@ -37,28 +32,19 @@ export class History extends BaseObservableValue {
}
/** does not emit */
replaceUrl(url) {
replaceUrlSilently(url) {
window.history.replaceState(null, null, url);
this._storeHash(url);
}
/** does not emit */
pushUrl(url) {
pushUrlSilently(url) {
window.history.pushState(null, null, 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
// // trigger onhashchange
// 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;
}
pushUrl(url) {
document.location.hash = url;
}
urlAsPath(url) {