Events System¶
Overview¶
Balancy's event system provides hooks into the UI lifecycle, button interactions, and backend notifications. Use these events to coordinate initialization, respond to user actions, and react to backend changes.
Event Categories:
- Lifecycle Events: Initialization stages
- Component Events: Component system lifecycle (new!)
- Button Events: User interactions
- Backend Events: Server notifications
- Custom Events: Custom messaging
Lifecycle Events¶
These events fire during view initialization, allowing you to coordinate async operations and delay the ready signal.
Event Flow¶
Page Load
↓
balancy-buttons-complete ← All buttons processed
↓
balancy-localization-complete ← All text localized
↓
balancy-text-complete ← All dynamic text updated
↓
balancy-images-complete ← All images loaded
↓
balancy-fonts-complete ← All fonts loaded
↓
balancy-components-complete ← All script components initialized (NEW)
↓
balancy-audio-complete ← All audio prepared
↓
balancy-ready ← Everything ready, view shown
balancy-ready¶
Fired when all initialization is complete and the view is ready to be shown.
Usage:
document.addEventListener('balancy-ready', () => {
console.log('Balancy UI is fully initialized');
// Start animations, timers, etc.
});
// Or with once flag
window.addEventListener('balancy-ready', main, { once: true });
async function main() {
// Your initialization code
const offers = await balancy.getActiveOffers();
displayOffers(offers);
}
Delaying Ready Signal:
If you need to load additional data before showing the view:
function main() {
// Prevent automatic ready signal
balancy.delayIsReady();
// Load your data
loadCustomData().then(() => {
// Signal ready when done
balancy.sendIsReady();
});
}
window.addEventListener('balancy-ready', main, { once: true });
balancy-components-complete (New!)¶
Fired when all script components have been initialized (after awake() and onEnable() called).
Usage:
document.addEventListener('balancy-components-complete', () => {
console.log('All components initialized');
// Safe to query components
const shopScript = elementsManager.getElementByGuid('shop-guid')
.getComponent(Balancy_UI_LiveOps_Shop);
shopScript.init(shopData);
});
balancy-images-complete¶
Fired when all images with data-image-id have been loaded.
Usage:
document.addEventListener('balancy-images-complete', () => {
console.log('All images loaded');
// Hide loading spinner
hideLoadingSpinner();
});
balancy-localization-complete¶
Fired when all elements with data-text-type="localized" have been localized.
Usage:
document.addEventListener('balancy-localization-complete', () => {
console.log('All text localized');
// Adjust layouts based on text length
adjustLayouts();
});
Other Lifecycle Events¶
// Buttons processed
document.addEventListener('balancy-buttons-complete', () => {
console.log('All buttons ready');
});
// Dynamic text updated
document.addEventListener('balancy-text-complete', () => {
console.log('All dynamic text updated');
});
// Fonts loaded
document.addEventListener('balancy-fonts-complete', () => {
console.log('All fonts loaded');
});
// Audio prepared
document.addEventListener('balancy-audio-complete', () => {
console.log('All audio ready');
});
Component Lifecycle Events (New!)¶
When using the component system, scripts have lifecycle methods that are called automatically:
awake()¶
Called immediately when component is created, after serialized parameters are applied.
class MyScript extends balancy.ElementBehaviour {
// @serialize {number}
maxHealth = 100;
awake() {
// Parameters already applied
console.log('Max health:', this.maxHealth);
// Set up event listeners
this.element.addEventListener('click', () => this.onClick());
// Initialize state
this.currentHealth = this.maxHealth;
}
}
start()¶
Called once before the first update(), after all components have called awake().
class MyScript extends balancy.ElementBehaviour {
start() {
// Safe to get references to other components
this.healthBar = this.getComponent(HealthBar);
// Start animations
this.startAnimation();
}
}
update(deltaTime)¶
Called every frame (typically 60 FPS). deltaTime is in seconds.
class MyScript extends balancy.ElementBehaviour {
update(deltaTime) {
// Update position
this.position += this.velocity * deltaTime;
// Update timer
this.timer -= deltaTime;
if (this.timer <= 0) {
this.onTimerExpired();
}
}
}
onEnable() / onDisable()¶
Called when component or element becomes active/inactive.
class MyScript extends balancy.ElementBehaviour {
onEnable() {
console.log('Component enabled');
this.startTimer();
}
onDisable() {
console.log('Component disabled');
this.stopTimer();
}
}
Triggered by:
balancy.ElementsManager.setActive(element, true/false)component.enabled = true/false- Parent element visibility changes
onDestroy()¶
Called when element is being destroyed.
class MyScript extends balancy.ElementBehaviour {
onDestroy() {
// Clean up timers
if (this.timer) clearInterval(this.timer);
// Remove event listeners
this.element.removeEventListener('click', this.clickHandler);
// Release references
this.targetElement = null;
}
}
Learn More: Prefabs & Components
Button Interaction Events¶
balancyButtonResponse¶
Fired when a button action completes (success or failure).
Event Detail:
{
actionId: number, // Action ID from RequestAction
result: any, // Response data
success: boolean, // Whether action succeeded
senderId: string, // Element ID that triggered action
error?: string // Error message (if failed)
}
Usage:
const button = document.getElementById('buy-button');
button.addEventListener('balancyButtonResponse', (event) => {
const { actionId, result, success, senderId } = event.detail;
if (success) {
console.log(`Action ${actionId} succeeded:`, result);
showSuccessAnimation();
updatePlayerCurrency();
} else {
console.log(`Action ${actionId} failed:`, event.detail.error);
showErrorDialog(event.detail.error);
}
});
balancyButtonError¶
Fired when a button action encounters an error before execution.
Event Detail:
{
actionId: number,
error: string,
paramsAttr: string // The data-button-params value
}
Usage:
button.addEventListener('balancyButtonError', (event) => {
const { actionId, error, paramsAttr } = event.detail;
console.error(`Button error: ${error}`);
console.error(`Action: ${actionId}, Params: ${paramsAttr}`);
// Show user-friendly error
showErrorToast('Something went wrong. Please try again.');
});
Example: Complete Purchase Flow¶
const buyButton = document.getElementById('buy-gems-button');
// Handle successful/failed purchases
buyButton.addEventListener('balancyButtonResponse', (event) => {
if (event.detail.success) {
// Purchase succeeded
const gems = event.detail.result.gemsAwarded;
// Show success
showSuccessMessage(`You got ${gems} gems!`);
// Update UI
updateGemsDisplay();
// Play sound
playSound('purchase_success');
} else {
// Purchase failed
showErrorDialog(event.detail.error);
}
});
// Handle errors
buyButton.addEventListener('balancyButtonError', (event) => {
console.error('Purchase error:', event.detail.error);
showErrorDialog('Purchase failed. Please try again.');
});
Backend Notification Events¶
balancy-notification¶
Custom event dispatched for all backend notifications.
Usage:
window.addEventListener('balancy-notification', (event) => {
const data = event.detail;
console.log('Backend notification:', data);
switch (data.type) {
case balancy.NotificationType.OnOfferDeactivated:
handleOfferDeactivated(data);
break;
case balancy.NotificationType.OnOfferGroupWasPurchased:
handleOfferPurchased(data);
break;
}
});
Notification Types¶
balancy.NotificationType = {
OnOfferDeactivated: 101, // Offer expired/deactivated
OnOfferGroupDeactivated: 102, // Group offer expired
OnOfferGroupWasPurchased: 122 // Group offer purchased
};
Override Notification Handler¶
You can override the default notification handler:
// Save original handler
const originalHandler = balancy.notificationReceived;
balancy.notificationReceived = function(data) {
console.log('Notification received:', data);
// Custom logic
if (data.type === balancy.NotificationType.OnOfferDeactivated) {
if (window.balancyViewOwner.instanceId === data.id) {
// Close view if this offer expired
balancy.closeView('Offer expired');
}
}
// Call original handler
originalHandler.call(this, data);
// Or handle completely custom
if (data.type === balancy.NotificationType.OnOfferGroupWasPurchased) {
refreshOfferList();
showCelebrationAnimation();
}
};
Example: Auto-close on Expiration¶
window.addEventListener('balancy-notification', (event) => {
const data = event.detail;
// Close view if current offer expired
if (data.type === balancy.NotificationType.OnOfferDeactivated) {
if (window.balancyViewOwner.instanceId === data.id) {
// Show expiration message
showToast('This offer has expired');
// Close after delay
setTimeout(() => {
balancy.closeView('Offer expired');
}, 2000);
}
}
});
Custom Message Events¶
Balancy provides two-way communication between your game code and Views. For complete documentation including SDK-side setup (intercepting messages, sending responses, broadcasting), see Working with Views - Custom Messages.
balancy-custom-message¶
Fired when receiving custom messages from the SDK.
Usage:
// Method 1: DOM event
window.addEventListener('balancy-custom-message', (event) => {
const data = event.detail;
console.log('Custom message:', data);
if (data.action === 'score-update') {
updateScoreDisplay(data.score);
}
});
// Method 2: Subscribe with callback (recommended)
const unsubscribe = balancy.subscribeToCustomMessages((data) => {
console.log('Custom message:', data);
switch (data.action) {
case 'score-update':
updateScoreDisplay(data.score);
break;
case 'achievement':
showAchievementPopup(data.achievementId);
break;
}
});
// Unsubscribe when done
unsubscribe();
Multiple Subscribers:
// Analytics subscriber
balancy.subscribeToCustomMessages((data) => {
if (data.action === 'track-event') {
analytics.track(data.eventName, data.properties);
}
});
// UI subscriber
balancy.subscribeToCustomMessages((data) => {
if (data.action === 'update-ui') {
refreshUI();
}
});
// Both subscribers receive all messages
SDK Setup:
To send messages from your game code to Views, you need to use Balancy.Actions.View.SendCustomMessageToView(). For complete examples including request-response patterns and message interception, see Custom Messages Documentation.
Event Best Practices¶
1. Use once Flag for Initialization¶
// ✓ Good: Automatically removes listener after first call
window.addEventListener('balancy-ready', main, { once: true });
// ✗ Avoid: Listener stays attached
window.addEventListener('balancy-ready', main);
2. Clean Up Event Listeners¶
class MyComponent extends balancy.ElementBehaviour {
awake() {
this.clickHandler = () => this.onClick();
this.element.addEventListener('click', this.clickHandler);
}
onDestroy() {
// ✓ Good: Remove listener
this.element.removeEventListener('click', this.clickHandler);
}
}
3. Handle Errors Gracefully¶
// ✓ Good: Handle both success and error
button.addEventListener('balancyButtonResponse', (event) => {
if (event.detail.success) {
handleSuccess(event.detail.result);
} else {
handleError(event.detail.error);
}
});
button.addEventListener('balancyButtonError', (event) => {
handleError(event.detail.error);
});
// ✗ Avoid: Only handling success
button.addEventListener('balancyButtonResponse', (event) => {
handleSuccess(event.detail.result); // Will break on errors!
});
4. Coordinate Async Operations¶
// ✓ Good: Wait for multiple events
let imagesReady = false;
let componentsReady = false;
document.addEventListener('balancy-images-complete', () => {
imagesReady = true;
checkAllReady();
}, { once: true });
document.addEventListener('balancy-components-complete', () => {
componentsReady = true;
checkAllReady();
}, { once: true });
function checkAllReady() {
if (imagesReady && componentsReady) {
startGame();
}
}
5. Use Custom Events for Communication¶
// ✓ Good: Decouple components with custom events
class ScoreManager extends balancy.ElementBehaviour {
addScore(points) {
this.score += points;
// Dispatch custom event
const event = new CustomEvent('score-changed', {
detail: { score: this.score, points }
});
window.dispatchEvent(event);
}
}
class ScoreDisplay extends balancy.ElementBehaviour {
awake() {
window.addEventListener('score-changed', (event) => {
this.updateDisplay(event.detail.score);
});
}
}
Common Patterns¶
Pattern 1: Initialization Sequence¶
async function main() {
balancy.delayIsReady();
try {
// Load data in parallel
const [offers, playerData] = await Promise.all([
balancy.getActiveOffers(),
balancy.getSystemProfileValue('GeneralInfo')
]);
// Initialize UI
displayOffers(offers);
displayPlayerInfo(playerData);
// Signal ready
balancy.sendIsReady();
} catch (error) {
console.error('Initialization failed:', error);
balancy.closeView('Initialization error');
}
}
window.addEventListener('balancy-ready', main, { once: true });
Pattern 2: Timer with Expiration¶
class OfferTimer extends balancy.ElementBehaviour {
// @serialize {element}
timerText = null;
_updateInterval = 1.0;
_timeSinceUpdate = 0;
update(deltaTime) {
this._timeSinceUpdate += deltaTime;
if (this._timeSinceUpdate >= this._updateInterval) {
this._timeSinceUpdate = 0;
this.updateTimer();
}
}
updateTimer() {
const timeLeft = balancy.getTimeLeft();
this.timerText.element.textContent = balancy.formatTime(timeLeft);
if (timeLeft <= 0) {
// Dispatch expiration event
const event = new CustomEvent('timer-expired');
window.dispatchEvent(event);
// Close view
balancy.closeView('Timer expired');
}
}
}
Pattern 3: Purchase Flow with State¶
class PurchaseManager extends balancy.ElementBehaviour {
// @serialize {element}
buyButton = null;
awake() {
this.buyButton.element.addEventListener('balancyButtonResponse',
(e) => this.handlePurchase(e));
}
handlePurchase(event) {
if (event.detail.success) {
// Dispatch success event
const successEvent = new CustomEvent('purchase-success', {
detail: event.detail.result
});
window.dispatchEvent(successEvent);
// Update button state
const buttonScript = this.buyButton.getComponent(Balancy_UI_Common_BuyButton);
buttonScript.setState(balancy.BuyButtonState.Cooldown, {
cooldownSeconds: 3600
});
} else {
// Dispatch error event
const errorEvent = new CustomEvent('purchase-error', {
detail: { error: event.detail.error }
});
window.dispatchEvent(errorEvent);
}
}
}
// Listen for purchase events
window.addEventListener('purchase-success', (event) => {
showSuccessAnimation();
updatePlayerCurrency();
});
window.addEventListener('purchase-error', (event) => {
showErrorDialog(event.detail.error);
});
Troubleshooting¶
Events Not Firing¶
Problem: Event listener never called.
Solutions:
- Check event name spelling
- Verify listener added before event fires
- Use
{ once: true }carefully (listener removed after first call) - Check browser console for errors
Multiple Event Calls¶
Problem: Event handler called multiple times.
Solutions:
- Use
{ once: true }flag for initialization events - Remove listeners in
onDestroy() - Check if listener added multiple times
- Use event delegation for dynamic elements
Race Conditions¶
Problem: Events fire in unexpected order.
Solutions:
- Don't assume event order
- Use flags to track state
- Wait for specific combination of events
- Use
balancy.delayIsReady()for async operations
Next Steps¶
- Learn Data Attributes for declarative UI
- Check Balancy API for methods referenced in events
- See Prefabs & Components for lifecycle methods
- Explore Templates for working examples