Skip to content

Composing a Vue app

In a Vue app, each feature — or a group of features — is a Task. A Task can return anything — this example uses { ui, api }.

Each feature is split into a View (.vue SFC) and a Task: the View takes the feature’s atom as a prop and stays a plain SFC; the Task creates the atom, wraps the View into a Widget closure that supplies it, and returns both ui.Widget and api.$atom.

One Task at the end — a Render Task — mounts the UI into the page. It reads upstream Tasks directly: every Task is known here, so a Tag adds nothing.

App.vue only hosts the mount target (<div id="root">) and runs compose().

What the example does:

  • ThemeSelect is a feature — it returns the chosen theme and a way to choose it.
  • LanguageSelect is a feature — it returns the chosen language and a way to choose it.
  • Appearance is a layout — through its context it reads the language from a shared Tag and the widgets from another Tag.
  • ThemeProvider is a provider — through its context it reads the chosen theme.
  • The Render Task mounts the Layout from Appearance wrapped in the Provider from ThemeProvider.
  • compose() wires it all; the Render Task runs last.

nanostores here is only for the task.api demo. Features can use any state manager.

<script setup>
import { compose, createTask, createWire, literal } from "@grlt-hub/app-compose"
import { createApp, h, onMounted } from "vue"
import { Appearance, appearanceWidgets } from "./appearance.js"
import { LanguageSelect, ThemeSelect } from "./features.js"
import { selectedLanguage, selectedTheme } from "./shared-tags.js"
import { ThemeProvider } from "./theme-provider.js"
// page-level — reads Tasks directly
const renderApp = createTask({
name: "render-app",
run: {
context: {
AppearanceLayout: Appearance.result.ui.Layout,
ThemeProvider: ThemeProvider.result.ui.Provider,
},
fn: (ctx) => {
createApp({
render: () => h(ctx.ThemeProvider, null, () => h(ctx.AppearanceLayout)),
}).mount("#root")
},
},
})
onMounted(() => {
compose()
.step([ThemeSelect, LanguageSelect])
// fill features data into shared tags
.step([
createWire({
from: {
theme: ThemeSelect.result.api.$theme,
language: LanguageSelect.result.api.$language,
},
to: { theme: selectedTheme, language: selectedLanguage },
}),
])
// collect feature widgets into one tag
.step(
createWire({
from: [
{ Widget: ThemeSelect.result.ui.Widget, key: literal(ThemeSelect.name) },
{ Widget: LanguageSelect.result.ui.Widget, key: literal(LanguageSelect.name) },
],
to: appearanceWidgets,
}),
)
.step([Appearance, ThemeProvider])
.step(renderApp)
.run()
})
</script>
<template>
<div id="root"></div>
</template>