Skip to content

Commit

Permalink
hook system
Browse files Browse the repository at this point in the history
  • Loading branch information
kerwanp committed Sep 25, 2024
1 parent dd67a84 commit 5508e39
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 10 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions providers/edgewire_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand All @@ -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() {}
Expand Down
5 changes: 4 additions & 1 deletion src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,6 +19,8 @@ export abstract class Component extends compose(BaseComponent, WithAttributes) {
}

render?(): Promise<string | View>

boot?(): void
mount?(args: Record<string, any>): void

public get id() {
Expand Down
17 changes: 17 additions & 0 deletions src/component_hook.ts
Original file line number Diff line number Diff line change
@@ -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<ComponentHookEvents>) => void
15 changes: 15 additions & 0 deletions src/component_hook_registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ComponentHook, ComponentHookEvents } from './component_hook.js'
import Hooks from '@poppinss/hooks'

export class ComponentHookRegistry {
components: ComponentHook[] = []
hooks: Hooks<ComponentHookEvents>

constructor() {
this.hooks = new Hooks()
}

async register(Hook: ComponentHook) {
Hook(this.hooks)
}
}
9 changes: 9 additions & 0 deletions src/edgewire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -36,4 +41,8 @@ export class Edgewire {
view(templatePath: string, state: Record<string, any> = {}) {
return View.template(templatePath, state)
}

componentHook(hook: ComponentHook) {
this.#componentHookRegistry.register(hook)
}
}
18 changes: 18 additions & 0 deletions src/features/lifecycle/component_hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Hooks from '@poppinss/hooks'
import { ComponentHookEvents } from '../../component_hook.js'

export const LifecycleComponentHook = (hooks: Hooks<ComponentHookEvents>) => {
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)
}
})
}
37 changes: 37 additions & 0 deletions src/features/lifecycle/mixins/lifecycle_hooks.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Constructor>(superclass: T) {
return class LifecycleHooksImpl extends superclass {
#hooks: Hooks<Events>

constructor(...args: any[]) {
super(...args)
this.#hooks = new Hooks()

if (this.mount) {
this.#hooks.add('mount', this.mount)
}
}

public get hooks(): Hooks<Events> {
return this.#hooks
}
}
}
4 changes: 4 additions & 0 deletions src/features/validation/component_hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Hooks from '@poppinss/hooks'
import { ComponentHookEvents } from '../../component_hook.js'

export const ValidationComponentHook = (hooks: Hooks<ComponentHookEvents>) => {}
5 changes: 5 additions & 0 deletions src/features/validation/handles_validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type Constructor = new (...args: any[]) => {}

export function HandlesValidation<T extends Constructor>(superclass: T) {
return class HandlesValidationImpl extends superclass {}
}
29 changes: 22 additions & 7 deletions src/handle_components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -57,6 +64,8 @@ export class HandleComponents {
'wire:snapshot': newSnapshot,
})

context.addEffect('html', html)

return { snapshot: newSnapshot, effects: context.effects }
}

Expand All @@ -81,15 +90,21 @@ export class HandleComponents {

async #render(component: Component, _default?: string): Promise<string> {
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
}
Expand Down
Empty file.
1 change: 1 addition & 0 deletions src/view_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class ViewContext {}

0 comments on commit 5508e39

Please sign in to comment.