Embedding applications using js instead of script tag

Hi all,

I’ve spent some time recently embedding a couple of Bokeh applications into my Django+Vue.js application, and I can’t say it was easy but it was certainly worth it.
My main struggle was the fact that you can’t really execute script tags in Vue.js templates. My first working solution was based on vue-script-component’s approach which relies on postscribe. It was hacky and I didn’t like it. I took a better look at the bokeh.embed.server module and ended up simply parsing the JavaScript out of the <script> tag returned by server_session() and serving it back instead of the HTML. This allowed me to use vue-script2 and create a much cleaner solution.

To get to the point, I was wondering whether anyone else thinks it could be beneficial to either create a separate function or add a keyword argument to the server_session() function which returns plain JS that can be embedded using <script src="/some/endpoint?elementId=element-id" />. The backend could then call server_session(session_id=session.id, url=url, html=False, element_id=element_id) and return the script (HttpResponse(script, content_type="text/javascript" in Django’s case).

What I’m doing right now is:

  1. In Django, where I’m using Django REST framework, I have:
from bokeh.client import pull_session
from bokeh.embed import server_session
from bs4 import BeautifulSoup
from my_app.models.my_model import MyModel
from my_app.serializers.my_model import MyModelSerializer
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.request import Request

BOKEH_URL = 'http://localhost:5006/my-app'


class MyModelViewSet(viewsets.ModelViewSet):
    serializer = MyModelSerializer
    ...
    @action(detail=False, methods=["GET"]) 
    def plot(self, request) -> HttpResponse:
        destination_id = request.GET.get("elementId", "bk-plot")
        with pull_session(url=BOKEH_URL) as session:
            html = server_session(session_id=session.id, url=BOKEH_URL)
            script = fix_script(html, destination_id)
        return HttpResponse(script, content_type="text/javascript")
   
    # or, if we would like to pass arguments:

    @action(detail=True, methods=["GET"])
    def plot_instance(self, request: Request, pk: int = None) -> Response:
        arguments = {"item_id": str(pk)}
        destination_id = request.GET.get("elementId", "bk-plot")
        with pull_session(url=BOKEH_URL, arguments=arguments) as session:
            html = server_session(session_id=session.id, url=BOKEH_URL)
            script = fix_script(html, destination_id)
        return HttpResponse(script, content_type="text/javascript")

def fix_script(html: str, destination_id: str) -> str:
    """ This functions parses the script out of the html tag returned
    by `server_session()` and replaces the tag ID that will be replaced
    by the `autoload.js` script. 
    """
    soup = BeautifulSoup(html, features="lxml")
    element = soup(["script"])[0]
    random_id = element.attrs["id"]
    script = element.contents[0]
    return script.replace(random_id, destination_id)
  1. In Vue.js:

(This really has nothing to do with the development of bokeh but I am including it here both for the completeness of the example and for future reference in case it’s useful to anyone).

a. Install vue-script2:

npm install vue-script2 --save

b. In main.js, add:

import VS2 from 'vue-script2'

Vue.use(VS2)

c. In the component (I also needed to be able to re-render the plot every time a certain selection is made):

<template>
...
<div :key="bokehAppKey">
  <script :id="bokehAppId" type="application/javascript"></script>
</div>    
<div v-for="item in items" :key="item.id">
  <v-btn @click="updateBokehApp(item.id)">
    Press Me!
  </v-btn>
</div>
...
</template>


<script>
import VueScript2 from 'vue-script2'

export default {
  name: 'MyComponent',
  data: () => ({
    // Key used to force re-render on item selection and generate unique
    // unique <script> IDs.
    bokehAppKey: 0
  }),
  computed: {
    // Generated unique <script> IDs.
    bokehAppId: function () {
      return `bokeh-app-${this.bokehAppKey}`
    }
  },
  methods: {
    updateBokehApp: function(itemId) {
      // Cause re-rendering of the <div>.
      this.bokehAppKey += 1
      // Load the script, which will in turn send the AJAX
      // request that will replace the <script> element for
      // which we passed the ID.
      let src = `/api/myApp/myModel/${itemId}/plot_instance`
      VueScript2.load(src)
    }
  }
} 
</script>

What I am proposing is simply to add the option of returning the script like the custom DRF endpoints do for the sake of non inline-<script>-friendly embedding scenarios for which the src attribute may be easily utilized instead.

Of course if anyone can think of a better solution or has suggestions for how to improve upon this one I would be more than happy for your input! And I would be happy to try and come up with a PR if you think my proposal makes sense.

Thanks for all your hard work! Yay Bokeh!