UI Framework
BeamNG UI Development Cheat Sheet
40+ high-frequency one-liners and snippets for UI development.
40+ high-frequency one-liners and snippets for UI development.
| Task | Code |
|---|
| Listen for event | events.on('EventName', (data) => { ... }) |
| Listen once | events.once('EventName', (data) => { ... }) |
| Remove listener | events.off('EventName', handler) |
| Emit event | events.emit('CustomEvent', data) |
| Auto-cleanup events | const events = useEvents() |
// Setup
import { useEvents } from '@/services/events'
const events = useEvents()
events.on('VehicleChange', () => { /* ... */ })
| Task | Code |
|---|
| Subscribe to streams | useStreams(['electrics'], handler) |
| Multiple streams | useStreams(['electrics', 'sensors'], handler) |
| Manual subscribe | $game.streams.add(['electrics']) |
| Manual unsubscribe | $game.streams.remove(['electrics']) |
| Get speed | streams.electrics?.wheelspeed |
| Get RPM | streams.electrics?.rpm |
| Get gear | streams.electrics?.gear |
| Get yaw | streams.sensors?.yaw |
// Setup
import { useStreams } from '@/services/events'
useStreams(['electrics'], (streams) => {
if (streams.electrics) {
speed.value = streams.electrics.wheelspeed * 3.6 // km/h
}
})
| Task | Code |
|---|
| Call extension | await lua.myMod_myExtension.myFunc() |
| With params | await lua.myExt.func(arg1, arg2) |
| Get vehicle details | await lua.core_vehicles.getCurrentVehicleDetails() |
| Check career active | await lua.career_career.isActive() |
| Raw Lua | await runRaw('print("hi")') |
| Vehicle Lua | api.activeObjectLua('electrics.setIgnitionLevel(2)') |
// Setup
import { lua } from '@/bridge'
import { runRaw } from '@/bridge/libs/Lua.js'
const data = await lua.myExtension.getData()
| Component | Usage |
|---|
| Button | <BngButton @click="fn">Text</BngButton> |
| Icon Button | <BngButton :icon="icons.save" /> |
| Primary Button | <BngButton :accent="ACCENTS.primary"> |
| Icon | <BngIcon type="engine" /> |
| Input | <BngInput v-model="text" /> |
| Slider | <BngSlider v-model="val" :min="0" :max="100" /> |
| Switch | <BngSwitch v-model="enabled" /> |
| Dropdown | <BngDropdown v-model="sel" :items="opts" /> |
| Card | <BngCard>Content</BngCard> |
| Binding | <BngBinding ui-event="back" /> |
// Import
import { BngButton, BngIcon, ACCENTS, icons } from '@/common/components/base'
| Directive | Usage |
|---|
| Blur background | v-bng-blur |
| Tooltip | v-bng-tooltip="'Text'" |
| Tooltip top | v-bng-tooltip:top="'Text'" |
| Disabled | v-bng-disabled="!ready" |
| UI Nav handler | v-bng-on-ui-nav:back="goBack" |
| Scoped nav | v-bng-scoped-nav="{ activateOnMount: true }" |
| Focus if | v-bng-focus-if="shouldFocus" |
// Import
import { vBngBlur, vBngTooltip, vBngOnUiNav } from '@/common/directives'
| Task | Code |
|---|
| Message | await openMessage('Title', 'Message') |
| Confirm | await openConfirmation('Title', 'Message') |
| Prompt | await openPrompt('Enter name:', 'Title') |
| Progress | openProgress('Loading...', 'Title') |
// Import
import { openMessage, openConfirmation, openPrompt } from '@/services/popup'
const confirmed = await openConfirmation('Confirm', 'Are you sure?')
if (confirmed) { /* ... */ }
| Task | Code |
|---|
| Instant translate | $translate.instant('ui.common.okay') |
| Template | {{ $t('ui.common.okay') }} |
| Context translate | $translate.contextTranslate({txt: 'key', context: {}}) |
| Template context | {{ $ctx_t({txt: 'key', context: {}}) }} |
// Import
import { $translate } from '@/services'
const text = $translate.instant('ui.common.confirm')
| Task | Code |
|---|
| Push route | router.push('/path') |
| Named route | router.push({ name: 'myRoute' }) |
| With params | router.push({ name: 'route', params: { id: '1' } }) |
| Go back | router.back() |
| Angular state | bngVue.gotoAngularState('menu.mainmenu') |
| Game state | bngVue.gotoGameState('garagemode') |
// Import
import { useRouter } from 'vue-router'
const router = useRouter()
router.push({ name: 'vehicleInventory' })
| Task | Code |
|---|
| Create storage | new Storage('myApp') |
| Get value | storage.get('key', default) |
| Set value | storage.set('key', value) |
| Check exists | storage.has('key') |
| Delete | storage.del('key') |
| Reactive values | storage.values.key |
// Import
import Storage from '@/services/storage'
const storage = new Storage('myApp', { theme: 'dark' })
storage.values.theme = 'light' // Auto-saves
<!-- Minimal UI App -->
<template>
<div class="my-app">{{ data }}</div>
</template>
<script setup>
import { ref } from 'vue'
import { useStreams } from '@/services/events'
const data = ref(0)
useStreams(['electrics'], (s) => {
if (s.electrics) data.value = s.electrics.wheelspeed
})
</script>
<style scoped>
.my-app {
background: rgba(0,0,0,0.5);
color: white;
}
</style>
// routes.js
import MyView from './views/MyView.vue'
export default [
{
path: '/myModule',
name: 'myModule',
component: MyView,
meta: {
uiApps: { shown: false },
infoBar: { visible: true }
}
}
]
// electrics
streams.electrics.wheelspeed // m/s
streams.electrics.rpm // RPM
streams.electrics.gear // gear number
streams.electrics.fuel // 0-1
streams.electrics.lowbeam // boolean
streams.electrics.ignitionLevel // 0-2
// sensors
streams.sensors.yaw // radians
streams.sensors.pitch // radians
streams.sensors.roll // radians
streams.sensors.gx // G-force
streams.sensors.gy
streams.sensors.gz
| Event | Data | Purpose |
|---|
VehicleChange | - | Vehicle switched |
VehicleFocusChanged | {mode} | Camera changed |
ChangeState | {state} | Navigate |
Message | {text,icon,ttl} | HUD message |
toastrMsg | {type,title,msg} | Toast |
MenuHide | - | Close menu |
// Career
await lua.career_career.isActive()
await lua.career_modules_inventory.sendDataToUi()
await lua.career_modules_playerAttributes.getAttribute('money')
// Vehicles
await lua.core_vehicles.getCurrentVehicleDetails()
await lua.core_vehicles.getModel()
// Extensions
await lua.extensions.load('myExtension')
await lua.extensions.myExtension.myFunction()
// Garage
await lua.extensions.gameplay_garageMode.setGarageMenuState('tuning')
// Common variables
color: var(--bng-off-white);
background: rgba(var(--bng-off-black-rgb), 0.5);
border-radius: var(--bng-corners-1);
font-family: var(--fnt-defs);
// Icon sizing
.icon {
--bng-icon-size: 2em;
--bng-icon-color: var(--bng-orange);
}
// Full screen layout
.my-view {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
padding: 2em;
}
// Components
import { BngButton, BngIcon, BngInput, ACCENTS, icons } from '@/common/components/base'
// Directives
import { vBngBlur, vBngTooltip, vBngOnUiNav, vBngDisabled } from '@/common/directives'
// Services
import { useEvents, useStreams } from '@/services/events'
import { openMessage, openConfirmation } from '@/services/popup'
import { $translate } from '@/services'
import Storage from '@/services/storage'
// Bridge
import { lua } from '@/bridge'
import { useBridge } from '@/bridge'
import { runRaw } from '@/bridge/libs/Lua.js'
// Router
import { useRouter, useRoute } from 'vue-router'
import { onMounted, onUnmounted, ref } from 'vue'
import { useEvents, useStreams } from '@/services/events'
import { lua } from '@/bridge'
const data = ref(null)
const events = useEvents()
onMounted(async () => {
// Fetch initial data
data.value = await lua.myExtension.getData()
// Listen for updates
events.on('DataUpdated', (d) => { data.value = d })
})
// Streams auto-cleanup with useStreams()
useStreams(['electrics'], (s) => { /* ... */ })
// Events auto-cleanup with useEvents()
async function safeLuaCall() {
try {
const result = await lua.myExtension.riskyFunction()
return result
} catch (e) {
console.error('Lua call failed:', e)
await openMessage('Error', 'Operation failed')
return null
}
}