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

Build docs developers (and LLMs) love