diff --git a/packages/virtualdom/src/index.ts b/packages/virtualdom/src/index.ts index 43a5bb37b..f6ccb0dda 100644 --- a/packages/virtualdom/src/index.ts +++ b/packages/virtualdom/src/index.ts @@ -755,6 +755,13 @@ export type VirtualNode = VirtualElement | VirtualText; +/** + * A type alias for virtual content. + */ +export +type VirtualContent = VirtualNode | ReadonlyArray | null; + + /** * Create a new virtual element node. * @@ -972,7 +979,7 @@ namespace VirtualDOM { * result in undefined rendering behavior. */ export - function render(content: VirtualNode | ReadonlyArray | null, host: HTMLElement): void { + function render(content: VirtualContent, host: HTMLElement): void { let oldContent = Private.hostMap.get(host) || []; let newContent = Private.asContentArray(content); Private.hostMap.set(host, newContent); @@ -995,7 +1002,7 @@ namespace Private { * Cast a content value to a content array. */ export - function asContentArray(value: VirtualNode | ReadonlyArray | null): ReadonlyArray { + function asContentArray(value: VirtualContent): ReadonlyArray { if (!value) { return []; } diff --git a/packages/widgets/src/index.ts b/packages/widgets/src/index.ts index 7f80e13ff..a6a8d5cc2 100644 --- a/packages/widgets/src/index.ts +++ b/packages/widgets/src/index.ts @@ -19,6 +19,7 @@ export * from './menu'; export * from './menubar'; export * from './panel'; export * from './panellayout'; +export * from './renderwidget'; export * from './scrollbar'; export * from './singlelayout'; export * from './splitlayout'; diff --git a/packages/widgets/src/renderwidget.ts b/packages/widgets/src/renderwidget.ts new file mode 100644 index 000000000..073f88eb7 --- /dev/null +++ b/packages/widgets/src/renderwidget.ts @@ -0,0 +1,225 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2014-2017, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +import { + Message, MessageLoop +} from '@phosphor/messaging'; + +import { + ISignal +} from '@phosphor/signaling'; + +import { + VirtualContent, VirtualDOM +} from '@phosphor/virtualdom'; + +import { + Widget +} from './widget'; + + +/** + * An object which can be used as a model for a render widget. + */ +export +interface IRenderModel { + /** + * A signal emitted when the model state has changed. + * + * #### notes + * If this signal is provided, the render widget will automatically + * update whenever the signal is emitted. + */ + readonly stateChanged?: ISignal; +} + + +/** + * A widget which renders its content using the virtual DOM. + * + * #### Notes + * Most subclasses will typically only implement the abstract `render()` + * method. Advanced use cases may reimplement some of the other methods. + */ +export +abstract class RenderWidget extends Widget { + /** + * Construct a new render widget. + */ + constructor() { + super(); + this.addClass('p-RenderWidget'); + this.setFlag(Widget.Flag.DisallowLayout); + } + + /** + * Get the model for the widget. + */ + get model(): T | null { + return this._model; + } + + /** + * Set the model for the widget. + */ + set model(value: T | null) { + // Bail early if the model does not change. + if (this._model === value) { + return; + } + + // Disconnect from the `stateChanged` signal, if provided. + if (this._model && this._model.stateChanged) { + this._model.stateChanged.disconnect(this.onModelStateChanged, this); + } + + // Update the internal model + this._model = value; + + // Connect to the `stateChanged` signal, if provided. + if (this._model && this._model.stateChanged) { + this._model.stateChanged.connect(this.onModelStateChanged, this); + } + + // Schedule an update of the widget. + this.update(); + } + + /** + * Process a message sent to the widget. + */ + processMessage(msg: Message): void { + switch (msg.type) { + case 'before-render': + this.onBeforeRender(msg); + break; + case 'after-render': + this.onAfterRender(msg); + break; + default: + super.processMessage(msg); + } + } + + /** + * Create the virtual DOM content for the widget. + * + * @returns The virtual DOM content to render into the widget. + * + * #### Notes + * This method is called automatically after the widget is attached + * or made visible. It can be triggered procedurally by calling the + * `update()` method. + * + * This will not be invoked if `shouldRender()` returns `false`. + * + * This method must be implemented by a subclass. + */ + protected abstract render(): VirtualContent; + + /** + * Test whether the widget should be rendered. + * + * @returns Whether the widget content should be rendered. + * + * #### Notes + * This method is invoked when the widget receives a message of type + * `'update-request'`. It is used to determine whether to (re)render + * the widget content. If this method returns `false`, the `render` + * method will not be invoked and the widget will not be updated. + * + * The default implementation of this method returns `true` IFF the + * widget is visible. + * + * A subclass may reimplement this method as needed. + */ + protected shouldRender(): boolean { + return this.isVisible; + } + + /** + * A message handler invoked on a `'before-render'` message. + * + * #### Notes + * The default implementation of this method is a no-op. + */ + protected onBeforeRender(msg: Message): void { } + + /** + * A message handler invoked on an `'after-render'` message. + * + * #### Notes + * The default implementation of this method is a no-op. + */ + protected onAfterRender(msg: Message): void { } + + /** + * A message handler invoked on a `'before-attach'` message. + */ + protected onBeforeAttach(msg: Message): void { + this.update(); + } + + /** + * A message handler invoked on a `'before-show'` message. + */ + protected onBeforeShow(msg: Message): void { + this.update(); + } + + /** + * A message handler invoked on an `'update-request'` message. + */ + protected onUpdateRequest(msg: Message): void { + // Bail if the widget should not render. + if (!this.shouldRender()) { + return; + } + + // Send a `'before-render'` message to the widget. + MessageLoop.sendMessage(this, RenderWidget.BeforeRender); + + // Render the virtual content into the widget. + VirtualDOM.render(this.render(), this.node); + + // Send an `'after-render'` message to the widget. + MessageLoop.sendMessage(this, RenderWidget.AfterRender); + } + + /** + * Handle the `stateChanged` signal from the model. + * + * #### Notes + * The default implementation schedules an update of the widget. + * + * A subclass may reimplement this method as needed. + */ + protected onModelStateChanged(): void { + this.update(); + } + + private _model: T | null = null; +} + + +/** + * The namespace for the `RenderWidget` class statics. + */ +export +namespace RenderWidget { + /** + * A singleton `'before-render'` message. + */ + export + const BeforeRender = new Message('before-render'); + + /** + * A singleton `'after-render'` message. + */ + export + const AfterRender = new Message('after-render'); +}