Edit
@learnhouse/vue

Activities as a Vue component

<LearnHouseActivity> for Vue 3 and Nuxt. Pages, quizzes, flipcards, scenarios, code playgrounds, math, video and PDFs render with LearnHouse's own blocks, with Vue props, events and slots around them.

$npm install @learnhouse/vue
<LearnHouseActivityactivity-id="activity_…":theme="theme"@complete="next"#footer…/>your vue appINTERACTIVE QUIZYOUR FOOTER SLOT

Quickstart

Install

npm install @learnhouse/vue

Vue 3.3 or later is required. The package brings @learnhouse/react and React itself with it: see What it costs.

Import the stylesheet once

src/main.ts
import { createApp } from 'vue'
import '@learnhouse/vue/styles.css'
import App from './App.vue'

createApp(App).mount('#app')

It is the same stylesheet as @learnhouse/react's, scoped under .lh-activity, so it never touches the rest of your page.

Render an activity

src/Lesson.vue
<script setup lang="ts">
import { LearnHouseActivity } from '@learnhouse/vue'
</script>

<template>
  <LearnHouseActivity
    base-url="https://learn.your-school.com"
    activity-id="activity_0b3c…"
    course-id="course_9f2a…"
  />
</template>

base-url is the address of your LearnHouse instance (its API). Ids are the full uuids, with their activity_ and course_ prefixes.

With activity-id, the browser fetches the activity, and LearnHouse only answers browsers on its own domain (and localhost). On another domain, fetch on your server with the SDK and pass :activity, as described in Where to fetch.

Four ways to say what to render

<!-- Passing course-id as well loads the course in parallel (faster). -->
<LearnHouseActivity activity-id="activity_…" course-id="course_…" />

course-id and org-id tell image, video, audio and PDF blocks where their files live.

Share one setup with a provider

LearnHouseProvider gives every activity below it the same instance, token and defaults, and gives your own code an SDK client.

src/App.vue
<script setup lang="ts">
import { LearnHouseProvider, type LearnHouseEvent } from '@learnhouse/vue'

const props = defineProps<{ accessToken?: string }>()
const track = (event: LearnHouseEvent) => analytics.track(`lesson_${event.type}`, event)
</script>

<template>
  <LearnHouseProvider
    base-url="https://learn.your-school.com"
    :token="props.accessToken"
    :theme="{ accent: '#7c3aed', fontFamily: 'Inter, sans-serif' }"
    locale="fr"
    @event="track"
  >
    <RouterView />
  </LearnHouseProvider>
</template>

An activity's own props win over the provider's; theme is merged key by key. You can also call provideLearnHouse({ baseUrl, token, … }) in a root component's setup() instead of rendering the component.

Composables

Both need a provider (or provideLearnHouse) above them.

<script setup lang="ts">
import { LearnHouseActivity, useActivity, useLearnHouseClient } from '@learnhouse/vue'

const props = defineProps<{ id: string }>()

// Refetches whenever props.id changes; aborts the previous request.
const { activity, loading, error, refresh } = useActivity(() => props.id)

// The provider's @learnhouse/sdk client, for anything else.
const client = useLearnHouseClient()
const loadOutline = () => client.courses.getMeta('course_…', { slim: true })
</script>

<template>
  <p v-if="loading">Loading…</p>
  <p v-else-if="error">{{ error.message }}</p>
  <LearnHouseActivity v-else-if="activity" :activity="activity" course-id="course_…" />
</template>
ComposableReturns
useActivity(id, { client? }){ activity, error, loading, refresh }. id can be a string, a ref or a getter
useLearnHouseClient()The provider's SDK client

Events

EventPayload
@loadThe activity, once fetched
@readyNothing. The first content is on screen
@complete{ type, activityId }, with track-completion, when the end scrolls into view
@media{ action, media, src, currentTime, duration } for video and audio
@resize{ width, height } of the rendered activity
@errorThe Error
@eventEvery event above, plus link_click, visible and analytics (see Events)
@link-click{ href, external, originalEvent }. Return false to stop the navigation
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { LearnHouseActivity, type LearnHouseLinkClickDetail } from '@learnhouse/vue'

const router = useRouter()
// Keep links to your own site inside the app.
function onLinkClick({ href, external }: LearnHouseLinkClickDetail) {
  if (external) return
  router.push(new URL(href).pathname)
  return false
}
</script>

<template>
  <LearnHouseActivity activity-id="activity_…" track-completion @complete="markDone" @link-click="onLinkClick" />
</template>

Slots

<LearnHouseActivity activity-id="activity_…">
  <template #fallback><Spinner /></template>
  <template #header="{ activity }"><h2>{{ activity.name }}</h2></template>
  <template #footer="{ activity }"><NextLessonButton :after="activity.activity_uuid" /></template>
  <template #error="{ error, retry }">
    <p>{{ error.message }}</p>
    <button @click="retry">Try again</button>
  </template>
</LearnHouseActivity>
SlotShownProps
fallbackWhile the renderer or the activity loads (and in server-rendered HTML)none
header / footerAbove / below the activity, once loaded{ activity }
errorInstead of the built-in message when loading fails{ error, retry }

Slots are Vue content placed around the renderer, so they use your app's styles, not the activity theme.

Nuxt

Add the stylesheet in nuxt.config.ts and use the components in any page. There is nothing to wrap in <ClientOnly>: on the server the component renders its fallback slot, and the renderer loads in the browser after hydration.

nuxt.config.ts
export default defineNuxtConfig({
  css: ['@learnhouse/vue/styles.css'],
})

To keep the activity off the browser's network path (another domain, private courses), load it in a server route with the SDK and your API token, which never reaches the browser:

server/api/lessons/[id].get.ts
import { createClient } from '@learnhouse/sdk'

const lh = createClient({ baseUrl: process.env.LEARNHOUSE_URL!, token: process.env.LEARNHOUSE_TOKEN! })

export default defineEventHandler(async (event) => {
  const activity = await lh.activities.get(getRouterParam(event, 'id')!)
  const course = await lh.GET('/api/v1/courses/id/{course_id}', {
    params: { path: { course_id: activity.course_id! } },
  })
  return { activity, courseId: course.course_uuid }
})
app/pages/lessons/[id].vue
<script setup lang="ts">
import { LearnHouseActivity } from '@learnhouse/vue'

const route = useRoute()
const { data } = await useFetch(`/api/lessons/${route.params.id}`)
</script>

<template>
  <LearnHouseActivity
    v-if="data"
    :activity="data.activity"
    :course-id="data.courseId"
    base-url="https://learn.your-school.com"
  >
    <template #fallback><p>Loading the lesson…</p></template>
  </LearnHouseActivity>
</template>

How it works

<LearnHouseActivity>
Your Vue component: props, events, slots
properties
<learnhouse-activity>
Custom element from @learnhouse/react
React root
LearnHouse blocks
The same renderer the React package and the app use

The Vue package has no blocks of its own. It passes your props to a custom element, which mounts the React renderer in its own root and sends its events back as DOM events.

@learnhouse/react also ships the renderer as a custom element, <learnhouse-activity>. @learnhouse/vue renders that element, sets its properties from your props and turns its DOM events (lh-load, lh-complete…) into Vue events. The blocks are the React components of the LearnHouse release you installed, so the Vue and React packages always render the same thing.

The element has no shadow DOM, which is why the one stylesheet works for both. You can also use it directly, from Svelte, Angular or plain HTML:

<script type="module">
  import '@learnhouse/react/element'
  import '@learnhouse/react/styles.css'
</script>

<learnhouse-activity base-url="https://learn.your-school.com" activity-id="activity_…" course-id="course_…"></learnhouse-activity>

What it costs

React is part of the bill. A Vue app using this package also downloads React and React DOM, about 67 KB gzipped, on top of the renderer itself. That weight loads lazily: @learnhouse/vue adds about 6 KB gzipped to your entry bundle (2 KB for the components, the rest is the SDK client), and React, the renderer and the blocks are fetched after the component mounts, only on pages that show an activity.

The renderer costs the same as in a React app. Blocks are split into their own chunks, so a page only downloads the blocks it contains, the video player only for videos and the PDF viewer only for PDFs. See Bundle size.

Props

PropTypeNotes
activity-idstringActivity uuid, fetched from the instance
activity-namestringLook up by name inside course-id
course-idstringNeeded with activity-name; tells media blocks their course
activityobjectAn activity you already fetched
contentobject | stringA raw document; no network for the content
base-urlstringYour instance. Defaults to the provider's
tokenstringA user access token. Defaults to the provider's
org-idstringOrg uuid for media URLs with content
site-urlstringYour LearnHouse site, for the "Made with LearnHouse" link
themeLearnHouseThemeMerged over the provider's. See Theming
localestringUI language of the blocks. Defaults to the provider's, then en
messagesobjectOverride UI strings
dir'ltr' | 'rtl' | 'auto'Text direction. auto follows the locale
hide-blocksstring[]Node names to drop (blockQuiz, table…)
link-target'_blank' | '_self' | '_parent' | '_top'Where links open. Default _blank
track-completionbooleanEmit complete at the end of the activity
show-titlebooleanShow the activity name above the content
show-powered-bybooleanA small "Made with LearnHouse" link
unstyledbooleanSkip the root theming styles
transform-content(doc) => docRewrite the document before rendering

LearnHouseProvider takes base-url (required), token, get-token, media-url, site-url, client, user, theme, locale, messages and link-target, and emits @event for every activity below it.

The component also exposes reload() (through a template ref), which mounts the activity again and refetches it.

Not in this version

  • Block and mark overrides. components and marks, which replace blocks with your own React components in @learnhouse/react, have no Vue equivalent yet. Use hide-blocks and transform-content to change what renders.
  • header and footer inside the theme. Vue slots render around the activity, not inside its themed root.
  • Several instances on one page. As with @learnhouse/react, media URLs follow the most recently mounted base-url, so keep to one LearnHouse instance per page.