Interactive Price Table Development on Vue.js for 1C-Bitrix
A price list in Bitrix looks at first glance like a task solvable with bitrix:catalog.section using a table template. Problems appear as soon as interactive requirements are added to the static output: filtering rows without a page reload, column sorting, switching price types (retail/wholesale/dealer), calculating the total amount when selecting items. Server-side rendering and jQuery post-processing scale poorly here — every new interactive element requires a separate handler and manual DOM synchronization.
Price Table Architecture
The Vue component PriceTable receives an array of items from Bitrix via window.BX_STATE.products. The data is assembled in result_modifier.php from the catalog module:
$priceIterator = \Bitrix\Catalog\PriceTable::getList([
'filter' => ['=CATALOG_GROUP_ID' => $priceTypeId, '=PRODUCT_ID' => $productIds],
'select' => ['PRODUCT_ID', 'PRICE', 'CURRENCY'],
]);
Each item in the array contains: id, name, article, unit, a set of prices by type, warehouse stock, and characteristics for filtering.
Reactive table capabilities:
Row filtering via computed:
const filteredProducts = computed(() => {
return products.value
.filter(p => selectedCategory.value ? p.categoryId === selectedCategory.value : true)
.filter(p => searchQuery.value
? p.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|| p.article.toLowerCase().includes(searchQuery.value.toLowerCase())
: true
)
.filter(p => showInStock.value ? p.stock > 0 : true);
});
Everything is computed locally — no server requests on every interaction. For a price list with 500–2,000 items this works instantly.
Sorting via sortKey and sortDirection refs. Clicking a column header toggles the direction. Numeric and string columns are handled separately.
Price type switching. Bitrix stores multiple prices per product in the b_catalog_price table (field CATALOG_GROUP_ID). All price types are passed in the JSON:
const activePriceType = ref('retail'); // 'retail' | 'wholesale' | 'dealer'
const displayPrice = (product) => product.prices[activePriceType.value];
Switching the price type is an instant reactive change with no server involvement.
Item Selection and Total Calculation
B2B price lists often need the ability to select items and get a total or submit a request. Checkboxes on each row, selectedIds — a Set in ref():
const total = computed(() =>
[...selectedIds.value]
.map(id => products.value.find(p => p.id === id))
.filter(Boolean)
.reduce((sum, p) => sum + displayPrice(p) * (quantities[p.id] || 1), 0)
);
The quantity field per row — v-model on quantities[product.id]. Changing the quantity instantly recalculates the total. Clicking "Submit request" — POST to a Bitrix handler with an array of {id, qty, price}.
Virtual Scroll for Large Price Lists
A price list with 5,000+ items cannot be fully rendered — the browser will stall. Virtualization via @vueuse/virtual-list or vue-virtual-scroller: only 50–100 visible rows are in the DOM at any time; the rest are computed on the fly. Filtering and search operate on the full in-memory array — only the rendering is virtual.
Case: Interactive Distributor Price List
A construction materials distributor, price list with 3,800 items, three price types (retail, small wholesale, large wholesale), filter by brand and category, search by article number.
Previous solution: an Excel file for download, updated manually once a week. An attempt to build an HTML table via bitrix:catalog.section hit a wall: the page took 8 seconds to load (3,800 rows in the DOM), and there was no search at all.
Solution: a Vue component with virtual scrolling. Data is loaded in a single AJAX request when the component mounts — a Bitrix controller returns a JSON with the full price list (~400 KB, ~80 KB gzipped). The request is cached in sessionStorage — subsequent page opens are instant.
Price type switching is only available to authorized users in the appropriate group — checked in Vue via window.BX_STATE.userGroups, plus server-side on every request. Unauthorized users only see the retail price.
Result: time to interactive — 1.2 seconds (JSON load + render). Article search — instant. Managers stopped sending Excel files.
Export and Print
"Download price list" — not a separate page, but CSV generation in the browser from the current filtered and sorted dataset:
function exportCsv() {
const rows = filteredProducts.value.map(p =>
[p.article, p.name, displayPrice(p), p.unit].join(';')
);
const blob = new Blob(['\uFEFF' + rows.join('\n')], { type: 'text/csv;charset=utf-8' });
// ... download link
}
The BOM \uFEFF is mandatory for correct opening in Excel on Windows.
Stages and Timelines
| Feature | Estimated timeline |
|---|---|
| Basic table with sorting and search | 2-3 days |
| Filtering + price type switching | 3-5 days |
| Item selection + total calculation + submission | 4-6 days |
| Virtualization for 3,000+ items | +2-3 days |
| CSV/Excel export | +1 day |
Development is iterative — core features first, extensions after review.







