v1.2.0

Recipe: Frame app skeleton

Minimum viable Bitrix24 iframe app: init handshake, set title, persist app state, open a slider, close cleanly.
We are still updating this page. Some data may be missing here — we will complete it shortly.

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.
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')
  // 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