Developing a Vue.js component system for 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    745
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Vue.js Component System Development for 1C-Bitrix

A single Vue component on a site is an integration. A component system is an architecture. The difference is fundamental: in a system, components share common state, a common design token, a unified initialization mechanism, and a standard way to interact with the Bitrix backend. Without a systematic approach, within six months the site ends up with three versions of the cart, two different approaches to AJAX, and duplicated code in every component.

What the Component System Includes

A typical Vue component system for a Bitrix store:

  • Core: app configuration, Pinia stores, API layer, utilities
  • UI components: buttons, inputs, modals, toasts — no dependency on business logic
  • Business components: cart, wishlist, comparison, reviews, filter — use stores and API
  • Mount points: places in the PHP template where Vue components are mounted

File Structure

/local/js/vue/
├── app.ts                    # initialization and mounting
├── api/
│   ├── client.ts             # base HTTP client
│   ├── cart.ts
│   ├── catalog.ts
│   └── wishlist.ts
├── stores/
│   ├── cartStore.ts
│   ├── wishlistStore.ts
│   ├── compareStore.ts
│   └── userStore.ts
├── components/
│   ├── ui/                   # design system
│   │   ├── Button.vue
│   │   ├── Modal.vue
│   │   ├── Toast.vue
│   │   ├── Spinner.vue
│   │   └── Badge.vue
│   ├── cart/
│   │   ├── CartButton.vue    # cart button in the header
│   │   ├── CartDrawer.vue    # slide-out cart panel
│   │   └── CartItem.vue
│   ├── catalog/
│   │   ├── AddToCartBtn.vue
│   │   ├── WishlistBtn.vue
│   │   └── CompareBtn.vue
│   └── product/
│       ├── Reviews.vue
│       └── SizeAdvisor.vue
└── types/
    ├── cart.ts
    ├── product.ts
    └── user.ts

Initialization Mechanism

The problem with multiple Vue components on a single page: you cannot create a separate app (createApp) for each one — they will not share Pinia stores, and the cart state in the header will differ from the state in the popup.

The correct approach: one application, multiple mount points.

// app.ts
import { createApp, defineAsyncComponent } from 'vue'
import { createPinia }  from 'pinia'

// Register components for mounting via data attributes
const componentRegistry: Record<string, any> = {
    'cart-button':   defineAsyncComponent(() => import('./components/cart/CartButton.vue')),
    'add-to-cart':   defineAsyncComponent(() => import('./components/catalog/AddToCartBtn.vue')),
    'wishlist-btn':  defineAsyncComponent(() => import('./components/catalog/WishlistBtn.vue')),
    'compare-btn':   defineAsyncComponent(() => import('./components/catalog/CompareBtn.vue')),
    'reviews':       defineAsyncComponent(() => import('./components/product/Reviews.vue')),
    'size-advisor':  defineAsyncComponent(() => import('./components/product/SizeAdvisor.vue')),
}

// Create one app with Pinia
const pinia = createPinia()

// Mount components into each element with data-vue-component
document.querySelectorAll('[data-vue-component]').forEach((el) => {
    const name      = el.getAttribute('data-vue-component')!
    const Component = componentRegistry[name]
    if (!Component) return

    // Parse props from data attributes
    const props: Record<string, any> = {}
    for (const attr of el.attributes) {
        if (attr.name.startsWith('data-prop-')) {
            const propName = attr.name.replace('data-prop-', '').replace(/-./g, m => m[1].toUpperCase())
            props[propName] = JSON.parse(attr.value)
        }
    }

    const app = createApp(Component, props)
    app.use(pinia) // One Pinia instance — shared state
    app.mount(el)
})

In the PHP template:

<!-- Header: cart button -->
<div data-vue-component="cart-button"></div>

<!-- Product card: add buttons -->
<div data-vue-component="add-to-cart"
     data-prop-product-id="<?= $product['ID'] ?>"
     data-prop-price="<?= $product['PRICE'] ?>"></div>

<div data-vue-component="wishlist-btn"
     data-prop-product-id="<?= $product['ID'] ?>"></div>

Key point: one pinia instance is passed to all apps. This means cartStore in the header and cartStore on the product card are the same store — state is synchronized.

API Layer: Unified HTTP Client

// api/client.ts
const CSRF_TOKEN = (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content

async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
    const res = await fetch(url, {
        ...options,
        headers: {
            'Content-Type': 'application/json',
            'X-Bitrix-Csrf-Token': CSRF_TOKEN,
            ...options.headers,
        },
    })

    if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`)
    const data = await res.json()
    if (data.errors?.length) throw new Error(data.errors[0].message)

    return data
}

export const apiGet  = <T>(url: string) => request<T>(url)
export const apiPost = <T>(url: string, body: unknown) =>
    request<T>(url, { method: 'POST', body: JSON.stringify(body) })

All components use apiGet / apiPost — a single point for adding authorization headers, error logging, and interceptors.

Design System: Tokens and UI Components

Colors, fonts, spacing — via CSS variables that match the variables in the Bitrix PHP template:

/* Defined in the Bitrix template, used by Vue components */
:root {
    --color-primary:   #0052cc;
    --color-success:   #00875a;
    --color-danger:    #de350b;
    --spacing-sm:      8px;
    --spacing-md:      16px;
    --border-radius:   4px;
}

The Vue Button.vue component uses these variables — visually compatible with the rest of the site.

Lazy Loading and Code Splitting

defineAsyncComponent + Vite automatically splits the code into chunks. CartDrawer.vue is loaded only when the user clicks the cart button — not in the initial bundle. This is critical for performance: the initial page does not load component code that may never be used.

// Load only on first interaction
const CartDrawer = defineAsyncComponent({
    loader: () => import('./components/cart/CartDrawer.vue'),
    loadingComponent: Spinner,
    delay: 200,
})

Testing the System

Unit tests for Pinia stores via Vitest:

// stores/cartStore.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from './cartStore'

describe('cartStore', () => {
    beforeEach(() => setActivePinia(createPinia()))

    it('adds a product to the cart', async () => {
        const store = useCartStore()
        await store.add(123, 1)
        expect(store.items).toHaveLength(1)
        expect(store.count).toBe(1)
    })
})

Component tests via Vue Test Utils + Vitest.

Build and Bitrix Integration

Vite is configured to build into /local/js/vue/dist/. In the Bitrix template it is connected via:

// In the template's header.php
\Bitrix\Main\Page\Asset::getInstance()->addJs('/local/js/vue/dist/app.js', true);
// Or via type=module for native ES modules
?>
<script type="module" src="/local/js/vue/dist/app.js"></script>

Timeline

Scale What's included Duration
Basic system (3–5 components) Initialization, Pinia, API layer, UI base 3–5 weeks
Full system + cart, wishlist, comparison, reviews 6–10 weeks
+ Design system, tests, CI + tokens, Vitest, auto-build +2–3 weeks

A component system is an investment in project maintainability. The first component is quick to write, but in a chaotic architecture; the tenth is already painful to duplicate. The right system from the first component saves time on every subsequent one.