From 525edf18eb601a0bb36accb894f47ea2ba43c7fd Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 17 May 2022 14:54:13 +0200 Subject: [PATCH] init router concept --- .../src/auth/login/umb-login.element.ts | 74 ++++---- .../src/core/router/index.ts | 158 ++++++++++++++++++ .../src/installer/installer.element.ts | 19 +++ .../src/mocks/handlers.ts | 11 ++ src/Umbraco.Web.UI.Client/src/umb-app.ts | 72 +++++--- 5 files changed, 275 insertions(+), 59 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/core/router/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/installer/installer.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/auth/login/umb-login.element.ts b/src/Umbraco.Web.UI.Client/src/auth/login/umb-login.element.ts index b223acb143..7e74420f96 100644 --- a/src/Umbraco.Web.UI.Client/src/auth/login/umb-login.element.ts +++ b/src/Umbraco.Web.UI.Client/src/auth/login/umb-login.element.ts @@ -46,7 +46,7 @@ export class UmbLogin extends LitElement { await fetch('/login', { method: 'POST' }); this._loggingIn = false; // TODO: Change to redirect when router has been added. - this.dispatchEvent(new CustomEvent('login', { detail: { username, password, persist } })); + this.dispatchEvent(new CustomEvent('login', { bubbles: true, composed: true, detail: { username, password, persist } })); } catch (error) { console.log(error); this._loggingIn = false; @@ -68,44 +68,46 @@ export class UmbLogin extends LitElement { render() { return html` -
-

${this._greeting}

- -
- - Email - - + +
+

${this._greeting}

+ + + + Email + + - - Password - - + + Password + + - - Remember me - + + Remember me + - - - - -
+ + + +
+
+ `; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/router/index.ts b/src/Umbraco.Web.UI.Client/src/core/router/index.ts new file mode 100644 index 0000000000..e0ef976391 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/router/index.ts @@ -0,0 +1,158 @@ +export interface UmbRoute { + path: string; + elementName: string; + meta?: any; +} + +export interface UmbRouteLocation { + pathname: string; + params: object; + fullPath: string; + route: UmbRoute; +} + +export class UmbRouter { + private _routes: Array = []; + private _host: HTMLElement; + private _outlet: HTMLElement; + private _element: any; + + constructor(host: HTMLElement, outlet: HTMLElement) { + this._host = host; + this._outlet = outlet; + + // Anchor Hijacker + this._host.addEventListener('click', async (event: any) => { + event.preventDefault(); + + const target = event.composedPath()[0]; + const href = target.href; + if (!href) return; + + const url = new URL(href); + const pathname = url.pathname; + + const canLeave = await this._requestLeave(); + if (!canLeave) return; + + this._navigate(pathname); + }); + } + + public setRoutes(routes: Array) { + this._routes = routes; + const pathname = window.location.pathname; + this.push(pathname); + } + + public getRoutes() { + return this._routes; + } + + public go(delta: number) { + history.go(delta); + } + + public back() { + history.back(); + } + + public forward() { + history.forward(); + } + + public push(pathname: string) { + history.pushState(null, '', pathname); + this._navigate(pathname); + } + + private async _requestLeave() { + if (this._element.beforeLeave) { + const res = await this._element.beforeLeave(); + if (!res) return; + } + + const beforeLeaveEvent = new CustomEvent('before-leave', { + bubbles: true, + composed: true, + cancelable: true, + }); + + this._host.dispatchEvent(beforeLeaveEvent); + + if (beforeLeaveEvent.defaultPrevented) return; + + return true; + } + + private async _requestEnter(to: UmbRouteLocation) { + if (this._element.beforeEnter) { + const res = await this._element.beforeEnter(); + if (!res) return; + } + + const beforeEnterEvent = new CustomEvent('before-enter', { + bubbles: true, + composed: true, + cancelable: true, + detail: { to }, + }); + + this._host.dispatchEvent(beforeEnterEvent); + + if (beforeEnterEvent.defaultPrevented) return; + + return true; + } + + private async _navigate(pathname: string) { + const location = this._resolve(pathname); + if (!location) return; + + this._setupElement(location); + + const canEnter = await this._requestEnter(location); + if (!canEnter) return; + + window.history.pushState(null, '', pathname); + this._render(); + } + + private _resolve(pathname: string): UmbRouteLocation | null { + let location: UmbRouteLocation | null = null; + + this._routes.forEach((route) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const pattern = new URLPattern({ pathname: route.path }); + const href = `${window.location.origin}${pathname}`; + const match = pattern.test(href); + + if (match) { + const result = pattern.exec(href); + location = { + pathname: result.pathname.input, + params: result.pathname.groups, + fullPath: result.pathname.input, + route, + }; + } + }); + + return location; + } + + private _setupElement(location: UmbRouteLocation) { + this._element = document.createElement(location.route.elementName); + this._element.location = location; + } + + private async _render() { + const childNodes = this._outlet.childNodes; + childNodes.forEach((node) => { + this._outlet.removeChild(node); + }); + + this._outlet.appendChild(this._element); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.element.ts b/src/Umbraco.Web.UI.Client/src/installer/installer.element.ts new file mode 100644 index 0000000000..c0cc7a1b8f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.element.ts @@ -0,0 +1,19 @@ +import { css, CSSResultGroup, html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +@customElement('umb-installer') +export class UmbInstaller extends LitElement { + static styles: CSSResultGroup = [ + css``, + ]; + + render() { + return html`
Installer
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-installer': UmbInstaller; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts index f9ffdd50be..3422fef735 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts @@ -1,6 +1,17 @@ import { rest } from 'msw'; export const handlers = [ + rest.post('/init', (_req, res, ctx) => { + return res( + // Respond with a 200 status code + ctx.status(200), + ctx.json({ + version: 'x.x.x', + installed: true + }), + ) + }), + rest.post('/login', (_req, res, ctx) => { // Persist user's authentication in the session sessionStorage.setItem('is-authenticated', 'true'); diff --git a/src/Umbraco.Web.UI.Client/src/umb-app.ts b/src/Umbraco.Web.UI.Client/src/umb-app.ts index ffeb87f758..ee9a9ab72b 100644 --- a/src/Umbraco.Web.UI.Client/src/umb-app.ts +++ b/src/Umbraco.Web.UI.Client/src/umb-app.ts @@ -1,6 +1,8 @@ +import './installer/installer.element'; import './auth/login/umb-login.element'; import './auth/umb-auth-layout.element'; import './backoffice/umb-backoffice.element'; + import '@umbraco-ui/uui'; import '@umbraco-ui/uui-css/dist/uui-css.css'; @@ -8,6 +10,22 @@ import { css, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { worker } from './mocks/browser'; +import { UmbRouter, UmbRoute } from './core/router'; + +const routes: Array = [ + { + path: '/login', + elementName: 'umb-login' + }, + { + path: '/install', + elementName: 'umb-installer' + }, + { + path: '/content', + elementName: 'umb-backoffice' + }, +]; // Import somewhere else? @customElement('umb-app') @@ -22,8 +40,7 @@ export class UmbApp extends LitElement { @state() _authorized = false; - @state() - _user: any; + _router?: UmbRouter; constructor() { super(); @@ -31,34 +48,43 @@ export class UmbApp extends LitElement { this._authorized = sessionStorage.getItem('is-authenticated') === 'true'; } - private async _getUser() { + connectedCallback(): void { + super.connectedCallback(); + // TODO: remove when router can be injected into login element + this.addEventListener('login', () => { + this._router?.push('/content'); + }); + } + + protected async firstUpdated(): Promise { + const outlet = this.shadowRoot?.getElementById('outlet'); + if (!outlet) return; + + this._router = new UmbRouter(this, outlet); + this._router.setRoutes(routes); + try { - const res = await fetch('/user'); - this._user = await res.json(); + const res = await fetch('/init', { method: 'POST' }); + const data = await res.json(); + + if (!data.installed) { + this._router.push('/install'); + return; + } + + if (!this._authorized) { + this._router.push('/login'); + } else { + this._router.push('/content'); + } + } catch (error) { console.log(error); } } - private _handleLogin = () => { - this._authorized = true; - this._getUser(); - }; - - private _renderBackoffice = () => html``; - - private _renderAuth = () => html` - - - - `; - render() { - return html` - - ${this._authorized ? this._renderBackoffice() : this._renderAuth()} - - `; + return html`
`; } }