Skip to main content
The LiveCodes SDK provides a powerful event system that allows you to watch for various playground events and react to changes in real-time.

The watch Method

The primary way to listen to events is through the watch method:
const watcher = playground.watch(eventName, callback);

// Later, remove the watcher
watcher.remove();

Signature

type WatchFn = (
  event: 'load' | 'ready' | 'code' | 'console' | 'tests' | 'destroy',
  callback: (data?: any) => void
) => { remove: () => void }

Available Events

load

Called when the playground first loads.
playground.watch('load', () => {
  console.log('Playground has loaded');
});
Callback Parameters: None When it fires: Once when the playground iframe loads for the first time. Example Use Case: Track analytics, show loading complete message

ready

Called when a new project is loaded and the playground is ready to run.
playground.watch('ready', ({ config }) => {
  console.log('Playground is ready with config:', config);
});
Callback Parameters:
  • config (Config): The current configuration object
When it fires:
  • After initial load
  • When importing a new project
  • When setConfig completes
Example Use Case: Auto-run code when ready, update UI state
playground.watch('ready', async ({ config }) => {
  console.log('Loaded project:', config.title);
  // Auto-run the code
  await playground.run();
});

code

Called when the playground content changes.
playground.watch('code', ({ code, config }) => {
  console.log('Code changed!');
  console.log('Script content:', code.script.content);
  console.log('Config:', config);
});
Callback Parameters:
  • code (Code): Current code in all editors
  • config (Config): Current configuration
When it fires: When content changes in:
  • Code editors
  • Editor languages
  • CSS processors
  • External resources
  • Custom settings
  • Project info
  • Project title
  • Test code
Example Use Case: Save to localStorage, sync with server, show unsaved indicator
let hasUnsavedChanges = false;

playground.watch('code', ({ code, config }) => {
  hasUnsavedChanges = true;
  // Save to localStorage
  localStorage.setItem('draft', JSON.stringify({ code, config }));
});

console

Called when console output occurs in the result page.
playground.watch('console', ({ method, args }) => {
  console[method](...args);
});
Callback Parameters:
  • method (string): Console method name ('log', 'info', 'warn', 'error', etc.)
  • args (any[]): Arguments passed to the console method
When it fires: When code in the result page calls console methods Example Use Case: Mirror console output, log monitoring, debugging
const logs = [];

playground.watch('console', ({ method, args }) => {
  logs.push({ method, args, timestamp: Date.now() });
  
  // Mirror to parent console
  console[method]('[Playground]', ...args);
});

tests

Called when tests run and produce results.
playground.watch('tests', ({ results, error }) => {
  results.forEach((result) => {
    console.log('Test:', result.testPath.join(' > '));
    console.log('Status:', result.status);
    console.log('Duration:', result.duration, 'ms');
    if (result.errors.length > 0) {
      console.error('Errors:', result.errors);
    }
  });
});
Callback Parameters:
  • results (TestResult[]): Array of test results
  • error (string, optional): Error message if tests failed to run
When it fires: After tests run (manually or via auto-test) Example Use Case: Display test results, CI/CD integration, test reporting
playground.watch('tests', ({ results, error }) => {
  if (error) {
    console.error('Test error:', error);
    return;
  }
  
  const passed = results.filter(r => r.status === 'pass').length;
  const failed = results.filter(r => r.status === 'fail').length;
  
  console.log(`Tests: ${passed} passed, ${failed} failed`);
});

destroy

Called when the playground is destroyed.
playground.watch('destroy', () => {
  console.log('Playground destroyed');
});
Callback Parameters: None When it fires: When playground.destroy() is called Example Use Case: Cleanup, analytics, state management
playground.watch('destroy', () => {
  console.log('Cleaning up playground resources');
  // Perform cleanup
});

Removing Watchers

Always remove watchers when you no longer need them to prevent memory leaks:
const watcher = playground.watch('code', ({ code }) => {
  console.log('Code changed');
});

// Later...
watcher.remove();

Multiple Watchers

const watchers = [];

watchers.push(
  playground.watch('code', handleCodeChange),
  playground.watch('console', handleConsole),
  playground.watch('tests', handleTests)
);

// Remove all watchers
watchers.forEach(w => w.remove());

Complete Examples

React Example

import { useEffect, useState } from 'react';
import { createPlayground } from 'livecodes';

function PlaygroundWithEvents() {
  const [playground, setPlayground] = useState(null);
  const [logs, setLogs] = useState([]);

  useEffect(() => {
    createPlayground('#container', { template: 'javascript' })
      .then(setPlayground);
  }, []);

  useEffect(() => {
    if (!playground) return;

    const watchers = [
      playground.watch('ready', () => {
        console.log('Ready!');
      }),
      playground.watch('console', ({ method, args }) => {
        setLogs(prev => [...prev, { method, args }]);
      }),
    ];

    return () => watchers.forEach(w => w.remove());
  }, [playground]);

  return (
    <div>
      <div id="container" />
      <div>
        {logs.map((log, i) => (
          <div key={i}>{log.method}: {JSON.stringify(log.args)}</div>
        ))}
      </div>
    </div>
  );
}

Vue Example

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { createPlayground } from 'livecodes';

const container = ref(null);
const playground = ref(null);
const logs = ref([]);
const watchers = [];

onMounted(async () => {
  playground.value = await createPlayground(container.value, {
    template: 'javascript',
  });

  watchers.push(
    playground.value.watch('ready', () => {
      console.log('Ready!');
    }),
    playground.value.watch('console', ({ method, args }) => {
      logs.value.push({ method, args });
    })
  );
});

onUnmounted(() => {
  watchers.forEach(w => w.remove());
  playground.value?.destroy();
});
</script>

<template>
  <div>
    <div ref="container"></div>
    <div v-for="(log, i) in logs" :key="i">
      {{ log.method }}: {{ JSON.stringify(log.args) }}
    </div>
  </div>
</template>

Svelte Example

<script>
import { onMount, onDestroy } from 'svelte';
import { createPlayground } from 'livecodes';

let container;
let playground;
let logs = [];
let watchers = [];

onMount(async () => {
  playground = await createPlayground(container, {
    template: 'javascript',
  });

  watchers.push(
    playground.watch('ready', () => {
      console.log('Ready!');
    }),
    playground.watch('console', ({ method, args }) => {
      logs = [...logs, { method, args }];
    })
  );
});

onDestroy(() => {
  watchers.forEach(w => w.remove());
  playground?.destroy();
});
</script>

<div>
  <div bind:this={container}></div>
  <div>
    {#each logs as log, i (i)}
      <div>{log.method}: {JSON.stringify(log.args)}</div>
    {/each}
  </div>
</div>

TestResult Interface

The test results object structure:
interface TestResult {
  duration: number;        // Test duration in milliseconds
  errors: string[];        // Array of error messages
  status: 'pass' | 'fail' | 'skip';  // Test status
  testPath: string[];      // Path to the test (e.g., ['describe block', 'test name'])
}
Example:
playground.watch('tests', ({ results }) => {
  results.forEach(result => {
    if (result.status === 'fail') {
      console.error(`❌ ${result.testPath.join(' > ')}`);
      result.errors.forEach(err => console.error('  ', err));
    } else if (result.status === 'pass') {
      console.log(`✅ ${result.testPath.join(' > ')} (${result.duration}ms)`);
    }
  });
});

Deprecated: onChange

The onChange method is deprecated. Use watch('code', callback) instead.
// ❌ Deprecated
playground.onChange(({ code, config }) => {
  console.log('Code changed');
});

// ✅ Use this instead
playground.watch('code', ({ code, config }) => {
  console.log('Code changed');
});

Event Flow

Typical event sequence:
  1. load - Playground iframe loads
  2. ready - Project is loaded and ready
  3. code - User modifies code (can fire multiple times)
  4. console - Code runs and produces console output
  5. tests - Tests run (if triggered)
  6. destroy - Playground is destroyed
playground.watch('load', () => console.log('1. Load'));
playground.watch('ready', () => console.log('2. Ready'));
playground.watch('code', () => console.log('3. Code changed'));
playground.watch('console', () => console.log('4. Console output'));
playground.watch('tests', () => console.log('5. Tests ran'));
playground.watch('destroy', () => console.log('6. Destroyed'));

Best Practices

  1. Always remove watchers: Prevent memory leaks by calling remove() when done
  2. Handle errors: Wrap callbacks in try-catch for production code
  3. Debounce expensive operations: For code events, consider debouncing
let debounceTimer;

playground.watch('code', ({ code }) => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    saveToServer(code);
  }, 1000);
});
  1. Check playground state: Ensure playground isn’t destroyed before calling methods
  2. Use TypeScript: Get type safety for event data
import type { Code, Config, TestResult } from 'livecodes';

playground.watch('code', ({ code, config }: { code: Code; config: Config }) => {
  // Fully typed!
});

Next Steps

Methods

Learn about all available playground methods

Types

Browse complete TypeScript type definitions

API Reference

Review the complete API reference

React Component

Use events with the React component