8000 Add ability to require explicit load of ReactiveHTML extension by philippjfr · Pull Request #3254 · holoviz/panel · GitHub
[go: up one dir, main page]

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
14 changes: 12 additions & 2 deletions examples/user_guide/Custom_Components.ipynb
Original file line number Diff line 8000 number Diff line change
Expand Up @@ -474,7 +474,7 @@
"source": [
"## External dependencies\n",
"\n",
"Often you will want to wrap a component with some external Javascript or CSS dependencies. To make this possible `ReactiveHTML` components may declare `__javascript__`, `__javascript_modules__` and `__css__` attributes, specifying the external dependencies to load. Note that in a notebook the component must be declared before `pn.extension` is called, otherwise the libraries won't be loaded (in this case we explicitly loaded them above).\n",
"Often the components you build will have dependencies on some external Javascript or CSS files. To make this possible `ReactiveHTML` components may declare `__javascript__`, `__javascript_modules__` and `__css__` attributes, specifying the external dependencies to load. Note that in a notebook as long as the component is imported before the call to `pn.extension` all its dependencies will be loaded automatically. If you want to require users to load the components as an extension explicitly via a `pn.extension` call you can declare an `_extension_name`.\n",
"\n",
"Below we will create a Material UI text field and declare the Javascript and CSS components to load:"
]
Expand All @@ -499,7 +499,10 @@
" \"\"\"\n",
" \n",
" _dom_events = {'text-input': ['change']}\n",
" \n",
"\n",
" # By declaring an _extension_name the component should be loaded explicitly with pn.extension('material-components')\n",
" _extension_name = 'material-components'\n",
"\n",
" _scripts = {\n",
" 'render': \"mdc.textField.MDCTextField.attachTo(text_field);\"\n",
" }\n",
Expand All @@ -517,6 +520,13 @@
"text_field"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In a notebook dependencies for this component will not be loaded unless the user explicitly loads them with a `pn.extension('material-components')`. In a server context you will also have to explicitly load this extension unless the component is rendered on initial page load, i.e. if the component is only added to the page in a callback you will also have to explicitly run `pn.extension('material-components')`."
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
15 changes: 10 additions & 5 deletions panel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,6 @@ class panel_extension(_pyviz_extension):
'perspective': 'panel.models.perspective',
'terminal': 'panel.models.terminal',
'tabulator': 'panel.models.tabulator',
'gridstack': 'panel.layout.gridstack',
'texteditor': 'panel.models.quill',
'jsoneditor': 'panel.models.json_editor'
}
Expand All @@ -463,13 +462,18 @@ class panel_extension(_pyviz_extension):
_loaded_extensions = []

def __call__(self, *args, **params):
# Abort if IPython not found
from .reactive import ReactiveHTML, ReactiveHTMLMetaclass
reactive_exts = {
v._extension_name: v for k, v in param.concrete_descendents(ReactiveHTML).items()
}
for arg in args:
if arg not in self._imports:
if arg in self._imports:
__import__(self._imports[arg])
elif arg in reactive_exts:
ReactiveHTMLMetaclass._loaded_extensions.add(arg)
else:
self.param.warning('%s extension not recognized and '
'will be skipped.' % arg)
else:
__import__(self._imports[arg])

for k, v in params.items():
if k in ['raw_css', 'css_files']:
Expand Down Expand Up @@ -509,6 +513,7 @@ def __call__(self, *args, **params):
else:
hv.Store.current_backend = backend

# Abort if IPython not found
try:
ip = params.pop('ip', None) or get_ipython() # noqa (get_ipython)
except Exception:
Expand Down
8 changes: 4 additions & 4 deletions panel/io/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def js_files(self):
files = super(Resources, self).js_files

for model in param.concrete_descendents(ReactiveHTML).values():
if hasattr(model, '__javascript__'):
if getattr(model, '__javascript__', None) and model._loaded():
for jsfile in model.__javascript__:
if jsfile not in files:
files.append(jsfile)
Expand Down Expand Up @@ -245,7 +245,7 @@ def js_modules(self):
from ..reactive import ReactiveHTML
modules = list(config.js_modules.values())
for model in param.concrete_descendents(ReactiveHTML).values():
if hasattr(model, '__javascript_modules__'):
if hasattr(model, '__javascript_modules__') and model._loaded():
for jsmodule in model.__javascript_modules__:
if jsmodule not in modules:
modules.append(jsmodule)
Expand All @@ -259,7 +259,7 @@ def css_files(self):
files = super(Resources, self).css_files

for model in param.concrete_descendents(ReactiveHTML).values():
if hasattr(model, '__css__'):
if getattr(model, '__css__', None) and model._loaded():
for css_file in model.__css__:
if css_file not in files:
files.append(css_file)
Expand Down Expand Up @@ -296,7 +296,7 @@ def __init__(self, **kwargs):
from ..reactive import ReactiveHTML
js_modules = list(config.js_modules.values())
for model in param.concrete_descendents(ReactiveHTML).values():
if hasattr(model, '__javascript_modules__'):
if getattr(model, '__javascript_modules__', None) and model._loaded():
for js_module in model.__javascript_modules__:
if js_module not in js_modules:
js_modules.append(js_module)
Expand Down
1 change: 1 addition & 0 deletions panel/layout/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
from .card import Card # noqa
from .flex import FlexBox # noqa
from .grid import GridBox, GridSpec # noqa
from .gridstack import GridStack # noqa
from .spacer import Divider, HSpacer, Spacer, VSpacer # noqa
from .tabs import Tabs # noqa
2 changes: 2 additions & 0 deletions panel/layout/gridstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class GridStack(ReactiveHTML, GridSpec):

height = param.Integer(default=None)

_extension_name = 'gridstack'

_template = """
<div id="grid" class="grid-stack">
{% for key, obj in objects.items() %}
Expand Down
36 changes: 36 additions & 0 deletions panel/reactive.py
493A
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,8 @@ class ReactiveHTMLMetaclass(ParameterizedMetaclass):
HTML attributes.
"""

_loaded_extensions = set()

_name_counter = Counter()

_script_regex = r"script\([\"|'](.*)[\"|']\)"
Expand Down Expand Up @@ -1291,6 +1293,8 @@ class ReactiveHTML(Reactive, metaclass=ReactiveHTMLMetaclass):

_dom_events = {}

_extension_name = None

_template = ""

_scripts = {}
Expand All @@ -1299,6 +1303,10 @@ class ReactiveHTML(Reactive, metaclass=ReactiveHTMLMetaclass):

__abstract = True

__css__ = None
__javascript__ = None
__javascript_modules__ = None

def __init__(self, **params):
from .pane import panel
for children_param in self._parser.children.values():
Expand Down Expand Up @@ -1327,6 +1335,16 @@ def __init__(self, **params):
self._panes = {}
self._event_callbacks = defaultdict(lambda: defaultdict(list))

@classmethod
def _loaded(cls):
"""
Whether the component has been loaded.
"""
return (
cls._extension_name is None or
cls._extension_name in ReactiveHTMLMetaclass._loaded_extensions
)

def _cleanup(self, root):
for child, panes in self._panes.items():
for pane in panes:
Expand Down Expand Up @@ -1546,8 +1564,26 @@ def _linked_properties(self):
def _get_model(self, doc, root=None, parent=None, comm=None):
properties = self._process_param_change(self._init_params())
model = _BkReactiveHTML(**properties)
if comm and not self._loaded():
self.param.warning(
f'{type(self).__name__} was not imported on instantiation and may not '
'render in a notebook. Restart the notebook kernel and '
'ensure you load it as part of the extension using:'
f'\n\npn.extension(\'{self._extension_name}\')\n'
)
elif root is not None and not self._loaded() and root.ref['id'] in state._views:
self.param.warning(
f'{type(self).__name__} was not imported on instantiation may not '
'render in the served application. Ensure you add the '
'following to the top of your application:'
f'\n\npn.extension(\'{self._extension_name}\')\n'
)
if self._extension_name:
ReactiveHTMLMetaclass._loaded_extensions.add(self._extension_name)

if not root:
root = model

for p, v in model.data.properties_with_values().items():
if isinstance(v, DataModel):
v.tags.append(f"__ref:{root.ref['id']}")
Expand Down
0