solara
solara copied to clipboard
post request to solara components
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.
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 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?
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>