Skip to content

Commit

Permalink
feat: Vue3 support
Browse files Browse the repository at this point in the history
  • Loading branch information
mariobuikhuizen committed Oct 31, 2023
1 parent bb3dfca commit 67a41b4
Show file tree
Hide file tree
Showing 12 changed files with 4,720 additions and 14,681 deletions.
18,280 changes: 4,320 additions & 13,960 deletions js/package-lock.json

Large diffs are not rendered by default.

13 changes: 4 additions & 9 deletions js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,25 @@
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"@jupyterlab/builder": "^3",
"ajv": "^6.10.0",
"css-loader": "^5",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-vue": "^5.2.2",
"file-loader": "^6",
"npm-run-all": "^4.1.5",
"rimraf": "^2.6.3",
"style-loader": "^0.23.1",
"webpack": "^5",
"webpack-cli": "^4"
"webpack": "^5"
},
"dependencies": {
"@jupyter-widgets/base": "^1 || ^2 || ^3 || ^4",
"@mariobuikhuizen/vue-compiler-addon": "^2.6.10-alpha.2",
"@jupyter-widgets/base": "^1 || ^2 || ^3 || ^4 || ^6",
"core-js": "^3.0.1",
"lodash": "^4.17.11",
"uuid": "^3.4.0",
"vue": "^2.6.10"
"vue": "^3.3.4"
},
"jupyterlab": {
"extension": "lib/labplugin",
"outputDir": "../ipyvue/labextension",
"webpackConfig": "webpack.config.lab.js",
"sharedPackages": {
"@jupyter-widgets/base": {
"bundled": false,
Expand Down
34 changes: 30 additions & 4 deletions js/src/VueComponentModel.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
/* eslint camelcase: off */
import { DOMWidgetModel } from '@jupyter-widgets/base';
import Vue from 'vue';
import httpVueLoader from './httpVueLoader';
import {TemplateModel} from './Template';
import {getAsyncComponent} from "./esmVueTemplate";

const apps = new Set();

export function addApp(app, widget_manager) {
apps.add(app);
(async () => {
const models = await Promise.all(Object.values(widget_manager._models));
models
.filter(model => model instanceof VueComponentModel)
.forEach(model => {
const name = model.get('name');
app.component(name, model.compiledComponent);
})
})();
}

export function removeApp(app) {
apps.delete(app);
}

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

const name = this.get('name');
Vue.component(name, httpVueLoader(this.get('component')));

this.compiledComponent = getAsyncComponent(this.get('component'), {});

apps.forEach(app => {
app.component(name, this.compiledComponent);
});
this.on('change:component', () => {
Vue.component(name, httpVueLoader(this.get('component')));
this.compiledComponent = getAsyncComponent(this.get('component'), {});
apps.forEach(app => {
app.component(name, this.compiledComponent);
});

(async () => {
const models = await Promise.all(Object.values(widget_manager._models));
Expand Down
164 changes: 65 additions & 99 deletions js/src/VueRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as base from '@jupyter-widgets/base';
import { vueTemplateRender } from './VueTemplateRenderer'; // eslint-disable-line import/no-cycle
import { VueModel } from './VueModel';
import { VueTemplateModel } from './VueTemplateModel';
import Vue from './VueWithCompiler';
import * as Vue from 'vue';

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

Expand Down Expand Up @@ -45,8 +45,8 @@ export function createObjectForNestedModel(model, parentView) {
destroyed = true;
}
},
render(createElement) {
return createElement('div', { style: { height: '100%' } });
render() {
return Vue.h('div', { style: { height: '100%' } });
},
};
}
Expand Down Expand Up @@ -81,16 +81,26 @@ export function eventToObject(event) {
return event;
}

export function vueRender(createElement, model, parentView, slotScopes) {
function resolve(componentOrTag) {
try {
return Vue.resolveComponent(componentOrTag);
} catch (e) {
return componentOrTag;
}
}

export function vueRender(model, parentView, slotScopes) {
if (model instanceof VueTemplateModel) {
return vueTemplateRender(createElement, model, parentView);
return vueTemplateRender(model, parentView);
}
if (!(model instanceof VueModel)) {
return createElement(createObjectForNestedModel(model, parentView));
return Vue.h(createObjectForNestedModel(model, parentView));
}
const tag = model.getVueTag();

const elem = createElement({
const childCache = {};

const elem = Vue.h({
data() {
return {
v_model: model.get('v_model'),
Expand All @@ -99,20 +109,23 @@ export function vueRender(createElement, model, parentView, slotScopes) {
created() {
addListeners(model, this);
},
render(createElement2) {
const element = createElement2(
tag,
createContent(createElement2, model, this, parentView, slotScopes),
renderChildren(createElement2, model.get('children'), this, parentView, slotScopes),
render() {
const element = Vue.h(
resolve(tag),
createContent(model, this, parentView, slotScopes),
{
default: () => {
updateCache(childCache, (model.get('children') || []).map(m => m.cid));
return renderChildren(model.get('children'), childCache, parentView, slotScopes);
},
...createSlots(model, this, parentView, slotScopes)
},
);
updateCache(this);

return element;
},
}, { ...model.get('slot') && { slot: model.get('slot') } });

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

Expand Down Expand Up @@ -147,33 +160,11 @@ function createAttrsMapping(model) {
}

function addEventWithModifiers(eventAndModifiers, obj, fn) { // eslint-disable-line no-unused-vars
/* Example Vue.compile output:
* (function anonymous() {
* with (this) {
* return _c('dummy', {
* on: {
* "[event]": function ($event) {
* if (!$event.type.indexOf('key') && _k($event.keyCode, "c", ...)
* return null;
* ...
* return [fn]($event)
* }
* }
* })
* }
* }
* )
*/
const { on } = Vue.compile(`<dummy @${eventAndModifiers}="fn"></dummy>`)
.render.bind({
_c: (_, data) => data,
_k: Vue.prototype._k,
fn,
})();
const [event, ...mods] = eventAndModifiers.split(".");

return {
...obj,
...on,
[`on${event.charAt(0).toUpperCase()}${event.slice(1)}`]: Vue.withModifiers(fn, mods),
};
}

Expand All @@ -192,104 +183,79 @@ function createEventMapping(model, parentView) {
), {});
}

function createSlots(createElement, model, vueModel, parentView, slotScopes) {
function createSlots(model, vueModel, parentView, slotScopes) {
const slots = model.get('v_slots');
if (!slots) {
return undefined;
}
return slots.map(slot => ({
key: slot.name,
...!slot.variable && { proxy: true },
fn(slotScope) {
return renderChildren(createElement,
const childCache = {};

return slots.reduce((res, slot) => ({
...res,
[slot.name]: (slotScope) => {
return renderChildren(
Array.isArray(slot.children) ? slot.children : [slot.children],
vueModel, parentView, {
childCache, parentView, {
...slotScopes,
...slot.variable && { [slot.variable]: slotScope },
});
},
}));
}

function getScope(value, slotScopes) {
const parts = value.split('.');
return parts
.slice(1)
.reduce(
(scope, name) => scope[name],
slotScopes[parts[0]],
);
}

function getScopes(value, slotScopes) {
return typeof value === 'string'
? getScope(value, slotScopes)
: Object.assign({}, ...value.map(v => getScope(v, slotScopes)));
}), {});
}

function slotUseOn(model, slotScopes) {
const vOnValue = model.get('v_on');
return vOnValue && getScopes(vOnValue, slotScopes);
return vOnValue && filterObject(slotScopes[vOnValue.split('.')[0]].props, (key, value) => key.startsWith('on'))
}

function filterObject(obj, predicate) {
return Object.entries(obj)
.filter(([key, value]) => predicate(key, value))
.reduce((res, [key, value]) => ({...res, [key]: value }), {});
}

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

const scopedSlots = createSlots(createElement, model, vueModel, parentView, slotScopes);

return {
on: { ...createEventMapping(model, parentView), ...slotUseOn(model, slotScopes) },
...slotUseOn(model, slotScopes),
...createEventMapping(model, parentView),
...model.get('style_') && { style: model.get('style_') },
...model.get('class_') && { class: model.get('class_') },
...scopedSlots && { scopedSlots: vueModel._u(scopedSlots) },
attrs: {
...createAttrsMapping(model),
...model.get('attributes') && model.get('attributes'),
},
...createAttrsMapping(model),
...model.get('attributes') && model.get('attributes'),
...model.get('v_model') !== '!!disabled!!' && {
model: {
value: vueModel.v_model,
callback: (v) => {
model.set('v_model', v === undefined ? null : v);
model.save_changes(model.callbacks(parentView));
},
expression: 'v_model',
modelValue: vueModel.v_model,
"onUpdate:modelValue": (v) => {
model.set('v_model', v === undefined ? null : v);
model.save_changes(model.callbacks(parentView));
},
},
};
}

function renderChildren(createElement, children, vueModel, parentView, slotScopes) {
if (!vueModel.childCache) {
vueModel.childCache = {}; // eslint-disable-line no-param-reassign
}
if (!vueModel.childIds) {
vueModel.childIds = []; // eslint-disable-line no-param-reassign
}
function renderChildren(children, childCache, parentView, slotScopes) {
const childViewModels = children.map((child) => {
if (typeof (child) === 'string') {
return child;
}
vueModel.childIds.push(child.cid);

if (vueModel.childCache[child.cid]) {
return vueModel.childCache[child.cid];
if (childCache[child.cid]) {
return childCache[child.cid];
}
const vm = vueRender(createElement, child, parentView, slotScopes);
vueModel.childCache[child.cid] = vm; // eslint-disable-line no-param-reassign
const vm = vueRender(child, parentView, slotScopes);
childCache[child.cid] = vm; // eslint-disable-line no-param-reassign
return vm;
});

return childViewModels;
}

function updateCache(vueModel) {
Object.keys(vueModel.childCache)
.filter(key => !vueModel.childIds.includes(key))
function updateCache(childCache, usedChildIds) {
Object.keys(childCache)
.filter(key => !usedChildIds.includes(key))
// eslint-disable-next-line no-param-reassign
.forEach(key => delete vueModel.childCache[key]);
vueModel.childIds = []; // eslint-disable-line no-param-reassign
.forEach(key => delete childCache[key]);
}
Loading

0 comments on commit 67a41b4

Please sign in to comment.