Skip to content

Commit 8872244

Browse files
feat: Vue3 support
1 parent 02ac363 commit 8872244

13 files changed

+4722
-14683
lines changed

.github/workflows/unit.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Install node
3030
uses: actions/setup-node@v1
3131
with:
32-
node-version: "14.x"
32+
node-version: "18.x"
3333
registry-url: "https://registry.npmjs.org"
3434

3535
- name: Install Python
@@ -144,7 +144,7 @@ jobs:
144144
- name: Install node
145145
uses: actions/setup-node@v1
146146
with:
147-
node-version: "14.x"
147+
node-version: "18.x"
148148
registry-url: "https://registry.npmjs.org"
149149

150150
- name: Install Python

js/package-lock.json

+4,320-13,960
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/package.json

+4-9
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,25 @@
3939
"@babel/core": "^7.4.4",
4040
"@babel/preset-env": "^7.4.4",
4141
"@jupyterlab/builder": "^3",
42-
"ajv": "^6.10.0",
43-
"css-loader": "^5",
4442
"eslint": "^5.16.0",
4543
"eslint-config-airbnb-base": "^13.1.0",
4644
"eslint-plugin-import": "^2.17.2",
4745
"eslint-plugin-vue": "^5.2.2",
48-
"file-loader": "^6",
4946
"npm-run-all": "^4.1.5",
5047
"rimraf": "^2.6.3",
51-
"style-loader": "^0.23.1",
52-
"webpack": "^5",
53-
"webpack-cli": "^4"
48+
"webpack": "^5"
5449
},
5550
"dependencies": {
56-
"@jupyter-widgets/base": "^1 || ^2 || ^3 || ^4",
57-
"@mariobuikhuizen/vue-compiler-addon": "^2.6.10-alpha.2",
51+
"@jupyter-widgets/base": "^1 || ^2 || ^3 || ^4 || ^6",
5852
"core-js": "^3.0.1",
5953
"lodash": "^4.17.11",
6054
"uuid": "^3.4.0",
61-
"vue": "^2.6.10"
55+
"vue": "^3.3.4"
6256
},
6357
"jupyterlab": {
6458
"extension": "lib/labplugin",
6559
"outputDir": "../ipyvue/labextension",
60+
"webpackConfig": "webpack.config.lab.js",
6661
"sharedPackages": {
6762
"@jupyter-widgets/base": {
6863
"bundled": false,

js/src/VueComponentModel.js

+30-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
/* eslint camelcase: off */
22
import { DOMWidgetModel } from '@jupyter-widgets/base';
3-
import Vue from 'vue';
4-
import httpVueLoader from './httpVueLoader';
53
import {TemplateModel} from './Template';
4+
import {getAsyncComponent} from "./esmVueTemplate";
5+
6+
const apps = new Set();
7+
8+
export function addApp(app, widget_manager) {
9+
apps.add(app);
10+
(async () => {
11+
const models = await Promise.all(Object.values(widget_manager._models));
12+
models
13+
.filter(model => model instanceof VueComponentModel)
14+
.forEach(model => {
15+
const name = model.get('name');
16+
app.component(name, model.compiledComponent);
17+
})
18+
})();
19+
}
20+
21+
export function removeApp(app) {
22+
apps.delete(app);
23+
}
624

725
export class VueComponentModel extends DOMWidgetModel {
826
defaults() {
@@ -24,9 +42,17 @@ export class VueComponentModel extends DOMWidgetModel {
2442
const [, { widget_manager }] = args;
2543

2644
const name = this.get('name');
27-
Vue.component(name, httpVueLoader(this.get('component')));
45+
46+
this.compiledComponent = getAsyncComponent(this.get('component'), {});
47+
48+
apps.forEach(app => {
49+
app.component(name, this.compiledComponent);
50+
});
2851
this.on('change:component', () => {
29-
Vue.component(name, httpVueLoader(this.get('component')));
52+
this.compiledComponent = getAsyncComponent(this.get('component'), {});
53+
apps.forEach(app => {
54+
app.component(name, this.compiledComponent);
55+
});
3056

3157
(async () => {
3258
const models = await Promise.all(Object.values(widget_manager._models));

js/src/VueRenderer.js

+65-99
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as base from '@jupyter-widgets/base';
33
import { vueTemplateRender } from './VueTemplateRenderer'; // eslint-disable-line import/no-cycle
44
import { VueModel } from './VueModel';
55
import { VueTemplateModel } from './VueTemplateModel';
6-
import Vue from './VueWithCompiler';
6+
import * as Vue from 'vue';
77

88
const JupyterPhosphorWidget = base.JupyterPhosphorWidget || base.JupyterLuminoWidget;
99

@@ -45,8 +45,8 @@ export function createObjectForNestedModel(model, parentView) {
4545
destroyed = true;
4646
}
4747
},
48-
render(createElement) {
49-
return createElement('div', { style: { height: '100%' } });
48+
render() {
49+
return Vue.h('div', { style: { height: '100%' } });
5050
},
5151
};
5252
}
@@ -81,16 +81,26 @@ export function eventToObject(event) {
8181
return event;
8282
}
8383

84-
export function vueRender(createElement, model, parentView, slotScopes) {
84+
function resolve(componentOrTag) {
85+
try {
86+
return Vue.resolveComponent(componentOrTag);
87+
} catch (e) {
88+
return componentOrTag;
89+
}
90+
}
91+
92+
export function vueRender(model, parentView, slotScopes) {
8593
if (model instanceof VueTemplateModel) {
86-
return vueTemplateRender(createElement, model, parentView);
94+
return vueTemplateRender(model, parentView);
8795
}
8896
if (!(model instanceof VueModel)) {
89-
return createElement(createObjectForNestedModel(model, parentView));
97+
return Vue.h(createObjectForNestedModel(model, parentView));
9098
}
9199
const tag = model.getVueTag();
92100

93-
const elem = createElement({
101+
const childCache = {};
102+
103+
const elem = Vue.h({
94104
data() {
95105
return {
96106
v_model: model.get('v_model'),
@@ -99,20 +109,23 @@ export function vueRender(createElement, model, parentView, slotScopes) {
99109
created() {
100110
addListeners(model, this);
101111
},
102-
render(createElement2) {
103-
const element = createElement2(
104-
tag,
105-
createContent(createElement2, model, this, parentView, slotScopes),
106-
renderChildren(createElement2, model.get('children'), this, parentView, slotScopes),
112+
render() {
113+
const element = Vue.h(
114+
resolve(tag),
115+
createContent(model, this, parentView, slotScopes),
116+
{
117+
default: () => {
118+
updateCache(childCache, (model.get('children') || []).map(m => m.cid));
119+
return renderChildren(model.get('children'), childCache, parentView, slotScopes);
120+
},
121+
...createSlots(model, this, parentView, slotScopes)
122+
},
107123
);
108-
updateCache(this);
124+
109125
return element;
110126
},
111127
}, { ...model.get('slot') && { slot: model.get('slot') } });
112128

113-
/* Impersonate the wrapped component (e.g. v-tabs uses this name to detect v-tab and
114-
* v-tab-item) */
115-
elem.componentOptions.Ctor.options.name = tag;
116129
return elem;
117130
}
118131

@@ -147,33 +160,11 @@ function createAttrsMapping(model) {
147160
}
148161

149162
function addEventWithModifiers(eventAndModifiers, obj, fn) { // eslint-disable-line no-unused-vars
150-
/* Example Vue.compile output:
151-
* (function anonymous() {
152-
* with (this) {
153-
* return _c('dummy', {
154-
* on: {
155-
* "[event]": function ($event) {
156-
* if (!$event.type.indexOf('key') && _k($event.keyCode, "c", ...)
157-
* return null;
158-
* ...
159-
* return [fn]($event)
160-
* }
161-
* }
162-
* })
163-
* }
164-
* }
165-
* )
166-
*/
167-
const { on } = Vue.compile(`<dummy @${eventAndModifiers}="fn"></dummy>`)
168-
.render.bind({
169-
_c: (_, data) => data,
170-
_k: Vue.prototype._k,
171-
fn,
172-
})();
163+
const [event, ...mods] = eventAndModifiers.split(".");
173164

174165
return {
175166
...obj,
176-
...on,
167+
[`on${event.charAt(0).toUpperCase()}${event.slice(1)}`]: Vue.withModifiers(fn, mods),
177168
};
178169
}
179170

@@ -192,104 +183,79 @@ function createEventMapping(model, parentView) {
192183
), {});
193184
}
194185

195-
function createSlots(createElement, model, vueModel, parentView, slotScopes) {
186+
function createSlots(model, vueModel, parentView, slotScopes) {
196187
const slots = model.get('v_slots');
197188
if (!slots) {
198189
return undefined;
199190
}
200-
return slots.map(slot => ({
201-
key: slot.name,
202-
...!slot.variable && { proxy: true },
203-
fn(slotScope) {
204-
return renderChildren(createElement,
191+
const childCache = {};
192+
193+
return slots.reduce((res, slot) => ({
194+
...res,
195+
[slot.name]: (slotScope) => {
196+
return renderChildren(
205197
Array.isArray(slot.children) ? slot.children : [slot.children],
206-
vueModel, parentView, {
198+
childCache, parentView, {
207199
...slotScopes,
208200
...slot.variable && { [slot.variable]: slotScope },
209201
});
210202
},
211-
}));
212-
}
213-
214-
function getScope(value, slotScopes) {
215-
const parts = value.split('.');
216-
return parts
217-
.slice(1)
218-
.reduce(
219-
(scope, name) => scope[name],
220-
slotScopes[parts[0]],
221-
);
222-
}
223-
224-
function getScopes(value, slotScopes) {
225-
return typeof value === 'string'
226-
? getScope(value, slotScopes)
227-
: Object.assign({}, ...value.map(v => getScope(v, slotScopes)));
203+
}), {});
228204
}
229205

230206
function slotUseOn(model, slotScopes) {
231207
const vOnValue = model.get('v_on');
232-
return vOnValue && getScopes(vOnValue, slotScopes);
208+
return vOnValue && filterObject(slotScopes[vOnValue.split('.')[0]].props, (key, value) => key.startsWith('on'))
209+
}
210+
211+
function filterObject(obj, predicate) {
212+
return Object.entries(obj)
213+
.filter(([key, value]) => predicate(key, value))
214+
.reduce((res, [key, value]) => ({...res, [key]: value }), {});
233215
}
234216

235-
function createContent(createElement, model, vueModel, parentView, slotScopes) {
217+
function createContent(model, vueModel, parentView, slotScopes) {
236218
const htmlEventAttributes = model.get('attributes') && Object.keys(model.get('attributes')).filter(key => key.startsWith('on'));
237219
if (htmlEventAttributes && htmlEventAttributes.length > 0) {
238220
throw new Error(`No HTML event attributes may be used: ${htmlEventAttributes}`);
239221
}
240222

241-
const scopedSlots = createSlots(createElement, model, vueModel, parentView, slotScopes);
242-
243223
return {
244-
on: { ...createEventMapping(model, parentView), ...slotUseOn(model, slotScopes) },
224+
...slotUseOn(model, slotScopes),
225+
...createEventMapping(model, parentView),
245226
...model.get('style_') && { style: model.get('style_') },
246227
...model.get('class_') && { class: model.get('class_') },
247-
...scopedSlots && { scopedSlots: vueModel._u(scopedSlots) },
248-
attrs: {
249-
...createAttrsMapping(model),
250-
...model.get('attributes') && model.get('attributes'),
251-
},
228+
...createAttrsMapping(model),
229+
...model.get('attributes') && model.get('attributes'),
252230
...model.get('v_model') !== '!!disabled!!' && {
253-
model: {
254-
value: vueModel.v_model,
255-
callback: (v) => {
256-
model.set('v_model', v === undefined ? null : v);
257-
model.save_changes(model.callbacks(parentView));
258-
},
259-
expression: 'v_model',
231+
modelValue: vueModel.v_model,
232+
"onUpdate:modelValue": (v) => {
233+
model.set('v_model', v === undefined ? null : v);
234+
model.save_changes(model.callbacks(parentView));
260235
},
261236
},
262237
};
263238
}
264239

265-
function renderChildren(createElement, children, vueModel, parentView, slotScopes) {
266-
if (!vueModel.childCache) {
267-
vueModel.childCache = {}; // eslint-disable-line no-param-reassign
268-
}
269-
if (!vueModel.childIds) {
270-
vueModel.childIds = []; // eslint-disable-line no-param-reassign
271-
}
240+
function renderChildren(children, childCache, parentView, slotScopes) {
272241
const childViewModels = children.map((child) => {
273242
if (typeof (child) === 'string') {
274243
return child;
275244
}
276-
vueModel.childIds.push(child.cid);
277-
278-
if (vueModel.childCache[child.cid]) {
279-
return vueModel.childCache[child.cid];
245+
if (childCache[child.cid]) {
246+
return childCache[child.cid];
280247
}
281-
const vm = vueRender(createElement, child, parentView, slotScopes);
282-
vueModel.childCache[child.cid] = vm; // eslint-disable-line no-param-reassign
248+
const vm = vueRender(child, parentView, slotScopes);
249+
childCache[child.cid] = vm; // eslint-disable-line no-param-reassign
283250
return vm;
284251
});
285252

286253
return childViewModels;
287254
}
288255

289-
function updateCache(vueModel) {
290-
Object.keys(vueModel.childCache)
291-
.filter(key => !vueModel.childIds.includes(key))
256+
function updateCache(childCache, usedChildIds) {
257+
Object.keys(childCache)
258+
.filter(key => !usedChildIds.includes(key))
292259
// eslint-disable-next-line no-param-reassign
293-
.forEach(key => delete vueModel.childCache[key]);
294-
vueModel.childIds = []; // eslint-disable-line no-param-reassign
260+
.forEach(key => delete childCache[key]);
295261
}

0 commit comments

Comments
 (0)