render section element when section changes

This commit is contained in:
Mads Rasmussen
2022-05-24 22:17:09 +02:00
parent 58eb1ad284
commit d9ee7081c5
6 changed files with 208 additions and 70 deletions

View File

@@ -1,31 +1,36 @@
import '@umbraco-ui/uui';
import '@umbraco-ui/uui-css/dist/uui-css.css';
// TODO: lazy load these
import './installer/installer.element';
import './auth/login/login.element';
import './auth/auth-layout.element';
import './backoffice/backoffice.element';
import './installer/installer.element';
import { UmbSectionContext } from './section.context';
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { getInitStatus } from './api/fetcher';
import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router';
import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouteLocation, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router';
import { UmbContextProvideMixin } from './core/context';
import { Subscription } from 'rxjs';
const routes: Array<UmbRoute> = [
{
path: '/login',
elementName: 'umb-login',
alias: 'login',
meta: { requiresAuth: false },
},
{
path: '/install',
elementName: 'umb-installer',
alias: 'install',
meta: { requiresAuth: false },
},
{
path: '/section/:section',
elementName: 'umb-backoffice',
alias: 'app',
meta: { requiresAuth: true },
},
];
@@ -42,7 +47,11 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) {
}
`;
_router?: UmbRouter;
private _isInstalled = false;
private _view?: HTMLElement;
private _router?: UmbRouter;
private _locationSubscription?: Subscription;
constructor() {
super();
@@ -51,7 +60,10 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) {
connectedCallback(): void {
super.connectedCallback();
const { extensionRegistry } = window.Umbraco;
this.provide('umbExtensionRegistry', window.Umbraco.extensionRegistry);
this.provide('umbSectionContext', new UmbSectionContext(extensionRegistry));
}
private _onBeforeEnter = (event: Event) => {
@@ -71,10 +83,7 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) {
}
protected async firstUpdated(): Promise<void> {
const outlet = this.shadowRoot?.getElementById('outlet');
if (!outlet) return;
this._router = new UmbRouter(this, outlet);
this._router = new UmbRouter(this);
this._router.setRoutes(routes);
// TODO: find a solution for magic strings
@@ -83,27 +92,59 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) {
try {
const { data } = await getInitStatus({});
if (!data.installed) {
this._isInstalled = data.installed;
if (!this._isInstalled) {
this._router.push('/install');
return;
}
if (!this._isAuthorized()) {
if (!this._isAuthorized() || window.location.pathname === '/install') {
this._router.push('/login');
} else {
const next = window.location.pathname === '/' ? '/section/content' : window.location.pathname;
const next = window.location.pathname === '/' ? '/section/Content' : window.location.pathname;
this._router.push(next);
}
this._useLocation();
} catch (error) {
console.log(error);
}
}
private _useLocation () {
this._locationSubscription?.unsubscribe();
this._locationSubscription = this._router?.location
.subscribe((location: UmbRouteLocation) => {
if (location.route.alias === 'login') {
this._renderView('umb-login');
return;
}
if (location.route.alias === 'install') {
this._renderView('umb-installer');
return;
}
this._renderView('umb-backoffice');
});
}
_renderView (view: string) {
if (this._view?.tagName === view.toUpperCase()) return;
this._view = document.createElement(view);
this.requestUpdate();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._locationSubscription?.unsubscribe();
}
render() {
return html`
<div id="outlet"></div>
`;
return html`${this._view}`;
}
}

View File

@@ -71,7 +71,7 @@ export class UmbLogin extends UmbContextInjectMixin(LitElement) {
await postUserLogin({ username, password, persist });
this._loggingIn = false;
// TODO: how do we know where to go?
this._router?.push('/section/content');
this._router?.push('/section/Content');
} catch (error) {
console.log(error);
this._loggingIn = false;

View File

@@ -1,13 +1,14 @@
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import { getUserSections } from '../api/fetcher';
import { UmbContextInjectMixin } from '../core/context';
import { UmbExtensionManifest, UmbExtensionRegistry, UmbManifestSectionMeta } from '../core/extension';
import { UmbRouter } from '../core/router';
import { UmbExtensionManifest, UmbManifestSectionMeta } from '../core/extension';
import { UmbRouteLocation, UmbRouter } from '../core/router';
import { UmbSectionContext } from '../section.context';
// TODO: umb or not umb in file name?
@@ -81,9 +82,6 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) {
@state()
private _open = false;
@state()
private _availableSections: Array<UmbExtensionManifest<UmbManifestSectionMeta>> = [];
@state()
private _allowedSection: Array<string> = [];
@@ -97,11 +95,14 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) {
private _extraSections: Array<UmbExtensionManifest<UmbManifestSectionMeta>> = [];
@state()
private _activeSection = '';
private _currentSectionAlias = '';
private _router?: UmbRouter;
private _extensionRegistry?: UmbExtensionRegistry;
private _subscription?: Subscription;
private _sectionContext?: UmbSectionContext;
private _sectionSubscription?: Subscription;
private _currentSectionSubscription?: Subscription;
private _locationSubscription?: Subscription;
private _location? : UmbRouteLocation;
private _handleMore(e: MouseEvent) {
e.stopPropagation();
@@ -118,12 +119,14 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) {
// TODO: this could maybe be handled by an anchor tag
this._router?.push(`/section/${section.name}`);
this._activeSection = section.alias;
this._sectionContext?.setCurrent(section.alias);
}
private _handleLabelClick(e: PointerEvent) {
const label = (e.target as any).label;
this._activeSection = label;
// TODO: set current section
//this._sectionContext?.setCurrent(section.alias);
const moreTab = this.shadowRoot?.getElementById('moreTab');
moreTab?.setAttribute('active', 'true');
@@ -135,40 +138,64 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) {
super.connectedCallback();
this.requestContext('umbRouter');
this.requestContext('umbExtensionRegistry');
this.requestContext('umbSectionContext');
}
contextInjected(contexts: Map<string, any>): void {
if (contexts.has('umbExtensionRegistry')) {
this._extensionRegistry = contexts.get('umbExtensionRegistry');
this._useSections();
}
if (contexts.has('umbRouter')) {
this._router = contexts.get('umbRouter');
this._useLocation();
}
if (contexts.has('umbSectionContext')) {
this._sectionContext = contexts.get('umbSectionContext');
this._useCurrentSection();
this._useSections();
}
}
private _useLocation () {
this._locationSubscription?.unsubscribe();
this._locationSubscription = this._router?.location
.subscribe((location: UmbRouteLocation) => {
this._location = location;
});
}
private _useCurrentSection () {
this._currentSectionSubscription?.unsubscribe();
this._currentSectionSubscription = this._sectionContext?.getCurrent()
.subscribe(section => {
this._currentSectionAlias = section.alias;
});
}
private async _useSections() {
this._subscription?.unsubscribe();
this._sectionSubscription?.unsubscribe();
const { data } = await getUserSections({});
this._allowedSection = data.sections;
this._subscription = this._extensionRegistry?.extensions
.pipe(
map((extensions: Array<UmbExtensionManifest<unknown>>) =>
extensions.filter(extension => extension.type === 'section')
))
this._sectionSubscription = this._sectionContext?.getSections()
.subscribe((sectionExtensions: any) => {
this._availableSections = [...sectionExtensions];
this._sections = this._availableSections.filter((section) => this._allowedSection.includes(section.alias));
// TODO: implement resize observer
this._sections = sectionExtensions.filter((section: any) => this._allowedSection.includes(section.alias));
this._visibleSections = this._sections;
this._activeSection = this._visibleSections?.[0].alias;
const currentSectionAlias = this._sections.find(section => section.name === this._location?.params?.section)?.alias;
if (!currentSectionAlias) return;
this._sectionContext?.setCurrent(currentSectionAlias);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._locationSubscription?.unsubscribe();
this._sectionSubscription?.unsubscribe();
this._currentSectionSubscription?.unsubscribe();
}
private _renderExtraSections() {
return when(
this._extraSections.length > 0,
@@ -183,7 +210,7 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) {
${this._extraSections.map(
(section) => html`
<uui-menu-item
?active="${this._activeSection === section.alias}"
?active="${this._currentSectionAlias === section.alias}"
label="${section.name}"
@click-label="${this._handleLabelClick}"></uui-menu-item>
`
@@ -207,7 +234,7 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) {
${this._visibleSections.map(
(section) => html`
<uui-tab
?active="${this._activeSection === section.alias}"
?active="${this._currentSectionAlias === section.alias}"
label="${section.name}"
@click="${(e: PointerEvent) => this._handleTabClick(e, section)}"></uui-tab>
`

View File

@@ -1,9 +1,20 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { UmbContextInjectMixin } from '../core/context';
import { UmbSectionContext } from '../section.context';
import { UmbExtensionManifest, UmbManifestSectionMeta } from '../core/extension';
// TODO: lazy load these. How to we handle dynamic import of our typescript file?
import '../content/content-section.element';
import '../media/media-section.element';
@defineElement('umb-backoffice-main')
export class UmbBackofficeMain extends LitElement {
export class UmbBackofficeMain extends UmbContextInjectMixin(LitElement) {
static styles = [
UUITextStyles,
css`
@@ -70,8 +81,56 @@ export class UmbBackofficeMain extends LitElement {
`,
];
@state()
private _sectionElement?: HTMLElement;
private _sectionContext?: UmbSectionContext;
private _currentSectionSubscription?: Subscription;
connectedCallback(): void {
super.connectedCallback();
this.requestContext('umbRouter');
this.requestContext('umbSectionContext');
}
contextInjected(contexts: Map<string, any>): void {
if (contexts.has('umbSectionContext')) {
this._sectionContext = contexts.get('umbSectionContext');
this._useCurrentSection();
}
}
private _useCurrentSection () {
this._currentSectionSubscription?.unsubscribe();
this._currentSectionSubscription = this._sectionContext?.getCurrent()
.subscribe(section => {
this._createSectionElement(section);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._currentSectionSubscription?.unsubscribe();
}
private async _createSectionElement (section: UmbExtensionManifest<UmbManifestSectionMeta>) {
if (!section) return;
// TODO: How do we handle dynamic imports of our files?
if (section.js) {
await import(/* @vite-ignore */section.js);
}
if (section.elementName) {
this._sectionElement = document.createElement(section.elementName);
}
}
render() {
return html`
${ this._sectionElement }
<!--
<div id="editor">
<div id="editor-top">
<uui-input value="Home"></uui-input>
@@ -88,6 +147,7 @@ export class UmbBackofficeMain extends LitElement {
<uui-button look="primary" color="positive">Save and publish</uui-button>
</div>
</div>
-->
`;
}
}

View File

@@ -4,7 +4,7 @@ import { UmbRouterBeforeLeaveEvent } from './router-before-leave.event';
export interface UmbRoute {
path: string;
elementName: string;
alias: string;
meta?: any;
}
@@ -24,15 +24,13 @@ export interface UmbRouteElement extends HTMLElement {
export class UmbRouter {
private _routes: Array<UmbRoute> = [];
private _host: HTMLElement;
private _outlet: HTMLElement;
private _element?: UmbRouteElement;
private _location: ReplaySubject<UmbRouteLocation> = new ReplaySubject(1);
public readonly location: Observable<UmbRouteLocation> = this._location.asObservable();
constructor(host: HTMLElement, outlet: HTMLElement) {
constructor(host: HTMLElement) {
this._host = host;
this._outlet = outlet;
// Anchor Hijacker
this._host.addEventListener('click', async (event: any) => {
@@ -110,15 +108,12 @@ export class UmbRouter {
const canLeave = await this._requestLeave(location);
if (!canLeave) return;
this._setupElement(location);
const canEnter = await this._requestEnter(location);
if (!canEnter) return;
window.history.pushState(null, '', pathname);
this._location.next(location);
this._render();
}
private _resolve(pathname: string): UmbRouteLocation | null {
@@ -144,20 +139,4 @@ export class UmbRouter {
return location;
}
private _setupElement(location: UmbRouteLocation) {
this._element = document.createElement(location.route.elementName);
this._element.location = location;
}
private async _render() {
if (!this._element) return;
const childNodes = this._outlet.childNodes;
childNodes.forEach((node) => {
this._outlet.removeChild(node);
});
this._outlet.appendChild(this._element);
}
}

View File

@@ -0,0 +1,31 @@
import { firstValueFrom, map, Observable, ReplaySubject } from 'rxjs';
import { UmbExtensionManifest, UmbExtensionRegistry, UmbManifestSectionMeta } from './core/extension';
export class UmbSectionContext {
private _extensionRegistry!: UmbExtensionRegistry;
private _current: ReplaySubject<UmbExtensionManifest<UmbManifestSectionMeta>> = new ReplaySubject(1);
public readonly current: Observable<UmbExtensionManifest<UmbManifestSectionMeta>> = this._current.asObservable();
constructor(_extensionRegistry: UmbExtensionRegistry) {
this._extensionRegistry = _extensionRegistry;
}
getSections () {
return this._extensionRegistry.extensions
.pipe(
map((extensions: Array<UmbExtensionManifest<unknown>>) => extensions.filter(extension => extension.type === 'section'))
);
}
getCurrent () {
return this.current;
}
async setCurrent (sectionAlias: string) {
const sections = await firstValueFrom(this.getSections());
const matchedSection = sections.find(section => section.alias === sectionAlias) as UmbExtensionManifest<UmbManifestSectionMeta>;
this._current.next(matchedSection);
}
}