v2.1.0

Recipe: Frame app skeleton

Minimum viable Bitrix24 iframe app: init handshake, set title, persist app state, open a slider, close cleanly.

Goal

A single Vue 3 component that demonstrates the four things every Bitrix24 iframe app needs: initialize the postMessage handshake, set the parent window title, persist a piece of state across reloads via app options, and open another portal page in a slider.

Stack & Prerequisites

  • Vue 3 + Vite (or Nuxt 3 with @bitrix24/b24jssdk-nuxt)
  • @bitrix24/b24jssdk@^1.0.0
  • The page must be served from the URL registered as the placement handler in your Bitrix24 app card. Outside the iframe, initializeB24Frame() rejects.
This skeleton covers a running app, not an installing one. If $b24.isInstallMode is true, the app must finish its installation with installFinish() — until it does, the portal delivers no events to it, and bot handlers and event.bind handlers never fire, with nothing failing loudly to say so. See the Installation Wizard recipe.
pnpm add @bitrix24/b24jssdk

Full Example

src/components/AppShell.vue:

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import {
  initializeB24Frame,
  type B24Frame,
  LoggerFactory
} from '@bitrix24/b24jssdk'

const logger = LoggerFactory.createForBrowser('AppShell', import.meta.env.DEV)
const ready = ref(false)
const userTheme = ref<'light' | 'dark'>('light')
const portalDomain = ref('')
let $b24: B24Frame | null = null

onMounted(async () => {
  try {
    $b24 = await initializeB24Frame()
  }
  catch (error) {
    logger.error('initializeB24Frame failed', error)
    return
  }

  $b24.parent.setTitle('My Bitrix24 App') // sets the in-page #pagetitle, not the browser tab
  // Auto-resize the iframe to fit its content; call again when content height
  // changes (e.g. after a network response renders).
  await $b24.parent.resizeWindowAuto()

  // Read persisted preferences. Returns `null` if nothing was set yet.
  const stored = $b24.options.appGet('theme') as 'light' | 'dark' | null
  if (stored) userTheme.value = stored

  const appInfo = await $b24.actions.v2.call.make({ method: 'app.info' })
  portalDomain.value = appInfo.getData().result.DOMAIN ?? ''

  ready.value = true
})

onBeforeUnmount(() => {
  $b24 = null
})

async function setTheme(value: 'light' | 'dark') {
  userTheme.value = value
  // App-scoped option: persisted on the portal under the app, not the user.
  await $b24?.options.appSet('theme', value)
}

async function openCrmDeals() {
  if (!$b24) return
  const url = new URL('/crm/deal/list/', `https://${portalDomain.value}`)
  await $b24.slider.openPath(url, 1200)
}

async function close() {
  await $b24?.parent.closeApplication()
}
</script>

<template>
  <section v-if="ready" class="app-shell" :data-theme="userTheme">
    <header>
      <h1>Hello from {{ portalDomain }}</h1>
    </header>

    <main>
      <button @click="setTheme(userTheme === 'light' ? 'dark' : 'light')">
        Switch theme (current: {{ userTheme }})
      </button>
      <button @click="openCrmDeals">
        Open deals in slider
      </button>
    </main>

    <footer>
      <button @click="close">Close app</button>
    </footer>
  </section>
  <p v-else>Loading…</p>
</template>

src/main.ts:

// @check-ignore: vue .vue module import — .vue files are not in tsconfig

import { createApp } from 'vue'
import AppShell from './components/AppShell.vue'

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

Run It

# 1. Local dev server (Vite)
pnpm dev
# → http://localhost:5173/ — running this URL directly will reject:
#   initializeB24Frame() needs a parent Bitrix24 window.

# 2. Expose the dev server (ngrok / cloudflared / your tunnel of choice)
ngrok http 5173
# → grab the https URL.

# 3. In Bitrix24: Applications → Developer resources → Local app →
#    point the placement handler URL at the tunnel URL and Save.

# 4. Open the placement inside Bitrix24.

Once the iframe loads, you'll see: the parent window title becomes "My Bitrix24 App", the iframe resizes to its content, and the body shows Hello from <your-portal>.bitrix24.com plus the two buttons. Toggling the theme persists across reloads (the value lives on the portal under the app, not in localStorage). Clicking "Open deals in slider" opens /crm/deal/list/ of the same portal in a Bitrix24-styled slider.

How It Works

  • initializeB24Frame() is the only correct way to construct B24Frame. It deduplicates concurrent calls, parses portal handshake parameters from window.name, and waits for the parent window to acknowledge the auth payload.
  • parent.setTitle() and parent.resizeWindowAuto() reach across the iframe boundary via postMessage. Both are idempotent — call them whenever your layout changes.
  • options.appGet() is synchronous because the values are loaded once at handshake time. options.appSet() is asynchronous because it round-trips to the server.
  • slider.openPath(url, width) accepts a URL instance (not a string) and returns a Promise<StatusClose> that resolves when the user closes the slider. The portal CSP only allows opening URLs on the same portal domain.

Limitations

  • The component must mount inside the Bitrix24 iframe; running it standalone (e.g. in Storybook) fails immediately because the parent window is missing. Guard for window.parent === window if you need a graceful fallback.
  • $b24.options.appSet() writes one option at a time. Batching multiple writes inside a single render is fine (the SDK serializes them), but each call is a network round-trip.
  • parent.closeApplication() only closes the app slider when the placement supports closing. Embedded placements (sidebar widgets) ignore the request.
  • App-scoped options are visible to every user of the app on the portal. Use userGet / userSet for per-user preferences.

See also