Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy registration of controllers #690

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/core/application.ts
Expand Up @@ -7,6 +7,8 @@ import { Router } from "./router"
import { Schema, defaultSchema } from "./schema"
import { ActionDescriptorFilter, ActionDescriptorFilters, defaultActionDescriptorFilters } from "./action_descriptor"

export type AsyncConstructor = () => Promise<ControllerConstructor>

export class Application implements ErrorHandler {
readonly element: Element
readonly schema: Schema
Expand Down Expand Up @@ -49,6 +51,10 @@ export class Application implements ErrorHandler {
this.load({ identifier, controllerConstructor })
}

registerLazy(identifier: string, controllerConstructor: AsyncConstructor) {
this.router.registerLazyModule(identifier, controllerConstructor)
}

registerActionOption(name: string, filter: ActionDescriptorFilter) {
this.actionDescriptorFilters[name] = filter
}
Expand Down
35 changes: 33 additions & 2 deletions src/core/router.ts
@@ -1,4 +1,4 @@
import { Application } from "./application"
import { Application, AsyncConstructor } from "./application"
import { Context } from "./context"
import { Definition } from "./definition"
import { Module } from "./module"
Expand All @@ -11,11 +11,13 @@ export class Router implements ScopeObserverDelegate {
private scopeObserver: ScopeObserver
private scopesByIdentifier: Multimap<string, Scope>
private modulesByIdentifier: Map<string, Module>
private lazyModulesByIdentifier: Map<string, AsyncConstructor>

constructor(application: Application) {
this.application = application
this.scopeObserver = new ScopeObserver(this.element, this.schema, this)
this.scopesByIdentifier = new Multimap()
this.lazyModulesByIdentifier = new Map()
this.modulesByIdentifier = new Map()
}

Expand Down Expand Up @@ -98,10 +100,13 @@ export class Router implements ScopeObserverDelegate {
}

scopeConnected(scope: Scope) {
this.scopesByIdentifier.add(scope.identifier, scope)
const { identifier } = scope
this.scopesByIdentifier.add(identifier, scope)
const module = this.modulesByIdentifier.get(scope.identifier)
if (module) {
module.connectContextForScope(scope)
} else if (this.lazyModulesByIdentifier.has(identifier)) {
this.loadLazyModule(identifier)
}
}

Expand All @@ -115,6 +120,14 @@ export class Router implements ScopeObserverDelegate {

// Modules

registerLazyModule(identifier: string, controllerConstructor: AsyncConstructor) {
if (!this.modulesByIdentifier.has(identifier) && !this.lazyModulesByIdentifier.has(identifier)) {
this.lazyModulesByIdentifier.set(identifier, controllerConstructor)
} else {
this.application.logger.warn(`Stimulus has already a controller with "${identifier}" registered.`)
}
}

private connectModule(module: Module) {
this.modulesByIdentifier.set(module.identifier, module)
const scopes = this.scopesByIdentifier.getValuesForKey(module.identifier)
Expand All @@ -126,4 +139,22 @@ export class Router implements ScopeObserverDelegate {
const scopes = this.scopesByIdentifier.getValuesForKey(module.identifier)
scopes.forEach((scope) => module.disconnectContextForScope(scope))
}

private loadLazyModule(identifier: string) {
const callback = this.lazyModulesByIdentifier.get(identifier)
if (callback && typeof callback === "function") {
callback().then((controllerConstructor) => {
if (!this.modulesByIdentifier.has(identifier)) {
this.loadDefinition({ identifier, controllerConstructor })
this.lazyModulesByIdentifier.delete(identifier)
}
})
} else {
this.application.logger.warn(
`Stimulus expected the callback registered for "${identifier}" to resolve to a controllerConstructor but didn't`,
`Failed to lazy load ${identifier}`,
{ identifier }
)
}
}
}
2 changes: 1 addition & 1 deletion src/tests/modules/core/error_handler_tests.ts
Expand Up @@ -2,7 +2,7 @@ import { Controller } from "../../../core/controller"
import { Application } from "../../../core/application"
import { ControllerTestCase } from "../../cases/controller_test_case"

class MockLogger {
export class MockLogger {
errors: any[] = []
logs: any[] = []
warns: any[] = []
Expand Down
28 changes: 28 additions & 0 deletions src/tests/modules/core/lazy_loading_tests.ts
@@ -0,0 +1,28 @@
import { ApplicationTestCase } from "../../cases"
import { Controller } from "../../../core"
import { MockLogger } from "./error_handler_tests"

class LazyController extends Controller {
connect() {
this.application.logger.log("Hello from lazy controller")
}
}

export default class LazyLoadingTests extends ApplicationTestCase {
async setupApplication() {
this.application.logger = new MockLogger()

this.application.registerLazy("lazy", () => new Promise((resolve, _reject) => resolve(LazyController)))
}

get mockLogger(): MockLogger {
return this.application.logger as any
}

async "test lazy loading of controllers"() {
await this.renderFixture(`<div data-controller="lazy"></div><div data-controller="lazy"></div>`)

this.assert.equal(this.mockLogger.logs.length, 2)
this.mockLogger.logs.forEach((entry) => this.assert.equal(entry, "Hello from lazy controller"))
}
}