From 5508e39dcbfdf7aec805fe7b0209568dd70e5d52 Mon Sep 17 00:00:00 2001 From: Martin PAUCOT Date: Wed, 25 Sep 2024 18:48:16 +0200 Subject: [PATCH] hook system --- package.json | 5 ++- providers/edgewire_provider.ts | 17 +++++++++ src/component.ts | 5 ++- src/component_hook.ts | 17 +++++++++ src/component_hook_registry.ts | 15 ++++++++ src/edgewire.ts | 9 +++++ src/features/lifecycle/component_hook.ts | 18 +++++++++ .../lifecycle/mixins/lifecycle_hooks.ts | 37 +++++++++++++++++++ src/features/validation/component_hook.ts | 4 ++ src/features/validation/handles_validation.ts | 5 +++ src/handle_components.ts | 29 +++++++++++---- src/mixins/with_lifecycle_hooks.ts | 0 src/view_context.ts | 1 + 13 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 src/component_hook.ts create mode 100644 src/component_hook_registry.ts create mode 100644 src/features/lifecycle/component_hook.ts create mode 100644 src/features/lifecycle/mixins/lifecycle_hooks.ts create mode 100644 src/features/validation/component_hook.ts create mode 100644 src/features/validation/handles_validation.ts create mode 100644 src/mixins/with_lifecycle_hooks.ts create mode 100644 src/view_context.ts diff --git a/package.json b/package.json index 8c8f034..f82ebc4 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@adonisjs/assembler": "^7.7.0", "@adonisjs/core": "^6.12.0", - "@adonisjs/eslint-config": "^1.3.0", + "@adonisjs/eslint-config": "^2.0.0-beta.7", "@adonisjs/prettier-config": "^1.3.0", "@adonisjs/tsconfig": "^1.3.0", "@japa/api-client": "^2.0.3", @@ -58,7 +58,7 @@ "copyfiles": "^2.4.1", "del-cli": "^5.1.0", "edge.js": "^6.0.2", - "eslint": "^8.57.0", + "eslint": "^9.9.0", "html-entities": "^2.5.2", "np": "^10.0.6", "prettier": "^3.3.2", @@ -93,6 +93,7 @@ "prettier": "@adonisjs/prettier-config", "dependencies": { "@adonisjs/shield": "^8.1.1", + "@poppinss/hooks": "^7.2.4", "edge-error": "^4.0.1", "edge-parser": "^9.0.2", "lodash": "^4.17.21", diff --git a/providers/edgewire_provider.ts b/providers/edgewire_provider.ts index d09bc99..c06c8d8 100644 --- a/providers/edgewire_provider.ts +++ b/providers/edgewire_provider.ts @@ -3,6 +3,10 @@ import { edgewireTag } from '../src/edge/tags/edgewire.js' import { ApplicationService } from '@adonisjs/core/types' import { ComponentRegistry } from '../src/component_registry.js' import { edgewireScriptsTag } from '../src/edge/tags/edgewire_scripts.js' +import { Edgewire } from '../src/edgewire.js' +import { LifecycleComponentHook } from '../src/features/lifecycle/component_hook.js' +import { ComponentHookRegistry } from '../src/component_hook_registry.js' +import { ValidationComponentHook } from '../src/features/validation/component_hook.js' export default class EdgewireProvider { constructor(protected app: ApplicationService) {} @@ -14,10 +18,23 @@ export default class EdgewireProvider { this.app.container.singleton(ComponentRegistry, () => { return new ComponentRegistry() }) + + this.app.container.singleton(ComponentHookRegistry, () => { + return new ComponentHookRegistry() + }) } async boot() { await import('../src/extensions.js') + + const router = await this.app.container.make('router') + const edgewire = await this.app.container.make(Edgewire) + + router.post('/edgewire/update', (ctx) => edgewire.handleUpdate(ctx)).as('edgewire') + + for (const hook of [LifecycleComponentHook, ValidationComponentHook]) { + edgewire.componentHook(hook) + } } async start() {} diff --git a/src/component.ts b/src/component.ts index 21569a8..2710b24 100644 --- a/src/component.ts +++ b/src/component.ts @@ -2,10 +2,11 @@ import { HttpContext } from '@adonisjs/core/http' import { View } from './view.js' import { compose } from '@adonisjs/core/helpers' import { WithAttributes } from './mixins/with_attributes.js' +import { LifecycleHooks } from './features/lifecycle/mixins/lifecycle_hooks.js' class BaseComponent {} -export abstract class Component extends compose(BaseComponent, WithAttributes) { +export abstract class Component extends compose(BaseComponent, LifecycleHooks, WithAttributes) { #id: string #name: string #ctx: HttpContext @@ -18,6 +19,8 @@ export abstract class Component extends compose(BaseComponent, WithAttributes) { } render?(): Promise + + boot?(): void mount?(args: Record): void public get id() { diff --git a/src/component_hook.ts b/src/component_hook.ts new file mode 100644 index 0000000..5765565 --- /dev/null +++ b/src/component_hook.ts @@ -0,0 +1,17 @@ +import Hooks from '@poppinss/hooks' +import { Component } from './component.js' +import { View } from './view.js' +import { ViewContext } from './view_context.js' + +export type ComponentHookEvents = { + boot: [[Component], []] + mount: [[Component, any], [string]] + hydrate: [[Component], []] + update: [[Component, string, string, any], []] + call: [[Component, string, any[], boolean], []] + exception: [[Component, unknown, boolean], []] + render: [[Component, View, any], [string, (html: string) => any, ViewContext]] + dehydrate: [[Component], []] +} + +export type ComponentHook = (hooks: Hooks) => void diff --git a/src/component_hook_registry.ts b/src/component_hook_registry.ts new file mode 100644 index 0000000..be113ad --- /dev/null +++ b/src/component_hook_registry.ts @@ -0,0 +1,15 @@ +import { ComponentHook, ComponentHookEvents } from './component_hook.js' +import Hooks from '@poppinss/hooks' + +export class ComponentHookRegistry { + components: ComponentHook[] = [] + hooks: Hooks + + constructor() { + this.hooks = new Hooks() + } + + async register(Hook: ComponentHook) { + Hook(this.hooks) + } +} diff --git a/src/edgewire.ts b/src/edgewire.ts index 1702d4e..da27074 100644 --- a/src/edgewire.ts +++ b/src/edgewire.ts @@ -4,19 +4,24 @@ import { HandleComponents } from './handle_components.js' import { HandleRequests } from './handle_requests.js' import { HttpContext } from '@adonisjs/core/http' import { View } from './view.js' +import { ComponentHook } from './component_hook.js' +import { ComponentHookRegistry } from './component_hook_registry.js' @inject() export class Edgewire { #componentRegistry: ComponentRegistry + #componentHookRegistry: ComponentHookRegistry #handleComponents: HandleComponents #handleRequests: HandleRequests constructor( componentRegistry: ComponentRegistry, + componentHookRegistry: ComponentHookRegistry, handleComponents: HandleComponents, handleRequests: HandleRequests ) { this.#componentRegistry = componentRegistry + this.#componentHookRegistry = componentHookRegistry this.#handleComponents = handleComponents this.#handleRequests = handleRequests } @@ -36,4 +41,8 @@ export class Edgewire { view(templatePath: string, state: Record = {}) { return View.template(templatePath, state) } + + componentHook(hook: ComponentHook) { + this.#componentHookRegistry.register(hook) + } } diff --git a/src/features/lifecycle/component_hook.ts b/src/features/lifecycle/component_hook.ts new file mode 100644 index 0000000..2e6e4bc --- /dev/null +++ b/src/features/lifecycle/component_hook.ts @@ -0,0 +1,18 @@ +import Hooks from '@poppinss/hooks' +import { ComponentHookEvents } from '../../component_hook.js' + +export const LifecycleComponentHook = (hooks: Hooks) => { + hooks.add('boot', async (component) => { + await component.hooks.runner('boot').run() + await component.hooks.runner('initialize').run() + await component.hooks.runner('mount').run() + await component.hooks.runner('booted').run() + }) + + hooks.add('render', async (component, view, data) => { + await component.hooks.runner('rendering').run(view, data) + return async (html) => { + await component.hooks.runner('rendered').run(view, html) + } + }) +} diff --git a/src/features/lifecycle/mixins/lifecycle_hooks.ts b/src/features/lifecycle/mixins/lifecycle_hooks.ts new file mode 100644 index 0000000..9ff2a30 --- /dev/null +++ b/src/features/lifecycle/mixins/lifecycle_hooks.ts @@ -0,0 +1,37 @@ +import Hooks from '@poppinss/hooks' +import { View } from '../../../view.js' + +type Events = { + boot: [[], []] + initialize: [[], []] + mount: [[], []] + hydrate: [[], []] + exception: [[unknown, boolean], []] + rendering: [[View, any], []] + rendered: [[View, string], []] + dehydrate: [[], []] + booted: [[], []] +} + +type Constructor = new (...args: any[]) => { + mount?(...args: any[]): void +} + +export function LifecycleHooks(superclass: T) { + return class LifecycleHooksImpl extends superclass { + #hooks: Hooks + + constructor(...args: any[]) { + super(...args) + this.#hooks = new Hooks() + + if (this.mount) { + this.#hooks.add('mount', this.mount) + } + } + + public get hooks(): Hooks { + return this.#hooks + } + } +} diff --git a/src/features/validation/component_hook.ts b/src/features/validation/component_hook.ts new file mode 100644 index 0000000..e23126f --- /dev/null +++ b/src/features/validation/component_hook.ts @@ -0,0 +1,4 @@ +import Hooks from '@poppinss/hooks' +import { ComponentHookEvents } from '../../component_hook.js' + +export const ValidationComponentHook = (hooks: Hooks) => {} diff --git a/src/features/validation/handles_validation.ts b/src/features/validation/handles_validation.ts new file mode 100644 index 0000000..5504760 --- /dev/null +++ b/src/features/validation/handles_validation.ts @@ -0,0 +1,5 @@ +export type Constructor = new (...args: any[]) => {} + +export function HandlesValidation(superclass: T) { + return class HandlesValidationImpl extends superclass {} +} diff --git a/src/handle_components.ts b/src/handle_components.ts index 2fd9ea9..9771d12 100644 --- a/src/handle_components.ts +++ b/src/handle_components.ts @@ -12,28 +12,35 @@ import { getPublicProperties } from './utils/object.js' import { HttpContext } from '@adonisjs/core/http' import app from '@adonisjs/core/services/app' import emitter from '@adonisjs/core/services/emitter' +import { ComponentHookRegistry } from './component_hook_registry.js' +import { ViewContext } from './view_context.js' @inject() export class HandleComponents { #componentsRegistry: ComponentRegistry + #componentHookRegistry: ComponentHookRegistry - constructor(edgewire: ComponentRegistry) { - this.#componentsRegistry = edgewire + constructor(componentsRegistry: ComponentRegistry, componentHookRegistry: ComponentHookRegistry) { + this.#componentsRegistry = componentsRegistry + this.#componentHookRegistry = componentHookRegistry } async mount(name: string, ctx: HttpContext) { const component = this.#componentsRegistry.new(ctx, name) - const context = new ComponentContext(component, true) - let html = await this.#render(component) + const mount = this.#componentHookRegistry.hooks.runner('mount') + + await mount.run(component, {}) - emitter.emit('edgewire:hydrate', { component, context }) + let html = await this.#render(component) html = insertAttributesIntoHtmlRoot(html, { 'wire:effects': [], 'wire:snapshot': this.#snapshot(component), }) + await mount.cleanup(html) + return html } @@ -57,6 +64,8 @@ export class HandleComponents { 'wire:snapshot': newSnapshot, }) + context.addEffect('html', html) + return { snapshot: newSnapshot, effects: context.effects } } @@ -81,15 +90,21 @@ export class HandleComponents { async #render(component: Component, _default?: string): Promise { const { view, properties } = await this.#getView(component) + const viewContext = new ViewContext() - emitter.emit('edgewire:render', { component, view, properties }) + const render = this.#componentHookRegistry.hooks.runner('render') + await render.run(component, view, properties) let html = await view.render() html = insertAttributesIntoHtmlRoot(html, { 'wire:id': component.id, }) - emitter.emit('edgewire:render:after', { component, view, properties }) + const replaceHtml = (newHtml: string) => { + html = newHtml + } + + await render.cleanup(html, replaceHtml, viewContext) return html } diff --git a/src/mixins/with_lifecycle_hooks.ts b/src/mixins/with_lifecycle_hooks.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/view_context.ts b/src/view_context.ts new file mode 100644 index 0000000..f4241ea --- /dev/null +++ b/src/view_context.ts @@ -0,0 +1 @@ +export class ViewContext {}