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:
		
							parent
							
								
									a1e42b0cf3
								
							
						
					
					
						commit
						901f1435d7
					
				| @ -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", | ||||
|   }, | ||||
|  | ||||
							
								
								
									
										75
									
								
								frontend/src/controllers/navigate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								frontend/src/controllers/navigate.ts
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										60
									
								
								frontend/src/controllers/notify.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								frontend/src/controllers/notify.ts
									
									
									
									
									
										Normal 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, | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @ -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`; | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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); | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user