Skip to content
This repository has been archived by the owner on Nov 6, 2019. It is now read-only.

add a RenderWidget #299

Open
wants to merge 2 commits into
base: master
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
11 changes: 9 additions & 2 deletions packages/virtualdom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,13 @@ export
type VirtualNode = VirtualElement | VirtualText;


/**
* A type alias for virtual content.
*/
export
type VirtualContent = VirtualNode | ReadonlyArray<VirtualNode> | null;


/**
* Create a new virtual element node.
*
Expand Down Expand Up @@ -972,7 +979,7 @@ namespace VirtualDOM {
* result in undefined rendering behavior.
*/
export
function render(content: VirtualNode | ReadonlyArray<VirtualNode> | 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);
Expand All @@ -995,7 +1002,7 @@ namespace Private {
* Cast a content value to a content array.
*/
export
function asContentArray(value: VirtualNode | ReadonlyArray<VirtualNode> | null): ReadonlyArray<VirtualNode> {
function asContentArray(value: VirtualContent): ReadonlyArray<VirtualNode> {
if (!value) {
return [];
}
Expand Down
1 change: 1 addition & 0 deletions packages/widgets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
225 changes: 225 additions & 0 deletions packages/widgets/src/renderwidget.ts
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice, this was the main point I wanted to make - that this should be a simple interface with only the signal. This will allow us to use it with our Redux style state stores as well. Is the name of the signal the same in both places?

Copy link
Member Author

Choose a reason for hiding this comment

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

The name is not the same in both places, but your model is likely to wrap the store anyway.

We were talking about a few alternatives:

  1. Don't have a model at all on this class, and let the subclass define it.
  2. Make the model a simple T | null and have a protected modelSwapped(old, new) method which gets called, and let the subclass subscribe/unsubscribe as/if needed to their model type.

/**
* 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<this, void>;
}


/**
* 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<T extends IRenderModel = {}> extends Widget {
Copy link
Contributor

Choose a reason for hiding this comment

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

In JLab we have stopped using the Widget suffix in classnames as it is redundant. Fine here unless we have a better idea.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've been trying to keep widget names to two words, so Render is clearly out. Not sure I have a better idea. We have overloaded the term "renderer" too much between Phosphor and JLab for me to use it here.

/**
* 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');
}