widgetti / ipyvuetify

Jupyter widgets based on vuetify UI components
MIT License
343 stars 59 forks source link

possible to use ipyvuetify with vuetify-jsonschema-form ? #182

Open jgunstone opened 3 years ago

jgunstone commented 3 years ago

I recently came across this: https://github.com/koumoul-dev/vuetify-jsonschema-form which generates a UI from a JSON schema.

I was wondering whether it might be possible to use this with ipyvuetify... maybe using the JSON schema to create a vuetify template?

cheers

mariobuikhuizen commented 2 years ago

That looks like a great library!

I tried to load it from CDN, and it works!

in try_vjsf.vue:

<template>
  <div>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@koumoul/vjsf@latest/dist/main.css">
    <div v-if="vjsf_loaded">
      <v-form v-model="valid">
        <v-jsf v-model="form_data" :schema="schema"></v-jsf>
      </v-form>
    </div>
  </div>    
</template>

<script>
module.exports = {
    async created() {
        const [VJsf] = await this.import(['https://cdn.jsdelivr.net/npm/@koumoul/vjsf@latest/dist/main.js']);
        this.$options.components['v-jsf'] = VJsf.default;
        this.vjsf_loaded = true;
    },
    methods: {
        import(deps) {
          return this.loadRequire()
              .then(() => new Promise((resolve, reject) => {
                requirejs(deps, (...modules) => resolve(modules));
              }));
        },
        loadRequire() {
          /* Needed in lab */
          if (window.requirejs) {
              console.log('require found');
              return Promise.resolve()
          }
          return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js';
            script.onload = resolve;
            script.onerror = reject;
            document.head.appendChild(script);
          });
        }
    }
}
</script>

In a notebook:

# uncomment next lines to enable hot reloading of vue template(s). (needs the watchdog package)
# import ipyvue
# ipyvue.watch('.')

import ipyvuetify as v
import traitlets

class TryVjsf(v.VuetifyTemplate):
    template_file = "try_vjsf.vue"

    vjsf_loaded = traitlets.Bool(False).tag(sync=True)
    form_data = traitlets.Dict(default_value={}).tag(sync=True)
    schema = traitlets.Dict().tag(sync=True)
    valid = traitlets.Bool(False).tag(sync=True)

schema = {
  "type": "object",
  "properties": {
    "stringProp": { "type": "string" },
    "colorProp": { "type": "string", "x-display": "color-picker" },
  }
}

my_form = TryVjsf(schema=schema)
my_form
Screenshot 2021-10-04 at 13 25 44
jgunstone commented 2 years ago

hi there,

sorry for the delay responding -

thanks so much for putting together the boiler-plate code above, it works great!

I was interested to see if it works with the schema's that are generated from pydantic - and it does!

from typing import List
from pydantic import BaseModel, Field

class PetCls:
    def __init__(self, *, name: str, species: str):
        self.name = name
        self.species = species

class PersonCls:
    def __init__(self, *, name: str, age: float = None, pets: List[PetCls]):
        self.name = name
        self.age = age
        self.pets = pets

class Pet(BaseModel):
    name: str
    species: str

    class Config:
        orm_mode = True

class Person(BaseModel):
    name: str
    age: float = Field
    pets: List[Pet]

    class Config:
        orm_mode = True

bones = PetCls(name='Bones', species='dog')
orion = PetCls(name='Orion', species='cat')
anna = PersonCls(name='Anna', age=20, pets=[bones, orion])
p_form = TryVjsf(schema=Person.schema())
p_form

image

I think that this creates a really nice workflow for rapidly creating UI's in python / jupyter:

thanks again!

jgunstone commented 2 years ago

Hi there, I've started to use the functionality described above and its working well.

vjsf implements a markdown editor which would be great to include.

https://koumoul-dev.github.io/vuetify-jsonschema-form/latest/examples#markdown-editor

where they describe as follows:

You can edit markdown content using the x-display=markdown annotation.

To do this VJsf integrates EasyMDE. But to prevent creating a larger distributable it is not declared as a dependency, you need to load it yourself and make it available in global.EasyMDE (or window.EasyMDE). For example:

import EasyMDE from 'easymde/dist/easymde.min.js'
import 'easymde/dist/easymde.min.css'
global.EasyMDE = EasyMDE

I tried installing EasyMDE into the conda environment i'm using and then adding the snippet above to the try_vjsf.vue boilerplate code you put together - but that didn't work. Is there a way to simply integrate this functionality?

try_vjsf.vue

<template>
  <div>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@koumoul/vjsf@latest/dist/main.css">
    <div v-if="vjsf_loaded">
      <v-form v-model="valid">
        <v-jsf v-model="form_data" :schema="schema"></v-jsf>
      </v-form>
    </div>
  </div>    
</template>
<!-- // <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
// <script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script> -->
<script>

import EasyMDE from 'easymde/dist/easymde.min.js'
import 'easymde/dist/easymde.min.css'
global.EasyMDE = EasyMDE

module.exports = {
    async created() {
        const [VJsf] = await this.import(['https://cdn.jsdelivr.net/npm/@koumoul/vjsf@latest/dist/main.js']);
        this.$options.components['v-jsf'] = VJsf.default;
        this.vjsf_loaded = true;
    },
    methods: {
        import(deps) {
          return this.loadRequire()
              .then(() => new Promise((resolve, reject) => {
                requirejs(deps, (...modules) => resolve(modules));
              }));
        },
        loadRequire() {
          /* Needed in lab */
          if (window.requirejs) {
              console.log('require found');
              return Promise.resolve()
          }
          return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js';
            script.onload = resolve;
            script.onerror = reject;
            document.head.appendChild(script);
          });
        }
    }
}
</script>

many thanks,

maartenbreddels commented 2 years ago

I was interested to see if it works with the schema's that are generated from pydantic - and it does!

yeah, it's really cool :)

maartenbreddels commented 2 years ago

I tried installing EasyMDE into the conda environmen

Did you try getting if from npm/jsdelivr like vjsf in thie example, using the import?

jgunstone commented 2 years ago

hi - apologies - missed your response.

i think this is where my lack of understanding of javascript kicks in -

do you mean importing EasyMDE in a similar way to how it is done here:

    async created() {
        const [VJsf] = await this.import(['https://cdn.jsdelivr.net/npm/@koumoul/vjsf@latest/dist/main.js']);
        this.$options.components['v-jsf'] = VJsf.default;
        this.vjsf_loaded = true;
    },

?

cheers

ektar commented 2 years ago

Stumbled on this issue while trying to get another library wrapped (AGGrid-Vue), really fantastic capability!

For @jgunstone 's question - I think you will need something in the pattern of what's shown for VJsf... I dug through the ipyvuetify and ipyvue code a bit, I think any code you put before the first { gets stripped, so all of your "import EasyMDE" isn't making it through to the final component: https://github.com/widgetti/ipyvue/blob/23e3a0fdfa6c20b8a9efbc8cef13e3a17f6b00fc/js/src/VueTemplateRenderer.js#L316-L324

That should successfully load your library (e.g. if you're watching page sources, you should see it get added), but it may/may not satisfy the dependencies... would love @maartenbreddels or @mariobuikhuizen 's input here as I'm pretty new to javascript, just feeling my way. Depending on the exact artifact you're importing from npm, they may or may not be usable it seems with the client-side dynamic import (is that true?). The "umd"/"amd" formats seem to work well, but I'm finding that plain javascript seems really to need to be wrapped. Will need to use a tool like https://browserify.org to make it importable.

Here's the code I have so far (adapted from example: https://www.ag-grid.com/vue-data-grid/vue2/)... aggrid-vue had been giving errors about no aggrid dependency, that's satisfied now, but giving error about Vue being undefined... I can import the Vue common js library, but they don't offer amd/umd, and I'd think that ipyvuetify/ipyvue must already have vue imported, not sure how to expose that to aggrid-vue...

try_aggrid.vue

<template>
  <div>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community@28.1.1/styles/ag-grid.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community@28.1.1/styles/ag-theme-alpine.css">
    <div v-if="aggrid_vue_loaded">
      <ag-grid-vue
        style="width: 500px; height: 200px"
        class="ag-theme-alpine"
        :columnDefs="columnDefs"
        :rowData="rowData"
      >
      </ag-grid-vue>
    </div>
  </div>
</template>

<script>
module.exports = {
    async created() {
        await this.import(['https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.runtime.js']);
        await this.import(['https://cdn.jsdelivr.net/npm/ag-grid-community@28.1.1/dist/ag-grid-community.amd.js']);
        const [AgGridVue] = await this.import(['https://cdn.jsdelivr.net/npm/ag-grid-vue@28.1.1/dist/ag-grid-vue.umd.min.js']);
        this.$options.components['ag-grid-vue'] = AgGridVue;
        this.aggrid_vue_loaded = true;
    },
    methods: {
        import(deps) {
          return this.loadRequire()
              .then(() => new Promise((resolve, reject) => {
                requirejs(deps, (...modules) => resolve(modules));
              }));
        },
        loadRequire() {
          /* Needed in lab */
          if (window.requirejs) {
              console.log('require found');
              return Promise.resolve()
          }
          return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js';
            script.onload = resolve;
            script.onerror = reject;
            document.head.appendChild(script);
          });
        }
    }
}
</script>
class TryAGGrid(v.VuetifyTemplate):
    template_file = "try_aggrid.vue"

    aggrid_loaded = traitlets.Bool(False).tag(sync=True)
    aggrid_vue_loaded = traitlets.Bool(False).tag(sync=True)

    columnDefs = traitlets.List(traitlets.Dict()).tag(sync=True)
    rowData = traitlets.List(traitlets.Dict()).tag(sync=True)

    def __init__(self, **kwargs):
        self.columnDefs = [
            {"headerName": "Make", "field": "make"},
            {"headerName": "Model", "field": "model"},
            {"headerName": "Price", "field": "price"},
        ]
        self.rowData = [
            {"make": "Toyota", "model": "Celica", "price": 35000},
            {"make": "Ford", "model": "Mondeo", "price": 32000},
            {"make": "Porsche", "model": "Boxster", "price": 72000},
        ]
        super().__init__(**kwargs)    

my_grid = TryAGGrid()
my_grid

javascript console error:

tslib.es6.js:25 Uncaught TypeError: Object prototype may only be an Object or null: undefined
    at setPrototypeOf (<anonymous>)
    at i (tslib.es6.js:25:5)
    at AgGridVue.ts:16:32
    at Module.fae3 (AgGridVue.ts:16:1)
    at n (bootstrap:19:22)
    at 8bbf (bootstrap:83:10)
    at ag-grid-vue.umd.min.js?v=20220916220758:1:1288
    at Object.execCb (require.js?v=d37b48bb2137faa0ab98157e240c084dd5b1b5e74911723aa1d1f04c928c2a03dedf922d049e4815f7e5a369faa2e6b6a1000aae958b7953b5cc60411154f593:1693:33)
    at Module.check (require.js?v=d37b48bb2137faa0ab98157e240c084dd5b1b5e74911723aa1d1f04c928c2a03dedf922d049e4815f7e5a369faa2e6b6a1000aae958b7953b5cc60411154f593:881:51)
    at Module.enable (require.js?v=d37b48bb2137faa0ab98157e240c084dd5b1b5e74911723aa1d1f04c928c2a03dedf922d049e4815f7e5a369faa2e6b6a1000aae958b7953b5cc60411154f593:1173:22)
mariobuikhuizen commented 2 years ago

I think this module has a misconfiguration in its build, making it not work with requirejs. I had a similar issue with https://github.com/jbaysolutions/vue-grid-layout for which I made a custom version with this change: https://github.com/jbaysolutions/vue-grid-layout/commit/370b0159c2e7c42f954a6a84eefc3325add811aa (I should make a PR to get in the original project).

So the solution would be to make this change for ag-grid-vue too.

ektar commented 1 year ago

Thanks, @mariobuikhuizen ! I looked at aggrid-vue, it looks like they are indeed missing vue as an externals definition, will work with them to see if can be added: https://github.com/ag-grid/ag-grid/blob/latest/community-modules/vue/vue.config.js

mariobuikhuizen commented 1 year ago

I've uploaded a patched version of the library to s3 and made a few changes to the template to make it work as a POC. (If you're using Jupyter Lab, you'll need ipyvue >= 1.8.0):

<template>
  <div>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community@28.1.1/styles/ag-grid.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community@28.1.1/styles/ag-theme-alpine.css">
    <div v-if="aggrid_vue_loaded">
      <ag-grid-vue
          style="width: 500px; height: 200px"
          class="ag-theme-alpine"
          :columnDefs="columnDefs"
          :rowData="rowData"
      >
      </ag-grid-vue>
    </div>
  </div>
</template>

<script>
module.exports = {
  async created() {
    await this.import(['https://cdn.jsdelivr.net/npm/ag-grid-community@28.1.1/dist/ag-grid-community.amd.js']);
    // const [{AgGridVue}] = await this.import(['https://cdn.jsdelivr.net/npm/ag-grid-vue@28.1.1/dist/ag-grid-vue.umd.js']);
    /* Load patched version of ag-grid-vue.umd.js */
    const [{AgGridVue}] = await this.import(['https://s3.us-east-2.amazonaws.com/mario.pub/ag-grid-vue.umd.js']);
    this.$options.components['ag-grid-vue'] = AgGridVue;
    this.aggrid_vue_loaded = true;
  },
  methods: {
    import(deps) {
      return this.loadRequire()
          .then(() => new Promise((resolve, reject) => {
            this.defineVue();
            requirejs(deps, (...modules) => resolve(modules));
          }));
    },
    loadRequire() {
      if (window.requirejs) {
        console.log('require found');
        return Promise.resolve()
      }
      /* Needed in lab */
      return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js';
        script.onload = resolve;
        script.onerror = reject;
        document.head.appendChild(script);
      });
    },
    defineVue() {
      if (require.defined("vue")) {
        return;
      }
      if (window.jupyterVue) {
        /* Since Lab doesn't use requirejs, jupyter-vue is not defined. Since ipyvue 1.8.0 it is added to
         * window, so we can use that to define it in requirejs */
        define('vue', [], () => window.jupyterVue.Vue);
      } else {
        define('vue', ['jupyter-vue'], jupyterVue => jupyterVue.Vue);
      }
    }
  }
}
</script>