You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在JupyterLab中免打包实现ipywidgets7风格的自定义复选框表格组件

解决JupyterLab中ipywidgets8免打包自定义组件问题

问题背景

之前在经典Jupyter Notebook中用ipywidgets7的免打包方式写了一个复选框表格组件,现在要迁移到JupyterLab+ipywidgets8环境,找不到直接插入前端JS的方法,不想为小功能做完整打包,尝试注入jQuery和RequireJS后出现Error displaying widget: model not found错误。

原前端代码

require.undef("my_widgets");
define("my_widgets", ["@jupyter-widgets/base"], function(widgets) {

  var TableView = widgets.DOMWidgetView.extend({
    // Render the view.
    render: function() {
      this.table_changed();

      // Python -> JavaScript update
      this.model.on("change:table", this.table_changed, this);

      // JavaScript -> Python update
      this.el.onchange = this.input_changed.bind(this);

      // Script
      $(this.el).on("click", ".my-table tbody tr", function (event) {
        if (event.target.type != 'checkbox'){
          $(this).find(".my-chk")[0].click()
        }
      });
    },

    table_changed: function() {
      this.el.innerHTML = this.model.get("table");
    },

    input_changed: function() {
      let value = [];
      let jcheck = $(this.el).find(".my-chk");
      for (let i=0;i<jcheck.length;i++) {
        if (jcheck[i].checked) {
          value.push(i)
        }
      };
      $(this.el).find(".my-table")[0].setAttribute("date-value", value.join());
      this.model.set('value', value);
      this.model.save_changes();
    },
  });
 
  return {
    TableView: TableView,
  };
});

原后端代码

import traitlets
from jinja2 import Template
from ipywidgets import Layout, DOMWidget, register

@register
class TaskTable(DOMWidget):
    _view_name = traitlets.Unicode('TableView').tag(sync=True)
    _view_module = traitlets.Unicode('my_widgets').tag(sync=True)

    value = traitlets.List([], help="selected").tag(sync=True)
    table = traitlets.Unicode('', help="rendered HTML table").tag(sync=True)

    @traitlets.validate('value')
    def _valid_value(self, proposal):
        proposal['value'] = [int(x) for x in proposal['value']]
        return proposal['value']

    def render(self, head, body):
        thead = ['#'] + head
        tbody = body
        self.table = template.render(head=thead, body=tbody)
        
template = Template(STATICS_TaskTable)

STATICS_TaskTable = """
<table class="my-table">
<thead>
<tr>
  {%- for th in head -%}
  <th>{{ th }}</th>
  {%- endfor %}
</tr>
</thead>
<tbody>
  {%- for row in body -%}
  <tr>
    {%- for cell in row -%}
      {%- if loop.first -%}
        <th><input class="my-chk" type="checkbox"> {{ cell }}</th>
      {%- else -%}
        <td>{{ cell }}</td>
      {%- endif -%}
    {%- endfor -%}
  </tr>
  {%- endfor -%}
</tbody>
</table>
"""

w = TaskTable()
w.render(['foo', 'bar'], [[1, 'A', 'S'], [2, 'D', 'F']])
w

解决方案

1. 调整前端代码注入方式

JupyterLab使用ES模块,需通过全局requirejs对象注册模块,同时确保jQuery加载完成:

在JupyterLab的代码单元格中运行以下JS(用%%javascript魔法命令):

%%javascript
// 确保jQuery可用
if (typeof $ === 'undefined') {
    require(['jquery'], function(jq) {
        $ = jq;
        registerWidget();
    });
} else {
    registerWidget();
}

function registerWidget() {
    requirejs.undef("my_widgets");
    define("my_widgets", ["@jupyter-widgets/base"], function(widgets) {
        var TableView = widgets.DOMWidgetView.extend({
            render: function() {
                this.table_changed();
                this.model.on("change:table", this.table_changed, this);
                this.el.onchange = this.input_changed.bind(this);

                $(this.el).on("click", ".my-table tbody tr", function (event) {
                    if (event.target.type !== 'checkbox'){
                        $(this).find(".my-chk")[0].click();
                    }
                });
            },

            table_changed: function() {
                this.el.innerHTML = this.model.get("table");
            },

            input_changed: function() {
                let value = [];
                let jcheck = $(this.el).find(".my-chk");
                for (let i=0;i<jcheck.length;i++) {
                    if (jcheck[i].checked) {
                        value.push(i);
                    }
                }
                $(this.el).find(".my-table")[0].setAttribute("data-value", value.join());
                this.model.set('value', value);
                this.model.save_changes();
            },
        });
     
        return {
            TableView: TableView,
        };
    });
}

注:修正了原代码中date-value的拼写错误为符合规范的data-value。

2. 修正后端代码的变量顺序与属性

原代码中模板定义顺序错误,且缺少ipywidgets8要求的版本属性,调整如下:

import traitlets
from jinja2 import Template
from ipywidgets import Layout, DOMWidget, register

STATICS_TaskTable = """
<table class="my-table">
<thead>
<tr>
  {%- for th in head -%}
  <th>{{ th }}</th>
  {%- endfor %}
</tr>
</thead>
<tbody>
  {%- for row in body -%}
  <tr>
    {%- for cell in row -%}
      {%- if loop.first -%}
        <th><input class="my-chk" type="checkbox"> {{ cell }}</th>
      {%- else -%}
        <td>{{ cell }}</td>
      {%- endif -%}
    {%- endfor -%}
  </tr>
  {%- endfor -%}
</tbody>
</table>
"""

template = Template(STATICS_TaskTable)

@register
class TaskTable(DOMWidget):
    _view_name = traitlets.Unicode('TableView').tag(sync=True)
    _view_module = traitlets.Unicode('my_widgets').tag(sync=True)
    # 新增版本属性,避免ipywidgets8的版本匹配报错
    _view_module_version = traitlets.Unicode('1.0.0').tag(sync=True)

    value = traitlets.List([], help="selected").tag(sync=True)
    table = traitlets.Unicode('', help="rendered HTML table").tag(sync=True)

    @traitlets.validate('value')
    def _valid_value(self, proposal):
        proposal['value'] = [int(x) for x in proposal['value']]
        return proposal['value']

    def render(self, head, body):
        thead = ['#'] + head
        tbody = body
        self.table = template.render(head=thead, body=tbody)

# 创建并渲染组件
w = TaskTable()
w.render(['foo', 'bar'], [[1, 'A', 'S'], [2, 'D', 'F']])
w

3. 确保JupyterLab扩展安装完整

执行以下命令安装依赖扩展并重启JupyterLab:

pip install ipywidgets jupyterlab-widgets
jupyter labextension install @jupyter-widgets/jupyterlab-manager

关键说明

  • ipywidgets8在JupyterLab中仍支持通过全局requirejs注册模块,无需完整打包
  • 必须确保依赖(如jQuery)加载完成后再注册组件
  • 新增_view_module_version是ipywidgets8的强制要求,可避免模型找不到的错误

内容的提问来源于stack exchange,提问作者DarkGoldBar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 17:47:18