Skip to content
Merged
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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,51 @@ For a development installation (requires npm),
$ jupyter nbextension enable --py --sys-prefix ipyvue
$ jupyter labextension develop . --overwrite

Scoped CSS Support
------------------

`<style scoped>` in `VueTemplate` templates is supported but disabled by default for backwards
compatibility. When enabled, CSS rules only apply to the component's own elements.

Enable globally via environment variable:

$ IPYVUE_SCOPED_CSS_SUPPORT=1 jupyter lab

Or in Python:

```python
import ipyvue
ipyvue.scoped_css_support = True
```

Or per widget:

```python
from ipyvue import VueTemplate

class MyComponent(VueTemplate):
template = """
<template>
<span class="styled">Hello</span>
</template>
<style scoped>
.styled { color: red; }
</style>
"""

widget = MyComponent(scoped_css_support=True)
```

Note: The `css` trait with `scoped=True` always works, regardless of this setting:

```python
widget = VueTemplate(
template="<template><span class='x'>Hi</span></template>",
css=".x { color: blue; }",
scoped=True
)
```

Sponsors
--------

Expand Down
147 changes: 147 additions & 0 deletions examples/ScopedCSS.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Scoped CSS\n",
"\n",
"By default, CSS in ipyvue templates is **global** — it affects all elements on the page with matching selectors. Scoped CSS limits styles to the component that defines them.\n",
"\n",
"**How it works:** ipyvue adds a unique `data-v-*` attribute to your component's elements and rewrites your CSS selectors to include it (e.g., `.my-class` → `.my-class[data-v-abc123]`)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import ipyvue as vue\n",
"import ipywidgets as widgets\n",
"from traitlets import default"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Enable scoped CSS support\n",
"\n",
"For backwards compatibility, `<style scoped>` in templates is **disabled by default**. Existing code that accidentally relied on CSS leaking would break if we enabled it automatically.\n",
"\n",
"Enable it globally for this notebook:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Enable scoped CSS support for <style scoped> in templates\n",
"# Can also be set via environment variable: IPYVUE_SCOPED_CSS_SUPPORT=1\n",
"vue.scoped_css_support = True"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Without scoped CSS (the problem)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class GlobalStyle(vue.VueTemplate):\n",
" @default(\"template\")\n",
" def _default_template(self):\n",
" return \"\"\"\n",
" <template>\n",
" <span class=\"demo-text\">Widget A</span>\n",
" </template>\n",
" <style>\n",
" .demo-text { color: red; }\n",
" </style>\n",
" \"\"\"\n",
"\n",
"widget_b = vue.Html(tag=\"span\", children=[\"Widget B (innocent bystander)\"], class_=\"demo-text\")\n",
"\n",
"widgets.VBox([GlobalStyle(), widget_b]) # Both turn red!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## With `<style scoped>`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class ScopedStyle(vue.VueTemplate):\n",
" @default(\"template\")\n",
" def _default_template(self):\n",
" return \"\"\"\n",
" <template>\n",
" <span class=\"demo-text-2\">Widget A (scoped)</span>\n",
" </template>\n",
" <style scoped>\n",
" .demo-text-2 { color: green; }\n",
" </style>\n",
" \"\"\"\n",
"\n",
"widget_b = vue.Html(tag=\"span\", children=[\"Widget B (unaffected)\"], class_=\"demo-text-2\")\n",
"\n",
"widgets.VBox([ScopedStyle(), widget_b]) # Only Widget A is green"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Using the `css` trait with `scoped=True`\n",
"\n",
"Alternative syntax when defining CSS outside the template:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class CssTrait(vue.VueTemplate):\n",
" @default(\"template\")\n",
" def _default_template(self):\n",
" return \"<template><span class='trait-demo'>Widget C (scoped via trait)</span></template>\"\n",
"\n",
"widget_c = CssTrait(css=\".trait-demo { color: blue; }\", scoped=True)\n",
"widget_d = vue.Html(tag=\"span\", children=[\"Widget D (unaffected)\"], class_=\"trait-demo\")\n",
"\n",
"widgets.VBox([widget_c, widget_d])"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.9.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
11 changes: 10 additions & 1 deletion ipyvue/VueTemplateWidget.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from traitlets import Any, Unicode, List, Dict, Union, Instance
from traitlets import Any, Bool, Unicode, List, Dict, Union, Instance, default
from ipywidgets import DOMWidget
from ipywidgets.widgets.widget import widget_serialization

Expand All @@ -8,6 +8,7 @@
from .ForceLoad import force_load_instance
import inspect
from importlib import import_module
import ipyvue

OBJECT_REF = "objectRef"
FUNCTION_REF = "functionRef"
Expand Down Expand Up @@ -118,6 +119,14 @@ class VueTemplate(DOMWidget, Events):

css = Unicode(None, allow_none=True).tag(sync=True)

scoped = Bool(None, allow_none=True).tag(sync=True)

scoped_css_support = Bool(allow_none=False).tag(sync=True)

@default("scoped_css_support")
def _default_scoped_css_support(self):
return ipyvue.scoped_css_support

methods = Unicode(None, allow_none=True).tag(sync=True)

data = Unicode(None, allow_none=True).tag(sync=True)
Expand Down
18 changes: 18 additions & 0 deletions ipyvue/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

from ._version import __version__
from .Html import Html
from .Template import Template, watch
Expand All @@ -10,6 +12,22 @@
)


def _parse_bool_env(key: str, default: bool = False) -> bool:
"""Parse boolean from environment variable."""
val = os.environ.get(key, "").lower()
if val in ("1", "true", "yes", "on"):
return True
if val in ("0", "false", "no", "off"):
return False
return default


# Global default for scoped CSS support in VueTemplate.
# Can be set via environment variable IPYVUE_SCOPED_CSS_SUPPORT=1
# or changed at runtime: ipyvue.scoped_css_support = True
scoped_css_support = _parse_bool_env("IPYVUE_SCOPED_CSS_SUPPORT", False)


def _jupyter_labextension_paths():
return [
{
Expand Down
1 change: 1 addition & 0 deletions js/src/VueTemplateModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class VueTemplateModel extends DOMWidgetModel {
_model_module_version: '^0.0.3',
template: null,
css: null,
scoped: null,
methods: null,
data: null,
events: null,
Expand Down
Loading