V3 Vue apps
Vue 3 is the best-supported multi-screen framework in V3 because a runtime-compiler build exists — meaning template strings compile in the browser without a toolchain. The traps below come from using the wrong Vue build, or reaching for features that assume Vite/webpack.
Loading the framework
Add vue via add_dependencies with lazy: true, then:
await Fliplet.require.lazy('vue');
const Vue = window.Vue;
Fliplet.require.lazy(name) resolves once the UMD bundle has executed; the module itself lands on window (window.Vue, window.VueRouter). Read it off window after the await — assigning the awaited value directly gives you the URL string, not the module.
Pick the runtime-compiler build (usually vue.global.js), not the runtime-only build. The runtime-only build (vue.runtime.global.js) does not include the template compiler — any component that uses template: '...' strings will fail to render. The runtime-only build only works if every component is authored as a pre-compiled render function, which is impractical without a bundler.
For Vue Router, the separate dependency:
await Fliplet.require.lazy('vue-router');
const VueRouter = window.VueRouter;
Features that need a build step
| Feature | Why it fails | Do this instead |
|---|---|---|
.vue single-file components |
There is no loader resolving them at runtime | Use plain component objects with template: strings, or add vue3-sfc-loader as a dependency if the app genuinely needs SFCs. Pick one approach per app and stay consistent. |
<script setup> |
Requires SFC compilation | Use the Options API or explicit setup() function on component objects. |
<style scoped> |
Requires SFC compilation | Scope manually by wrapping each component’s CSS in a root class selector. |
Bare ESM imports (import Vue from 'vue') |
No bundler resolves the specifier | Use Fliplet.require.lazy('vue'). |
TypeScript (.ts, .tsx) |
No transpiler | Plain JavaScript. |
Wiring to Fliplet.Router
Full contract is in V3 routing. Vue-specific note: the history backend is platform-conditional — path history with the base path on web, hash history on native (Cordova file:// blocks pushState path changes):
const router = VueRouter.createRouter({
history: Fliplet.Router.isNative()
? VueRouter.createWebHashHistory() // native — hash only
: VueRouter.createWebHistory(Fliplet.Router.getBasePath()), // web — path + basename
routes: [...]
});
Unconditional createWebHistory() throws a file: SecurityError on native; unconditional createWebHashHistory() produces ugly #/route URLs on web. The boot-HTML lint flags both unless you branch on Fliplet.Router.isNative() (unguarded-web-history / create-web-hash-history).
Build routes from Fliplet.Router.getRouteManifest() — do not hardcode. In each route’s resolver, call Fliplet.Router.resolveRoute(path). The content field in its result IS the screen’s source — already fetched for you via Fliplet.Media.getContents. Return it from your loader and render it in your component; don’t fetch the file again.
Binding Fliplet.Media.authenticate
Authenticated URLs resolve asynchronously — bind through reactivity, not at module load:
// inside a component
data() { return { logoSrc: '' }; },
async mounted() {
this.logoSrc = await Fliplet.Media.authenticate(rawUrl);
}
Then <img :src="logoSrc">. Using src="" directly, or computing the authenticated URL at module scope, leaves the image broken until the template re-renders.
Updating reactive state asynchronously
Update the reactive state used by the template when processing asynchronous results, progress events or streamed text. In Vue 3, inserting a plain object into a reactive array does not make that original object reactive. Retrieve the item from the array before keeping it in a callback; otherwise its mutations may appear only when another state change causes a render. See Vue’s reactive proxy behavior.
After loading Vue as described above, add <div id="async-results"></div> to the screen and run this example. demoUpdates simulates two delayed text updates before completion; replace it with your asynchronous source while keeping its updates directed to reactive state.
function demoUpdates(onText) {
return new Promise(function(resolve) {
setTimeout(function() { onText('First update. '); }, 100);
setTimeout(function() { onText('Second update.'); }, 200);
setTimeout(resolve, 300);
});
}
const resultsScreen = Vue.createApp({
data: function() { return { results: [], nextResultId: 1 }; },
template: '<ol><li v-for="result in results" :key="result.id">' +
'<p>{{ result.text }}</p><small>{{ result.status }}</small></li></ol>',
methods: {
createResult: function() {
const rawResult = { id: this.nextResultId++, text: '', status: 'pending' };
this.results.push(rawResult);
// Return the reactive item, not rawResult, to the asynchronous callback.
return this.results[this.results.length - 1];
},
beginUpdates: async function(receiveUpdates) {
const result = this.createResult();
try {
await receiveUpdates(function(text) { result.text += text; });
result.status = 'complete';
} catch (error) {
result.status = 'interrupted';
}
return result;
}
}
}).mount('#async-results');
resultsScreen.beginUpdates(demoUpdates);
The first update appears while the operation is pending, the second extends the same item, and completion changes that item’s status. In the Composition API, retrieve an inserted item through results.value[index] for a normal array ref, or through the array returned by reactive(). Keep using that proxy rather than the original object.
Vue batches DOM updates. Use await this.$nextTick() (Options API) or await Vue.nextTick() when you need to inspect or scroll the updated DOM. Calling nextTick() after mutating a non-reactive original object does not schedule a render. Test updates after the initial pending item has rendered so an earlier scheduled render cannot hide the mistake. Apply your screen’s unmount/cancellation handling to stop late updates after disposal.
Common errors
Symptom in get_preview_logs('errors') |
Cause | Fix |
|---|---|---|
[Vue warn]: Component is missing template or render function |
Runtime-only Vue build loaded; template: strings silently ignored |
Switch to the runtime-compiler build (vue.global.js) |
Uncaught SyntaxError: Unexpected token '<' inside a component file |
.vue SFC uploaded without vue3-sfc-loader |
Either convert the component to a plain object with template: string, or add vue3-sfc-loader |
| Blank screen, no errors | Component returned from Fliplet.Media.getContents wasn’t registered before router resolved |
Register the component inside the route resolver, not at module load |
DO / DON’T
- DO use
Fliplet.require.lazy('vue')and the runtime-compiler build. - DO build the router from
Fliplet.Router.getRouteManifest()+getBasePath(). - DO branch the history backend on
Fliplet.Router.isNative()—createWebHashHistory()on native,createWebHistory(getBasePath())on web. - DO bind authenticated media URLs into reactive
data()fields. - DON’T use
createWebHistory()unconditionally — it throws afile:SecurityErroron native. Gate it onFliplet.Router.isNative(). - DON’T use
createWebHashHistory()on web — hash mode is only for native. - DON’T author
.vueSFCs withoutvue3-sfc-loaderloaded. - DON’T
importanything — useFliplet.require.lazy.