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

Make the Application fully composable #705

Open
wants to merge 3 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 45 additions & 17 deletions packages/application/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export { type IPlugin };
* UI applications with the ability to be safely extended by third
* party code via plugins.
*/
export class Application<T extends Widget = Widget> {
export class Application<T extends Widget | HTMLElement = Widget> {
jtpio marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to have the full picture here, and to provide more context for the future when looking back at that PR.

The main purpose for allowing an HTMLElement directly would be for apps reusing the Lumino plugin system to not have to depend on @lumino/widgets to instantiate a placeholder shell widget, for example like in the following example?

const shell = new Widget();
const app = new Application({
shell
});

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, although that the ContextMenu API is preventing to make @lumino/widgets an optional dependency;

readonly contextMenu: ContextMenu;

/**
* Construct a new application.
*
Expand All @@ -51,12 +51,16 @@ export class Application<T extends Widget = Widget> {
this.pluginRegistry.application = this;

// Initialize the application state.
this.commands = new CommandRegistry();
this.contextMenu = new ContextMenu({
this.commands = options.commands ?? new CommandRegistry();
const contextMenuOptions = {
commands: this.commands,
renderer: options.contextMenuRenderer
});
};
this.contextMenu = options.contextMenuFactory
? options.contextMenuFactory(contextMenuOptions)
: new ContextMenu(contextMenuOptions);
this.shell = options.shell;
this._hasShellWidget = this.shell instanceof Widget;
}

/**
Expand Down Expand Up @@ -196,7 +200,9 @@ export class Application<T extends Widget = Widget> {
* If the plugin provides a service which has already been provided
* by another plugin, the new service will override the old service.
*/
registerPlugin(plugin: IPlugin<this, any>): void {
registerPlugin(
plugin: IPlugin<Application<Widget | HTMLElement>, any>
): void {
this.pluginRegistry.registerPlugin(plugin);
}

Expand All @@ -208,7 +214,9 @@ export class Application<T extends Widget = Widget> {
* #### Notes
* This calls `registerPlugin()` for each of the given plugins.
*/
registerPlugins(plugins: IPlugin<this, any>[]): void {
registerPlugins(
plugins: IPlugin<Application<Widget | HTMLElement>, any>[]
): void {
this.pluginRegistry.registerPlugins(plugins);
}

Expand Down Expand Up @@ -339,10 +347,17 @@ export class Application<T extends Widget = Widget> {
* A subclass may reimplement this method as needed.
*/
protected attachShell(id: string): void {
Widget.attach(
this.shell,
(id && document.getElementById(id)) || document.body
);
if (this._hasShellWidget) {
Widget.attach(
this.shell as Widget,
(id && document.getElementById(id)) || document.body
);
} else {
const host = (id && document.getElementById(id)) || document.body;
if (!host.contains(this.shell as HTMLElement)) {
host.appendChild(this.shell as HTMLElement);
}
}
}

/**
Expand Down Expand Up @@ -419,7 +434,9 @@ export class Application<T extends Widget = Widget> {
* A subclass may reimplement this method as needed.
*/
protected evtResize(event: Event): void {
this.shell.update();
if (this._hasShellWidget) {
(this.shell as Widget).update();
}
}

/**
Expand All @@ -429,25 +446,36 @@ export class Application<T extends Widget = Widget> {
private _delegate = new PromiseDelegate<void>();
private _started = false;
private _bubblingKeydown = false;
private _hasShellWidget = true;
}

/**
* The namespace for the `Application` class statics.
* The namespace for the {@link Application} class statics.
*/
export namespace Application {
/**
* An options object for creating an application.
*/
export interface IOptions<T extends Widget> extends PluginRegistry.IOptions {
export interface IOptions<T extends Widget | HTMLElement>
extends PluginRegistry.IOptions {
/**
* The shell widget to use for the application.
*
* This should be a newly created and initialized widget.
* The shell element to use for the application.
*
* The application will attach the widget to the DOM.
* If it is a {@link Widget}, this should be a newly created and initialized widget.
* and the application will attach the widget to the DOM.
*/
shell: T;

/**
* A custom commands registry.
*/
commands?: CommandRegistry;

/**
* A custom context menu factory.
*/
contextMenuFactory?: (options: ContextMenu.IOptions) => ContextMenu;

/**
* A custom renderer for the context menu.
*/
Expand Down
57 changes: 57 additions & 0 deletions packages/application/tests/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,39 @@ describe('@lumino/application', () => {
expect(app.hasPlugin(id1)).to.be.true;
expect(app.isPluginActivated(id2)).to.be.true;
});

it('should instantiate an application with a HTMLElement as shell', () => {
const shell = document.createElement('div');
const app = new Application<HTMLDivElement>({
shell
});

expect(app).to.be.instanceOf(Application);
expect(app.commands).to.be.instanceOf(CommandRegistry);
expect(app.contextMenu).to.be.instanceOf(ContextMenu);
expect(app.shell).to.equal(shell);
});

it('should instantiate an application with a custom command registry', () => {
const commands = new (class extends CommandRegistry {})();

const app = new Application({ shell: new Widget(), commands });

expect(app.commands).to.be.equal(commands);
});

it('should instantiate an application with a custom context menu factory', () => {
const contextMenuFactory = (options: ContextMenu.IOptions) =>
new (class extends ContextMenu {})(options);

const app = new Application({
shell: new Widget(),
contextMenuFactory
});

expect(app.contextMenu).to.be.instanceOf(ContextMenu);
expect(app.contextMenu.menu.commands).to.be.equal(app.commands);
});
});

describe('#getPluginDescription', () => {
Expand Down Expand Up @@ -624,5 +657,29 @@ describe('@lumino/application', () => {
expect(app.isPluginActivated(id3)).to.be.false;
});
});

describe('#start', () => {
it('should attach the shell widget to the document body', async () => {
const shell = new Widget();
const app = new Application({
shell
});

await app.start();

expect(document.body.contains(shell.node)).to.be.true;
});

it('should attach the shell HTML element to the document body', async () => {
const shell = document.createElement('div');
const app = new Application<HTMLDivElement>({
shell
});

await app.start();

expect(document.body.contains(shell)).to.be.true;
});
});
});
});
10 changes: 6 additions & 4 deletions review/api/application.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Token } from '@lumino/coreutils';
import { Widget } from '@lumino/widgets';

// @public
export class Application<T extends Widget = Widget> {
export class Application<T extends Widget | HTMLElement = Widget> {
constructor(options: Application.IOptions<T>);
activateDeferredPlugins(): Promise<void>;
activatePlugin(id: string): Promise<void>;
Expand All @@ -34,8 +34,8 @@ export class Application<T extends Widget = Widget> {
isPluginActivated(id: string): boolean;
listPlugins(): string[];
protected pluginRegistry: PluginRegistry;
registerPlugin(plugin: IPlugin<this, any>): void;
registerPlugins(plugins: IPlugin<this, any>[]): void;
registerPlugin(plugin: IPlugin<Application<Widget | HTMLElement>, any>): void;
registerPlugins(plugins: IPlugin<Application<Widget | HTMLElement>, any>[]): void;
resolveOptionalService<U>(token: Token<U>): Promise<U | null>;
resolveRequiredService<U>(token: Token<U>): Promise<U>;
readonly shell: T;
Expand All @@ -45,7 +45,9 @@ export class Application<T extends Widget = Widget> {

// @public
export namespace Application {
export interface IOptions<T extends Widget> extends PluginRegistry.IOptions {
export interface IOptions<T extends Widget | HTMLElement> extends PluginRegistry.IOptions {
commands?: CommandRegistry;
contextMenuFactory?: (options: ContextMenu.IOptions) => ContextMenu;
contextMenuRenderer?: Menu.IRenderer;
pluginRegistry?: PluginRegistry;
shell: T;
Expand Down
Loading