solara icon indicating copy to clipboard operation
solara copied to clipboard

post request to solara components

Open havok2063 opened this issue 1 year ago • 3 comments

Is there a mechanism for submitting data via a POST request to one or more Solara components, similar to the requestWidget in the Voila-Embed code? If so, are there any examples that I can take a look at? For example, populating the items in a Solara dropdown select with content from the front-end. The content to be loaded into the Solara app is dynamic based on user selection of info not originally available in the Solara app.

I know one can send query parameters with an iframe src but I don't want to append or expose a long list of parameters to the url.

havok2063 avatar Jan 10 '24 21:01 havok2063

Hi Brian,

In our video call we talked about using postMessage? Do you think this is still the way to do? If so, we could provide an example.

Regards,

Maarten

maartenbreddels avatar Feb 28 '24 10:02 maartenbreddels

@maartenbreddels Sorry for the long response on this. Yes indeed this is something I still need and the postMessage approach would work well for me. I have some initial code submitting a postMessage to the iframe. I imagine on the solara side it's an event listener set up on the main solara Page component? If you have an example that would be great. Or if there's anything in the docs already to point me to?

havok2063 avatar Jul 11 '24 18:07 havok2063

Here is my attempt at adding an event handler for the iframes postMessage. This example embeds a Solara app into a single page Vue app, and adds two postMessage events. One for changing the vue+solara theme, and one for updating a solara.Text based on added items in a list. I created a custom Message vue component to add the event listener to, with a generic event_handler to handle the different postMessage content. Maybe there is a simpler way to do this?

message.vue

<template>
    <p>Last received message type: {{ lastMessageType }}</p>
</template>

<script>
export default {
  props: {
  },
  data() {
    return {
      lastMessageType: '',
      postData: {}
    }
  },
  mounted() {
    window.addEventListener('message', this.handleMessage)
  },
  beforeUnmount() {
    window.removeEventListener('message', this.handleMessage)
  },
  methods: {
    handleMessage(event) {
      if (event.data && event.data.type) {
        this.lastMessageType = event.data.type
        this.postData = event.data
        this.update(this.postData)
      }
    }
  }
}
</script>

test.py

import solara


params = solara.reactive([])
text = solara.reactive('initial')


def check_theme(theme=None):
    if theme is not None:
        solara.lab.theme.dark = theme == 'dark'
    else:
        solara.lab.theme.dark = solara.lab.theme.dark_effective

def event_handler(data):
    """ postMessage event handler """
    event_type = data.get('type')
    if event_type == 'themeChange':
        check_theme('dark' if data.get('isDark') else 'light')
    elif event_type == 'itemsChanged':
        text.value = ','.join(data.get('items'))


@solara.component_vue("message.vue")
def Message(
    event_update: solara.Callable[[dict], None] = None):
    pass


@solara.component
def Page():
    router = solara.use_router()
    params.value = dict(i.split('=') for i in router.search.split('&')) if router.search else {}

    isDark = params.value.get('theme', None)
    theme = 'dark' if isDark == 'true' else 'light'
    check_theme(theme)

    with solara.Column():
        Message(event_update=event_handler),
        solara.Text(f'Text: {text.value}')

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue Test App</title>
    <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.css" rel="stylesheet">
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.js"></script>
</head>
<body>
    <div id="app">
        <v-app :theme="isDark ? 'dark' : 'light'">
            <v-app-bar>
                <v-app-bar-title>{{ title }}</v-app-bar-title>
                <v-spacer></v-spacer>
                <v-btn icon @click="toggleTheme">
                    <v-icon icon="mdi-theme-light-dark"></v-icon>
                </v-btn>
            </v-app-bar>
            <v-main>
                <v-container>
                    <v-text-field
                        v-model="newItem"
                        @keyup.enter="addItem"
                        label="Add a new item"
                        append-icon="mdi-plus"
                        @click:append="addItem"
                    ></v-text-field>
                    <v-list>
                        <v-list-item v-for="(item, index) in items" :key="index">
                            <v-list-item-title>{{ item }}</v-list-item-title>
                            <template v-slot:append>
                                <v-btn icon @click="removeItem(index)">
                                    <v-icon>mdi-delete</v-icon>
                                </v-btn>
                            </template>
                        </v-list-item>
                    </v-list>
                    <iframe-component :src="iframeSrc" :is-dark="isDark" :items="items"></iframe-component>
                </v-container>
            </v-main>
        </v-app>
    </div>

    <script>
        const { createApp, ref, watch, toRaw } = Vue
        const vuetify = Vuetify.createVuetify()

        const IframeComponent = {
            props: ['src', 'isDark', 'items'],
            template: `
                <v-sheet class="mt-4">
                    <iframe :src="src" frameborder="0" width="100%" height="400" ref="iframe"></iframe>
                </v-sheet>
            `,
            setup(props) {
                const iframe = ref(null)

                // watch the isDark property
                watch(() => props.isDark, (newIsDark) => {
                    if (iframe.value && iframe.value.contentWindow) {
                        iframe.value.contentWindow.postMessage({ type: 'themeChange', isDark: newIsDark }, '*')
                    }
                })

                // watch the list of items
                watch(() => props.items, (newItems) => {
                    if (iframe.value && iframe.value.contentWindow) {
                        iframe.value.contentWindow.postMessage({ type: 'itemsChanged', 'items': toRaw(newItems) }, '*')
                    }
                }, { deep: true })

                return { iframe }
            }
        }

        const app = createApp({
            components: {
                'iframe-component': IframeComponent
            },
            setup() {
                const title = ref('Vue Test App')
                const newItem = ref('')
                const items = ref([])
                const isDark = ref(false)
                const iframeSrc = ref(`http://localhost:8765?theme=${isDark.value}`)

                function addItem() {
                    if (newItem.value.trim()) {
                        items.value.push(newItem.value.trim())
                        newItem.value = ''
                    }
                }

                function removeItem(index) {
                    items.value.splice(index, 1)
                }

                function toggleTheme() {
                    isDark.value = !isDark.value
                }

                return {
                    title,
                    newItem,
                    items,
                    iframeSrc,
                    isDark,
                    addItem,
                    removeItem,
                    toggleTheme
                }
            }
        })

        app.use(vuetify)
        app.mount('#app')
    </script>
</body>
</html>

havok2063 avatar Jul 15 '24 21:07 havok2063