Refactor LiteElement into reactive controllers (#1423)

- Copies navigation and notification utility methods into separate
controllers
- Adds deprecation notice to `LitElement` methods
- Default type import start to inline
This commit is contained in:
sua yoo 2023-12-05 15:30:10 -08:00 committed by GitHub
parent a1e42b0cf3
commit 901f1435d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 163 additions and 92 deletions

View File

@ -29,7 +29,12 @@ module.exports = {
destructuredArrayIgnorePattern: "^_",
},
],
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/no-explicit-any": "warn",
},

View File

@ -0,0 +1,75 @@
import type { ReactiveController, ReactiveControllerHost } from "lit";
import appState from "@/utils/state";
const NAVIGATE_EVENT_NAME = "navigate";
export interface NavigateEvent extends CustomEvent {
detail: {
url: string;
state?: object;
};
}
/**
* Manage app navigation
*/
export class NavigateController implements ReactiveController {
private host: ReactiveControllerHost & EventTarget;
get orgBasePath() {
const slug = appState.orgSlug;
if (slug) {
return `/orgs/${slug}`;
}
return "/";
}
constructor(host: NavigateController["host"]) {
this.host = host;
host.addController(this);
}
hostConnected() {}
hostDisconnected() {}
to(url: string, state?: object): void {
const evt: NavigateEvent = new CustomEvent(NAVIGATE_EVENT_NAME, {
detail: { url, state },
bubbles: true,
composed: true,
});
this.host.dispatchEvent(evt);
}
/**
* Bind to anchor tag to prevent full page navigation
* @example
* ```ts
* <a href="/" @click=${this.navigate.link}>go</a>
* ```
* @param event Click event
*/
link(event: MouseEvent, _href?: string): void {
if (
// Detect keypress for opening in a new tab
event.ctrlKey ||
event.shiftKey ||
event.metaKey ||
(event.button && event.button == 1) ||
// Account for event prevented on anchor tag
event.defaultPrevented
) {
return;
}
event.preventDefault();
const evt: NavigateEvent = new CustomEvent(NAVIGATE_EVENT_NAME, {
detail: { url: (event.currentTarget as HTMLAnchorElement).href },
bubbles: true,
composed: true,
});
this.host.dispatchEvent(evt);
}
}

View File

@ -0,0 +1,60 @@
import type {
ReactiveController,
ReactiveControllerHost,
TemplateResult,
} from "lit";
export interface NotifyEvent extends CustomEvent {
detail: {
/**
* Notification message body.
* Example:
* ```ts
* message: html`<strong>Look!</strong>`
* ```
*
* Note: In order for `this` methods to work, you'll
* need to bind `this` or use a fat arrow function.
* For example:
* ```ts
* message: html`<button @click=${this.onClick.bind(this)}>Go!</button>`
* ```
* Or:
* ```ts
* message: html`<button @click=${(e) => this.onClick(e)}>Go!</button>`
* ```
**/
message: string | TemplateResult;
/** Notification title */
title?: string;
/** Shoelace icon name */
icon?: string;
variant?: "success" | "warning" | "danger" | "primary" | "info";
duration?: number;
};
}
/**
* Manage global app notifications
*/
export class NotifyController implements ReactiveController {
private host: ReactiveControllerHost & EventTarget;
constructor(host: NotifyController["host"]) {
this.host = host;
host.addController(this);
}
hostConnected() {}
hostDisconnected() {}
toast(detail: NotifyEvent["detail"]) {
this.host.dispatchEvent(
new CustomEvent("notify", {
bubbles: true,
composed: true,
detail,
})
);
}
}

View File

@ -25,7 +25,7 @@ import queryString from "query-string";
import { RelativeDuration } from "@/components/ui/relative-duration";
import type { Crawl } from "@/types/crawler";
import { srOnly, truncate } from "@/utils/css";
import type { NavigateEvent } from "@/utils/LiteElement";
import type { NavigateEvent } from "@/controllers/navigate";
import type { OverflowDropdown } from "@/components/ui/overflow-dropdown";
const mediumBreakpointCss = css`30rem`;

View File

@ -24,7 +24,7 @@ import { msg, localized, str } from "@lit/localize";
import { RelativeDuration } from "@/components/ui/relative-duration";
import type { ListWorkflow } from "@/types/crawler";
import { srOnly, truncate } from "@/utils/css";
import type { NavigateEvent } from "@/utils/LiteElement";
import type { NavigateEvent } from "@/controllers/navigate";
import { humanizeSchedule } from "@/utils/cron";
import { numberFormatter } from "@/utils/number";
import type { OverflowDropdown } from "@/components/ui/overflow-dropdown";

View File

@ -11,7 +11,8 @@ import "tailwindcss/tailwind.css";
import "./utils/polyfills";
import appState, { use, AppStateService } from "./utils/state";
import type { OrgTab } from "./pages/org";
import type { NotifyEvent, NavigateEvent } from "./utils/LiteElement";
import type { NavigateEvent } from "@/controllers/navigate";
import type { NotifyEvent } from "@/controllers/notify";
import LiteElement, { html } from "./utils/LiteElement";
import APIRouter from "./utils/APIRouter";
import AuthService from "./utils/AuthService";

View File

@ -1,123 +1,53 @@
import { LitElement, html } from "lit";
import type { TemplateResult } from "lit";
import { APIController } from "@/controllers/api";
import { NotifyController } from "@/controllers/notify";
import { NavigateController } from "@/controllers/navigate";
import appState, { use } from "./state";
export interface NavigateEvent extends CustomEvent {
detail: {
url: string;
state?: object;
};
}
export interface NotifyEvent extends CustomEvent {
detail: {
/**
* Notification message body.
* Example:
* ```ts
* message: html`<strong>Look!</strong>`
* ```
*
* Note: In order for `this` methods to work, you'll
* need to bind `this` or use a fat arrow function.
* For example:
* ```ts
* message: html`<button @click=${this.onClick.bind(this)}>Go!</button>`
* ```
* Or:
* ```ts
* message: html`<button @click=${(e) => this.onClick(e)}>Go!</button>`
* ```
**/
message: string | TemplateResult;
/** Notification title */
title?: string;
/** Shoelace icon name */
icon?: string;
variant?: "success" | "warning" | "danger" | "primary" | "info";
duration?: number;
};
}
export { html };
export default class LiteElement extends LitElement {
@use()
appState = appState;
private api = new APIController(this);
private apiController = new APIController(this);
private notifyController = new NotifyController(this);
private navigateController = new NavigateController(this);
protected get orgBasePath() {
const slug = this.appState.orgSlug;
if (slug) {
return `/orgs/${slug}`;
}
return "/";
return this.navigateController.orgBasePath;
}
createRenderRoot() {
return this;
}
navTo(url: string, state?: object): void {
const evt: NavigateEvent = new CustomEvent("navigate", {
detail: { url, state },
bubbles: true,
composed: true,
});
this.dispatchEvent(evt);
/**
* @deprecated New components should use NavigateController directly
*/
navTo(...args: Parameters<NavigateController["to"]>) {
return this.navigateController.to(...args);
}
/**
* Bind to anchor tag to prevent full page navigation
* @example
* ```ts
* <a href="/" @click=${this.navLink}>go</a>
* ```
* @param event Click event
* @deprecated New components should use NavigateController directly
*/
navLink(event: MouseEvent, _href?: string): void {
if (
// Detect keypress for opening in a new tab
event.ctrlKey ||
event.shiftKey ||
event.metaKey ||
(event.button && event.button == 1) ||
// Account for event prevented on anchor tag
event.defaultPrevented
) {
return;
}
event.preventDefault();
const evt: NavigateEvent = new CustomEvent("navigate", {
detail: { url: (event.currentTarget as HTMLAnchorElement).href },
bubbles: true,
composed: true,
});
this.dispatchEvent(evt);
navLink(...args: Parameters<NavigateController["link"]>) {
return this.navigateController.link(...args);
}
/**
* Emit global notification
* @deprecated New components should use NotifyController directly
*/
notify(detail: NotifyEvent["detail"]) {
this.dispatchEvent(
new CustomEvent("notify", {
bubbles: true,
composed: true,
detail,
})
);
notify(...args: Parameters<NotifyController["toast"]>) {
return this.notifyController.toast(...args);
}
/**
* @deprecated New components should use APIController directly
*/
async apiFetch<T = unknown>(...args: Parameters<APIController["fetch"]>) {
return this.api.fetch<T>(...args);
return this.apiController.fetch<T>(...args);
}
}