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}
-
-
+
+
+
`;
}
}
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``;
}
}