Skip to content

Commit 9d60d1e

Browse files
author
Maarten Breddels AI
committed
fix: make scoped CSS support optional via opt-in
Add scoped_css_support trait to VueTemplate that controls whether <style scoped> in templates is processed. This is a backwards-compatible change that defaults to False to avoid breaking existing code. Configuration options: - Environment variable: IPYVUE_SCOPED_CSS_SUPPORT=1 - Global setting: ipyvue.scoped_css_support = True - Per-widget: VueTemplate(..., scoped_css_support=True) The explicit scoped=True/False trait (used with the css trait) still works independently of this setting.
1 parent 0d32bd1 commit 9d60d1e

File tree

5 files changed

+81
-6
lines changed

5 files changed

+81
-6
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,51 @@ For a development installation (requires npm),
2626
$ jupyter nbextension enable --py --sys-prefix ipyvue
2727
$ jupyter labextension develop . --overwrite
2828

29+
Scoped CSS Support
30+
------------------
31+
32+
`<style scoped>` in `VueTemplate` templates is supported but disabled by default for backwards
33+
compatibility. When enabled, CSS rules only apply to the component's own elements.
34+
35+
Enable globally via environment variable:
36+
37+
$ IPYVUE_SCOPED_CSS_SUPPORT=1 jupyter lab
38+
39+
Or in Python:
40+
41+
```python
42+
import ipyvue
43+
ipyvue.scoped_css_support = True
44+
```
45+
46+
Or per widget:
47+
48+
```python
49+
from ipyvue import VueTemplate
50+
51+
class MyComponent(VueTemplate):
52+
template = """
53+
<template>
54+
<span class="styled">Hello</span>
55+
</template>
56+
<style scoped>
57+
.styled { color: red; }
58+
</style>
59+
"""
60+
61+
widget = MyComponent(scoped_css_support=True)
62+
```
63+
64+
Note: The `css` trait with `scoped=True` always works, regardless of this setting:
65+
66+
```python
67+
widget = VueTemplate(
68+
template="<template><span class='x'>Hi</span></template>",
69+
css=".x { color: blue; }",
70+
scoped=True
71+
)
72+
```
73+
2974
Sponsors
3075
--------
3176

ipyvue/VueTemplateWidget.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from traitlets import Any, Bool, Unicode, List, Dict, Union, Instance
2+
from traitlets import Any, Bool, Unicode, List, Dict, Union, Instance, default
33
from ipywidgets import DOMWidget
44
from ipywidgets.widgets.widget import widget_serialization
55

@@ -8,6 +8,7 @@
88
from .ForceLoad import force_load_instance
99
import inspect
1010
from importlib import import_module
11+
import ipyvue
1112

1213
OBJECT_REF = "objectRef"
1314
FUNCTION_REF = "functionRef"
@@ -120,6 +121,12 @@ class VueTemplate(DOMWidget, Events):
120121

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

124+
scoped_css_support = Bool(allow_none=False).tag(sync=True)
125+
126+
@default("scoped_css_support")
127+
def _default_scoped_css_support(self):
128+
return ipyvue.scoped_css_support
129+
123130
methods = Unicode(None, allow_none=True).tag(sync=True)
124131

125132
data = Unicode(None, allow_none=True).tag(sync=True)

ipyvue/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
from ._version import __version__
24
from .Html import Html
35
from .Template import Template, watch
@@ -10,6 +12,22 @@
1012
)
1113

1214

15+
def _parse_bool_env(key: str, default: bool = False) -> bool:
16+
"""Parse boolean from environment variable."""
17+
val = os.environ.get(key, "").lower()
18+
if val in ("1", "true", "yes", "on"):
19+
return True
20+
if val in ("0", "false", "no", "off"):
21+
return False
22+
return default
23+
24+
25+
# Global default for scoped CSS support in VueTemplate.
26+
# Can be set via environment variable IPYVUE_SCOPED_CSS_SUPPORT=1
27+
# or changed at runtime: ipyvue.scoped_css_support = True
28+
scoped_css_support = _parse_bool_env("IPYVUE_SCOPED_CSS_SUPPORT", False)
29+
30+
1331
def _jupyter_labextension_paths():
1432
return [
1533
{

js/src/VueTemplateRenderer.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ function createComponentObject(model, parentView) {
101101
const cssId = (vuefile.STYLE && vuefile.STYLE.id);
102102
const scopedFromTemplate = (vuefile.STYLE && vuefile.STYLE.scoped);
103103
const scoped = model.get('scoped');
104-
const useScoped = scoped !== null && scoped !== undefined ? scoped : scopedFromTemplate;
104+
const scopedCssSupport = model.get('scoped_css_support');
105+
// If scoped trait is explicitly set, use it (for css trait with scoped=True/False)
106+
// If scoped is not set (None), only honor <style scoped> from template if scoped_css_support is enabled
107+
const useScoped = scoped !== null && scoped !== undefined
108+
? scoped
109+
: (scopedCssSupport && scopedFromTemplate);
105110
const scopeId = useScoped && css ? getScopeId(model, cssId) : null;
106111

107112
if (css) {
@@ -217,7 +222,7 @@ function createComponentObject(model, parentView) {
217222
function createDataMapping(model) {
218223
return model.keys()
219224
.filter(prop => !prop.startsWith('_')
220-
&& !['events', 'template', 'components', 'layout', 'css', 'scoped', 'data', 'methods'].includes(prop))
225+
&& !['events', 'template', 'components', 'layout', 'css', 'scoped', 'scoped_css_support', 'data', 'methods'].includes(prop))
221226
.reduce((result, prop) => {
222227
result[prop] = _.cloneDeep(model.get(prop)); // eslint-disable-line no-param-reassign
223228
return result;
@@ -227,7 +232,7 @@ function createDataMapping(model) {
227232
function addModelListeners(model, vueModel) {
228233
model.keys()
229234
.filter(prop => !prop.startsWith('_')
230-
&& !['v_model', 'components', 'layout', 'css', 'scoped', 'data', 'methods'].includes(prop))
235+
&& !['v_model', 'components', 'layout', 'css', 'scoped', 'scoped_css_support', 'data', 'methods'].includes(prop))
231236
// eslint-disable-next-line no-param-reassign
232237
.forEach(prop => model.on(`change:${prop}`, () => {
233238
if (_.isEqual(model.get(prop), vueModel[prop])) {
@@ -253,7 +258,7 @@ function addModelListeners(model, vueModel) {
253258

254259
function createWatches(model, parentView, templateWatchers) {
255260
const modelWatchers = model.keys().filter(prop => !prop.startsWith('_')
256-
&& !['events', 'template', 'components', 'layout', 'css', 'scoped', 'data', 'methods'].includes(prop))
261+
&& !['events', 'template', 'components', 'layout', 'css', 'scoped', 'scoped_css_support', 'data', 'methods'].includes(prop))
257262
.reduce((result, prop) => ({
258263
...result,
259264
[prop]: {

tests/ui/test_template.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def kernel_code():
167167
import ipywidgets as widgets
168168
from IPython.display import display
169169

170-
scoped = ScopedStyleTemplate()
170+
scoped = ScopedStyleTemplate(scoped_css_support=True)
171171
unscoped = vue.Html(
172172
tag="span",
173173
children=["Unscoped text"],

0 commit comments

Comments
 (0)