PhosphorJS Documentation

repository·master·Indexed 22 days ago

https://github.com/phosphorjs/phosphor

An archived framework for building complex, widget-based web applications. It features a plugin-based architecture using services and Tokens for decoupling, an Application class for lifecycle management, a CommandRegistry for managing commands and key bindings, and utility packages including @phosphor/collections (BPlusTree, LinkedList) and @phosphor/coreutils (JSON, MIME, Promise, and random utilities).

Tokens
14K
Snippets
42
Records
65
Agent score
77%

What's inside PhosphorJS

  1. How signals and slots work for inter-object communication

    master

    PhosphorJS uses a type-safe publish-subscribe pattern based on Signal and Slot.

    • Signal: An object (the publisher) declares one or more signals. A signal is associated with a sender object.
    • Slot: A callback function (the subscriber) that is invoked when a signal is emitted. A slot has the signature (sender: T, args: U) => void.
    • Connection: A subscriber connects a slot to a signal using signal.connect(slot, thisArg).

    When a signal is emitted via signal.emit(args), all connected slots are invoked synchronously in the order they were connected. If a slot throws an exception, it is caught and passed to a global exception handler to prevent the emission loop from breaking.

    import { ISignal, Signal } from '@phosphor/signaling';
    
    class SomeClass {
      constructor(name: string) {
        this.name = name;
      }
    
      readonly name: string;
    
      get valueChanged: ISignal<this, number> {
        return this._valueChanged;
      }
    
      get value(): number {
        return this._value;
      }
    
      set value(value: number) {
        if (value === this._value) {
          return;
        }
        this._value = value;
        this._valueChanged.emit(value);
      }
    
      private _value = 0;
      private _valueChanged = new Signal<this, number>(this);
    }
    
    function logger(sender: SomeClass, value: number): void {
      console.log(sender.name, value);
    }
    
    let m1 = new SomeClass('foo');
    m1.valueChanged.connect(logger);
    
    m1.value = 42; // logs: foo 42
  2. Configure attributes for virtual elements

    master

    Virtual elements use an ElementAttrs object to define their properties. This object combines several types:

    • Base Attributes (ElementBaseAttrs): Standard HTML attributes (e.g., id, href, src, type). These are applied via element.setAttribute() using lower-case names.
    • Event Listeners (ElementEventAttrs): Inline event handlers (e.g., onclick, oninput, onkeydown). These are assigned directly to the element object.
    • Special Attributes (ElementSpecialAttrs):
      • key: A unique string used to optimize reordering of sibling nodes during diffing.
      • className: The JS-safe name for the class attribute.
      • htmlFor: The JS-safe name for the for attribute.
      • dataset: An object where keys are automatically prefixed with data- (e.g., { info: '12' } becomes data-info='12').
      • style: An object using camel-cased CSS property names (e.g., { backgroundColor: 'red' }).
    import { h } from '@phosphor/virtualdom';
    
    const element = h('button', {
      key: 'submit-btn',
      className: 'btn-primary',
      id: 'submit',
      dataset: { role: 'action' },
      style: { marginTop: '10px', color: 'blue' },
      onclick: (e) => console.log('Clicked!', e)
    }, 'Submit');
  3. Optimize rendering with keys

    master

    When rendering lists of children, providing a unique key attribute to VirtualElement nodes allows the virtual DOM diffing algorithm to optimize reordering.

    If a node moves among its siblings in the render tree, the algorithm will move the existing real DOM node instead of recreating it, provided the key remains consistent. Keys must be unique among sibling nodes.

    import { h } from '@phosphor/virtualdom';
    
    // Without keys, reordering these might cause full re-renders
    // With keys, the DOM nodes are moved instead of recreated
    const list = h('ul', [
      h('li', { key: 'item-1' }, 'First'),
      h('li', { key: 'item-2' }, 'Second'),
      h('li', { key: 'item-3' }, 'Third')
    ]);
  4. Manage commands with CommandRegistry

    master

    The CommandRegistry class is the central hub for managing a collection of commands. It allows you to register commands, execute them, and manage key bindings. A registry can be used to populate UI elements like command palettes, menus, and toolbars.

    Key capabilities:

    • Registering Commands: Use addCommand(id, options) to add a new command. It returns an IDisposable which, when disposed, removes the command.
    • Executing Commands: Use execute(id, args) to run a command by its ID. It returns a Promise that resolves with the command's result.
    • Key Bindings: Use addKeyBinding(options) to map key sequences to commands. Bindings can be restricted to specific DOM contexts using a CSS selector.
    • Observing Changes: The registry provides signals (commandChanged, commandExecuted, keyBindingChanged) to react to state changes.
    import { CommandRegistry } from '@phosphor/commands';
    
    const registry = new CommandRegistry();
    
    // Add a command
    const disposable = registry.addCommand('my-command', {
      execute: (args) => console.log('Executed with:', args),
      label: 'My Command',
      isEnabled: (args) => true
    });
    
    // Execute a command
    registry.execute('my-command', { some: 'arg' }).then(result => {
      console.log('Result:', result);
    });
    
    // Remove the command later
    disposable.dispose();
  5. How key binding sequences are matched

    master

    PhosphorJS uses a SequenceMatch enum to determine how a sequence of user-pressed keys relates to a registered key binding. This allows the system to distinguish between a complete match and a partial match (where the user has started typing a sequence but hasn't finished).

    Possible SequenceMatch values:

    • SequenceMatch.None: The user's key sequence does not match the start of the binding.
    • SequenceMatch.Partial: The user's sequence matches the beginning of the binding, but more keys are required.
    • SequenceMatch.Exact: The user's sequence matches the binding perfectly.
  6. How plugins and services work in PhosphorJS

    master

    PhosphorJS uses a plugin-based architecture to build extensible applications. Instead of direct imports, components communicate via services defined by Tokens.

    • Plugins are the unit of extensibility. They consume required services and provide new services.
    • Services are concrete implementations of interfaces or abstract types. They are singletons; once a service is provided by a plugin and resolved, the same instance is returned to all consumers.
    • Decoupling: Plugins depend on Tokens rather than specific implementations, allowing third-party code to provide alternative implementations of a service without changing the consumer code.

    When a plugin is activated, the application resolves its requires (mandatory) and optional (can be null) services and passes them as arguments to the plugin's activate() method.

  7. Use AttachedProperty to extend external objects

    master

    The AttachedProperty class allows you to attach semantic data to an external object without modifying the object's class definition. This is useful for extending the state of an object with data from an unrelated class.

    Important Note on Performance: Attached properties are stored in a hash table within a WeakMap keyed on the owner object. This involves non-trivial storage overhead, so this pattern is best reserved for storing relatively rare data.

    import { AttachedProperty } from '@phosphor/properties';
    
    interface MyOwner { id: number }
    
    // Define an attached property for a 'label' string
    const labelProperty = new AttachedProperty<MyOwner, string>({
      name: 'label',
      create: (owner) => `Default label for ${owner.id}`,
      changed: (owner, oldVal, newVal) => {
        console.log(`Label changed from ${oldVal} to ${newVal}`);
      }
    });
    
    const owner: MyOwner = { id: 1 };
    
    // Get the default value (triggers 'create')
    console.log(labelProperty.get(owner)); 
    
    // Set a new value (triggers 'changed' if different)
    labelProperty.set(owner, 'New Label');
    
    // Clear all attached data for this owner
    AttachedProperty.clearData(owner);
  8. How message conflation works

    master

    Message conflation is an advanced feature used to compress messages of the same type to avoid unnecessary processing cycles.

    1. isConflatable: A getter on the Message class. If true, the MessageLoop will attempt to merge this message with existing pending messages of the same type.
    2. conflate(other: Message): boolean: This method is called by the loop when a new conflatable message arrives. You should merge the state of other into the current message. If you return true, the other message is discarded (successfully merged). If false, the other message is enqueued normally.

    ConflatableMessage: A built-in convenience class for messages that are conflatable but carry no state other than their type. It automatically implements isConflatable: true and conflate: () => true.

    Use a custom Message subclass if you need to merge actual data/state during conflation.

  9. How drag-and-drop operations work in PhosphorJS

    master

    PhosphorJS provides a custom drag-and-drop API designed for transferring arbitrary non-string objects within a single application. This is not a replacement for the native HTML5 drag-and-drop API, but rather a programmatic way to initiate drags and pass complex data.

    A drag operation is managed by a Drag object and involves four primary custom events dispatched to drop targets:

    1. 'p-dragenter': Dispatched when the mouse enters a target. Crucially, a drop target must call event.preventDefault() on this event to receive any subsequent drag events.
    2. 'p-dragover': Dispatched when the mouse moves over the target. The target must call event.preventDefault() and set the dropAction property to one of its supported actions to allow a drop.
    3. 'p-dragleave': Dispatched when the mouse leaves the target (including moving into child elements).
    4. 'p-drop': Dispatched when the mouse is released over a target that has indicated a valid dropAction.

    Automatic Scrolling: To enable automatic scrolling when dragging near the edges of an element, add the data-p-dragscroll attribute to that element.

    // Example of enabling auto-scroll on a container
    const container = document.createElement('div');
    container.setAttribute('data-p-dragscroll', '');
  10. Implement a drop target using IDragEvent

    master

    To create a drop target, listen for PhosphorJS drag events ('p-dragenter', 'p-dragover', 'p-dragleave', 'p-drop') on your DOM elements. The event object implements IDragEvent, which extends MouseEvent.

    Key properties of IDragEvent:

    • dropAction: (Writable) The action the target is currently performing. During 'p-dragover', you must set this to a valid DropAction (e.g., 'copy', 'move') and call event.preventDefault() to allow the drop to proceed.
    • proposedAction: (Read-only) The action preferred by the drag initiator.
    • supportedActions: (Read-only) The actions supported by the initiator.
    • mimeData: (Read-only) The MimeData associated with the drag.
    • source: (Read-only) The arbitrary source object provided by the initiator.

    Supported DropAction values: 'none', 'copy', 'link', 'move'.

    element.addEventListener('p-dragenter', (e: IDragEvent) => {
      e.preventDefault(); // Required to receive other events
    });
    
    element.addEventListener('p-dragover', (e: IDragEvent) => {
      e.preventDefault(); 
      // Tell the initiator we want to perform a 'copy'
      e.dropAction = 'copy'; 
    });
    
    element.addEventListener('p-drop', (e: IDragEvent) => {
      const data = e.mimeData;
      const source = e.source;
      // Handle the drop logic here
      e.dropAction = 'copy'; // Report what was actually taken
    });
  11. Configure Application.start options

    master

    When calling app.start(options), you can provide IStartOptions to control the initial bootstrapping process:

    • hostID: The ID of the DOM node where the shell widget will be attached. If omitted, the document.body is used.
    • startPlugins: An array of plugin IDs to explicitly activate during startup (in addition to autoStart plugins).
    • ignorePlugins: An array of plugin IDs to explicitly not activate during startup (overrides both startPlugins and autoStart).
    app.start({
      hostID: 'my-app-root',
      startPlugins: ['plugin-a', 'plugin-b'],
      ignorePlugins: ['plugin-c']
    });
  12. Clear all attached data for an owner

    master

    Use AttachedProperty.clearData(owner) to remove all property values associated with a specific object.

    Note: This operation clears the stored data but does not trigger any changed notifications for the properties being cleared.

    import { AttachedProperty } from '@phosphor/properties';
    
    const owner = { some: 'object' };
    // ... use properties ...
    AttachedProperty.clearData(owner);