Skip to content

Commit

Permalink
Connect signals in DOM elements/nodes (bokeh#14132)
Browse files Browse the repository at this point in the history
* Connect signals in DOM elements/nodes

* Add notes about API changes
  • Loading branch information
mattpap authored Jan 8, 2025
1 parent d6d1cf1 commit fe06f34
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 26 deletions.
2 changes: 1 addition & 1 deletion bokehjs/src/less/base.less
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
font-family: inherit;
}

pre, code {
pre, code, tt {
font-family: var(--mono-font);
margin: 0;
}
12 changes: 11 additions & 1 deletion bokehjs/src/lib/core/dom_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {assert} from "./util/assert"
import type {BBox} from "./util/bbox"
import base_css from "styles/base.css"

export type RenderingTarget = HTMLElement | ShadowRoot

export interface DOMView extends View {
constructor: Function & {tag_name: keyof HTMLElementTagNameMap}
}
Expand Down Expand Up @@ -101,7 +103,7 @@ export abstract class DOMView extends View {
* This is useful when creating "floating" components or adding
* components to canvas' layers.
*/
rendering_target(): HTMLElement | ShadowRoot | null {
rendering_target(): RenderingTarget | null {
return null
}
}
Expand All @@ -115,6 +117,10 @@ export abstract class DOMElementView extends DOMView {
super.initialize()
this.class_list = new ClassList(this.el.classList)
}

get self_target(): RenderingTarget {
return this.el
}
}

export abstract class DOMComponentView extends DOMElementView {
Expand All @@ -123,6 +129,10 @@ export abstract class DOMComponentView extends DOMElementView {

declare shadow_el: ShadowRoot

override get self_target(): RenderingTarget {
return this.shadow_el
}

override initialize(): void {
super.initialize()
this.shadow_el = this.el.attachShadow({mode: "open"})
Expand Down
14 changes: 10 additions & 4 deletions bokehjs/src/lib/models/dom/color_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ import * as styles from "styles/tooltips.css"
export class ColorRefView extends ValueRefView {
declare model: ColorRef

value_el?: HTMLElement
swatch_el?: HTMLElement
value_el: HTMLElement
swatch_el: HTMLElement

override connect_signals(): void {
super.connect_signals()

const {hex, swatch} = this.model.properties
this.on_change([hex, swatch], () => this.render())
}

override render(): void {
super.render()

this.value_el = span()
this.swatch_el = span({class: styles.tooltip_color_block}, " ")

this.el.appendChild(this.value_el)
this.el.appendChild(this.swatch_el)
this.el.append(this.value_el, this.swatch_el)
}

override update(source: ColumnarDataSource, i: Index | null, _vars: PlainObject, _formatters?: Formatters): void {
Expand Down
71 changes: 67 additions & 4 deletions bokehjs/src/lib/models/dom/dom_element.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {DOMNode, DOMNodeView} from "./dom_node"
import {StylesLike} from "../ui/styled_element"
import {UIElement} from "../ui/ui_element"
import type {ViewStorage, IterViews} from "core/build_views"
import type {ViewStorage, BuildResult, IterViews} from "core/build_views"
import {build_views, remove_views} from "core/build_views"
import type {RenderingTarget} from "core/dom_view"
import {isString} from "core/util/types"
import {apply_styles} from "core/css"
import {empty, bounding_box} from "core/dom"
Expand All @@ -17,6 +18,10 @@ export abstract class DOMElementView extends DOMNodeView {
return bounding_box(this.el).relative()
}

get self_target(): RenderingTarget {
return this.el
}

readonly child_views: ViewStorage<DOMNode | UIElement> = new Map()

override *children(): IterViews {
Expand All @@ -35,17 +40,75 @@ export abstract class DOMElementView extends DOMNodeView {
super.remove()
}

override connect_signals(): void {
super.connect_signals()

const {children, style} = this.model.properties
this.on_change(children, async () => {
await this._update_children()
})
this.on_change(style, () => {
this.el.removeAttribute("style")
apply_styles(this.el.style, this.model.style)
})
}

protected async _build_children(): Promise<BuildResult<DOMNode | UIElement>> {
const children = this.model.children.filter((obj): obj is DOMNode | UIElement => !isString(obj))
return await build_views(this.child_views, children, {parent: this})
}

protected async _update_children(): Promise<void> {
const {created} = await this._build_children()
const created_elements = new Set(created)

// First remove and then either reattach existing elements or render and
// attach new elements, so that the order of children is consistent, while
// avoiding expensive re-rendering of existing views.
for (const element_view of this.child_views.values()) {
element_view.el.remove()
}

for (const child of this.model.children) {
if (isString(child)) {
const node = document.createTextNode(child)
this.el.append(node)
} else {
const child_view = this.child_views.get(child)
if (child_view == null) {
continue
}

const is_new = created_elements.has(child_view)

const target = child_view.rendering_target() ?? this.self_target
if (is_new) {
child_view.render_to(target)
} else {
target.append(child_view.el)
}
}
}

this.r_after_render()
}

override render(): void {
empty(this.el)
apply_styles(this.el.style, this.model.style)

for (const child of this.model.children) {
if (isString(child)) {
const node = document.createTextNode(child)
this.el.appendChild(node)
this.el.append(node)
} else {
const child_view = this.child_views.get(child)!
child_view.render_to(this.el)
const child_view = this.child_views.get(child)
if (child_view == null) {
continue
}

const target = child_view.rendering_target() ?? this.self_target
child_view.render_to(target)
}
}

Expand Down
16 changes: 15 additions & 1 deletion bokehjs/src/lib/models/dom/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,35 @@ export class HTMLView extends DOMElementView {
]
}

protected async _update_refs(): Promise<void> {
await build_views(this._refs, this.refs)
}

override *children(): IterViews {
yield* super.children()
yield* this._refs.values()
}

override async lazy_initialize(): Promise<void> {
await super.lazy_initialize()
await build_views(this._refs, this.refs)
await this._update_refs()
}

override remove(): void {
remove_views(this._refs)
super.remove()
}

override connect_signals(): void {
super.connect_signals()

const {refs, html} = this.model.properties
this.on_change([refs, html], async () => {
await this._update_refs()
this.render()
})
}

override render(): void {
super.render()

Expand Down
22 changes: 16 additions & 6 deletions bokehjs/src/lib/models/dom/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,38 @@ import {PlaceholderView} from "./placeholder"
import type {Formatters} from "./placeholder"
import type {ColumnarDataSource} from "../sources/columnar_data_source"
import type {Index} from "core/util/templating"
import type {ViewStorage, IterViews} from "core/build_views"
import type {ViewStorage, IterViews, ViewOf} from "core/build_views"
import {build_views, remove_views, traverse_views} from "core/build_views"
import type {PlainObject} from "core/types"
import type * as p from "core/properties"

export class TemplateView extends DOMElementView {
declare model: Template

readonly action_views: ViewStorage<Action> = new Map()
readonly _action_views: ViewStorage<Action> = new Map()
get actions(): Action[] {
return this.model.actions
}
get action_views(): ViewOf<Action>[] {
return this.actions.map((model) => this._action_views.get(model)).filter((view) => view != null)
}

protected async _update_actions(): Promise<void> {
await build_views(this._action_views, this.actions)
}

override *children(): IterViews {
yield* super.children()
yield* this.action_views.values()
yield* this.action_views
}

override async lazy_initialize(): Promise<void> {
await super.lazy_initialize()
await build_views(this.action_views, this.model.actions, {parent: this})
await this._update_actions()
}

override remove(): void {
remove_views(this.action_views)
remove_views(this._action_views)
super.remove()
}

Expand All @@ -35,7 +45,7 @@ export class TemplateView extends DOMElementView {
view.update(source, i, vars, formatters)
}
})
for (const action of this.action_views.values()) {
for (const action of this.action_views) {
action.update(source, i, vars)
}
}
Expand Down
7 changes: 7 additions & 0 deletions bokehjs/src/lib/models/dom/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export class TextView extends DOMNodeView {
declare model: Text
declare el: globalThis.Text

override connect_signals(): void {
super.connect_signals()

const {content} = this.model.properties
this.on_change(content, () => this.render())
}

override render(): void {
this.el.textContent = this.model.content
}
Expand Down
23 changes: 20 additions & 3 deletions bokehjs/src/lib/models/dom/value_of.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,27 @@ export class ValueOfView extends DOMElementView {
override connect_signals(): void {
super.connect_signals()

const {obj, attr} = this.model
if (attr in obj.properties) {
this.on_change(obj.properties[attr], () => this.render())
const fn = () => this.render()
let prop: p.Property<unknown> | null = null

const reconnect = () => {
if (prop != null) {
this.disconnect(prop.change, fn)
}

const {obj, attr} = this.model
if (attr in obj.properties) {
prop = obj.properties[attr]
this.connect(prop.change, fn)
} else {
prop = null
}
}

reconnect()

const {obj, attr} = this.model.properties
this.on_change([obj, attr], () => reconnect())
}

override render(): void {
Expand Down
4 changes: 0 additions & 4 deletions bokehjs/src/lib/models/ui/pane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ export class PaneView extends UIElementView {
return await build_views(this._element_views, this.elements, {parent: this})
}

get self_target(): HTMLElement | ShadowRoot {
return this.shadow_el
}

protected async _update_elements(): Promise<void> {
const {created} = await this._build_elements()
const created_elements = new Set(created)
Expand Down
2 changes: 1 addition & 1 deletion bokehjs/src/lib/models/ui/styled_element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export abstract class StyledElementView extends DOMComponentView {
this.on_change(styles, () => this._update_styles())
this.on_change(css_classes, () => this._update_css_classes())
this.on_transitive_change(css_variables, () => this._update_css_variables())
this.on_change(stylesheets, () => this._update_stylesheets())
this.on_transitive_change(stylesheets, () => this._update_stylesheets())
}

override render(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Pane bbox=[0, 0, 200, 17]
bokeh.models.dom.HTML bbox=[0, 0, 200, 17]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit fe06f34

Please sign in to comment.