Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

post request to solara components #457

Open
havok2063 opened this issue Jan 10, 2024 · 3 comments
Open

post request to solara components #457

havok2063 opened this issue Jan 10, 2024 · 3 comments

Comments

@havok2063
Copy link
Collaborator

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.

@maartenbreddels
Copy link
Contributor

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

@havok2063
Copy link
Collaborator Author

@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
Copy link
Collaborator Author

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/font@6.x/css/materialdesignicons.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/vuetify@3.3.0/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/vuetify@3.3.0/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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
2 participants