How do I implement a custom button by extending the existing Button?

Hi

I’m trying to implement a custom FastButton based on the fast-button web component.

image

The strategy is to inherit from the existing Panel and Bokeh button classes. But I cannot get it working. And I don’t know where to look.

The .gif shows where I am. The problems are that

  • Nothing happens when I click or select before I refresh the browser.
  • When I refresh after having selected a new appearance I get a bokeh.min.js?v=16636fdaaea87a3216e9adf3b34ca3c7:551 [bokeh] Failed to repull session Error: property awesome_panel_extensions.bokeh_extensions.fast.fast_button.FastButton.appearance wasn't declared error. But as far as I can see it’s declared in all panel and bokeh files.

bokeh-model-error

Code

bokeh fast_button.ts


import {Button, ButtonView} from "@bokehjs/models/widgets/button"

import * as p from "@bokehjs/core/properties"

export class FastButtonView extends ButtonView {
  model: FastButton
}

export namespace FastButton {
  export type Attrs = p.AttrsOf<Props>

  export type Props = Button.Props & {
    appearance: p.Property<string>
    autofocus: p.Property<boolean>
    }
}

export interface FastButton extends FastButton.Attrs {}

export class FastButton extends Button {
  properties: FastButton.Props
  __view_type__: FastButtonView

  constructor(attrs?: Partial<FastButton.Attrs>) {
    super(attrs)
  }

  static __module__ = "awesome_panel_extensions.bokeh_extensions.fast.fast_button"

  static init_FastButton(): void {
    this.prototype.default_view = FastButtonView

    this.define<FastButton.Props>({
        appearance: [p.String, ],
        autofocus: [p.Boolean, ],
    })
  }
}

bokeh fast_button.py

from bokeh.models import Button as _BkButton
from bokeh.core import properties

class FastButton(_BkButton):
    appearance = properties.String(
        default="neutral",
        help="The appearance attribute",
    )
    autofocus = properties.Bool(
        default=False,
        help="The autofocus attribute",
    )

panel fast_button.py

"""The FastButton extends the Panel Button to a Fast Design Framework Button.

It is built on the the fast-button web component. The component supports several visual apperances
(accent, lightweight, neutral, outline, stealth).

For more information view the [component specification]\
(https://github.com/microsoft/fast/tree/master/packages/web-components/fast-foundation/\
src/button/button.spec.md).

See also https://explore.fast.design/components/fast-button.
    """
import param
from panel.widgets import Button
from awesome_panel_extensions.bokeh_extensions.fast.fast_button import FastButton as _BkFastButton

FAST_BUTTON_APPEARENCES = [
    "accent",
    "lightweight",
    "neutral",
    "outline",
    "stealth",
]
DEFAULT_FAST_BUTTON_APPEARANCE = "neutral"
BUTTON_TYPE_TO_APPEARANCE = {
    "default": "neutral",
    "primary": "accent",
    "success": "outline",
    "warning": "accent",
    "danger": "accent",
}


class FastButton(Button):
    """The FastButton extends the Panel Button into the Fast Design Framework.

It is built on the the fast-button web component. The component supports several visual apperances
(accent, lightweight, neutral, outline, stealth).

For more information view the [component specification]\
(https://github.com/microsoft/fast/tree/master/packages/web-components/fast-foundation/\
src/button/button.spec.md).

See also https://explore.fast.design/components/fast-button.
    """
    appearance = param.ObjectSelector(
        default=DEFAULT_FAST_BUTTON_APPEARANCE,
        objects=FAST_BUTTON_APPEARENCES,
        doc="The appearance attribute",
        allow_None=True,
    )
    autofocus = param.Boolean(default=False, doc="The autofocus attribute",)

    _widget_type = _BkFastButton

    def __init__(self, **params):
        if "button_type" in params and "appearance" not in params:
            params["appearance"] = BUTTON_TYPE_TO_APPEARANCE[params["button_type"]]
        super().__init__(**params)

    @param.depends("button_type", watch=True)
    def _update_accent(self, *_):
        self.appearance = BUTTON_TYPE_TO_APPEARANCE[self.button_type]

app code

import panel as pn
from awesome_panel_extensions.frameworks.fast import config
from awesome_panel_extensions.frameworks.fast import FastButton
pn.Column(
    FastButton(name="Hello World"),
    pn.Param(FastButton, parameters=["button_type", "clicks", "autofocus", "appearance"]),
    config.get_fast_js_panel(),
    ).servable()

@Marc

I have implemented a very small number of custom model extensions using TypeScript in Bokeh. With that caveat, my realizations have always include an _implementation__ assignment in the Python class that extends the model. I do not see anything similar in the code posted here. Is this difference in code architecture by design?

Here’s a simple example for reference in case it helps.

iso_div.ts

import {Div, DivView} from "models/widgets/div"

export class IsoDivView extends DivView {
    connect_signals(): void {
        // ***
        // Isolate root layout from div changes
        // Ref. https://discourse.bokeh.org/t/layout-guidelines-tips-for-rapidly-rendering-many-plots/5119
        //  * comment out super.connect_signals(), this might work b/c (Widget,WidgetView) has corresponding
        //    do-nothing void method ???
        //  * comment out this.root.compute_layout() to prevent re-layout of entire page if div model changes
        // ***
        //super.connect_signals()
        this.connect(this.model.change, () => {
            this.render()
            //this.root.compute_layout(); // XXX: invalidate_layout?
        })
    }
}

export class IsoDiv extends Div {
    static init_IsoDiv(): void {
        this.prototype.default_view = IsoDivView
    }
}

iso_div.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Custom bokeh HTML div model
"""
from bokeh.models import Div

class IsoDiv(Div):
    """Custom bokeh HTML div model
    """
    __implementation__ = "iso_div.ts"

Thanks @_jm

The reason why I don’t have an implementation is that I prebuild the bokeh models. Cf. https://awesome-panel.readthedocs.io/en/latest/guides/awesome-panel-extensions-guide/bokeh-extensions.html#prebuilt-bokeh-extensions.

Your model will always build when you start the application. My models are built once (like Panel) and can be distributed in a python package.

Thanks for reaching out. You actually gave me an idea to simplify my code, get something working and then expand from there.

It was an error 40 on my side. I did not instantiate the FastButton in the code below

pn.Column(
    FastButton(name="Hello World"),
    pn.Param(FastButton, parameters=["button_type", "clicks", "autofocus", "appearance"]),
    config.get_fast_js_panel(),
    ).servable()

If changed to

button = FastButton(name="Hello World")
pn.Column(
    button,
    pn.Param(button, parameters=["button_type", "clicks", "autofocus", "appearance"]),
    config.get_fast_js_panel(),
    ).show(port=5007)

then it works.

Alway, always reduce the problemsize and investigate. Please learn the @Marc :slight_smile: