Skip to content

Add css_id parameter to Viewable components #7617

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
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
29 changes: 27 additions & 2 deletions panel/io/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,13 +333,38 @@ def init_doc(doc: Document | None) -> Document:
if thread_id:
state._thread_id_[curdoc] = thread_id

ready_callbacks = [CustomJS(code="""
let timeout = null
const doc = cb_context.index.roots[0].model.document
const write_ids = () => {
for (const v of cb_context.index.all_views()) {
for (const t of v.model.tags) {
if (typeof t === 'object' && t !== null && 'css_id' in t) {
v.el.id = t.css_id
}
}
}
timeout = null
}
doc.on_change((event) => {
if (event.kind == 'ModelChanged') {
if (timeout !== null) {
clearTimeout(timeout)
}
timeout = setTimeout(write_ids, 100)
}
})
write_ids()
""")
]
if config.global_loading_spinner:
curdoc.js_on_event(
'document_ready', CustomJS(code=f"""
ready_callbacks.append(
CustomJS(code=f"""
const body = document.getElementsByTagName('body')[0]
body.classList.remove({LOADING_INDICATOR_CSS_CLASS!r}, {config.loading_spinner!r})
""")
)
curdoc.js_on_event('document_ready', *ready_callbacks)

session_id = curdoc.session_context.id
sessions = state.session_info['sessions']
Expand Down
2 changes: 1 addition & 1 deletion panel/layout/float.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class FloatPanel(ListLike, ReactiveHTML):
</div>
"""

_rename = {'loading': None}
_rename = {'loading': None, 'css_id': None}

_scripts = {
"render": """
Expand Down
4 changes: 2 additions & 2 deletions panel/pane/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ class Pane(PaneBase, Reactive):

# Mapping from parameter name to bokeh model property name
_rename: ClassVar[Mapping[str, str | None]] = {
'default_layout': None, 'loading': None
'default_layout': None, 'loading': None, 'css_id': None
}

# List of parameters that trigger a rerender of the Bokeh model
Expand Down Expand Up @@ -331,7 +331,7 @@ def _linkable_params(self) -> list[str]:
@property
def _synced_params(self) -> list[str]:
ignored_params = [
'name', 'default_layout', 'loading', 'stylesheets'
'name', 'default_layout', 'loading', 'stylesheets', 'css_id'
] + self._rerender_params
return [p for p in self.param if p not in ignored_params and not p.startswith('_')]

Expand Down
7 changes: 5 additions & 2 deletions panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def _synced_params(self) -> list[str]:
Parameters which are synced with properties using transforms
applied in the _process_param_change method.
"""
ignored = ['default_layout', 'loading', 'background']
ignored = ['default_layout', 'loading', 'background', 'css_id']
return [p for p in self.param if p not in self._manual_params+ignored]

def _init_params(self) -> dict[str, Any]:
Expand Down Expand Up @@ -621,7 +621,7 @@ class Reactive(Syncable, Viewable):
_ignored_refs: ClassVar[tuple[str,...]] = ()

_rename: ClassVar[Mapping[str, str | None]] = {
'design': None, 'loading': None
'design': None, 'loading': None, 'css_id': None
}

__abstract = True
Expand Down Expand Up @@ -665,6 +665,9 @@ def _get_properties(self, doc: Document | None) -> dict[str, Any]:
params[k] = v = params[k] + v
elif k not in params or self.param[k].default is not v:
params[k] = v
if self.css_id:
tags = params.get('tags', [])
params['tags'] = tags + [{"css_id": self.css_id}]
properties = self._process_param_change(params)
if 'stylesheets' not in properties:
return properties
Expand Down
2 changes: 1 addition & 1 deletion panel/tests/test_reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def test_text_input_controls():
assert isinstance(wb2, WidgetBox)

params1 = {w.name.replace(" ", "_").lower() for w in wb2 if len(w.name)}
params2 = set(Viewable.param) - {"background", "design", "stylesheets", "loading"}
params2 = set(Viewable.param) - {"background", "design", "stylesheets", "loading", "css_id"}
# Background should be moved when Layoutable.background is removed.

assert not len(params1 - params2)
Expand Down
17 changes: 17 additions & 0 deletions panel/tests/ui/pane/test_markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@

expect(page.locator(".markdown").locator("div")).to_have_text('Updated\n')

def test_markdown_pane_css_id(page):
md = Markdown('Initial', css_id='foo')

serve_component(page, md)

expect(page.locator("#foo").locator("div")).to_have_text('Initial\n')

def test_markdown_pane_css_id_added(page):
md = Markdown('Initial', css_id='foo')
row = Row()

serve_component(page, row)

row.append(md)

expect(page.locator("#foo").locator("div")).to_have_text('Initial\n')

def test_update_markdown_pane_empty(page):
md = Markdown('Initial')

Expand Down Expand Up @@ -114,7 +131,7 @@
serve_component(page, html)

header_element = page.locator('h1:has-text("Header")')
assert header_element.is_visible()

Check failure on line 134 in panel/tests/ui/pane/test_markup.py

View workflow job for this annotation

GitHub Actions / ui:test-ui:ubuntu-latest

test_html_model_no_stylesheet assert False + where False = is_visible() + where is_visible = <Locator frame=<Frame name= url='http://localhost:37547/'> selector='h1:has-text("Header")'>.is_visible
assert header_element.text_content() == "Header"

def test_anchor_scroll(page):
Expand Down
13 changes: 13 additions & 0 deletions panel/tests/ui/test_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ def test_update(page, component):
expect(page.locator('h1')).to_have_text('Foo!')


@pytest.mark.parametrize('component', [JSUpdate, ReactUpdate, AnyWidgetUpdate])
def test_css_id(page, component):
example = component(text='Hello World!', css_id='foo')

serve_component(page, example)

expect(page.locator('#foo')).to_have_text('Hello World!')

example.text = "Foo!"

expect(page.locator('#foo')).to_have_text('Foo!')


class AnyWidgetInitialize(AnyWidgetComponent):

count = param.Integer(default=0)
Expand Down
7 changes: 7 additions & 0 deletions panel/tests/ui/test_reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ def test_reactive_html_param_event(page):

wait_until(lambda: component.count == 5, page)

def test_reactive_html_css_id(page):
component = ReactiveComponent(css_id='foo')

serve_component(page, component)

expect(page.locator("#foo")).to_have_text('1')

def test_reactive_html_set_loading_no_rerender(page):
component = ReactiveComponent()

Expand Down
3 changes: 3 additions & 0 deletions panel/viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,9 @@ class Viewable(Renderable, Layoutable, ServableMixin):
objects to be displayed in the notebook and on bokeh server.
"""

css_id = param.String(default=None, constant=True, doc="""
The id set on the rendered DOM node.""")

loading = param.Boolean(doc="""
Whether or not the Viewable is loading. If True a loading spinner
is shown on top of the Viewable.""")
Expand Down
Loading