Skip to main content

Let's Make an Interactive Coding Tutorial

· 16 min read
Hatem Hosny

Let's assume we want to make an interactive coding tutorial (for HTML, CSS and JavaScript) on a blog or a tutorials website. The tutorial should have a playground that helps us guide the students to write code, see the results and complete an assignment task.

We are going to use LiveCodes and its powerful SDK to achieve this. So, here we go.

What Are We Building?

We are going to build a playground for a simple interactive coding tutorial.

We will add some HTML, add styles using CSS and dynamically change its content using JavaScript. The student can interact with the playground, add code and see its result. At the end, the student is asked to complete a simple task (e.g. changing the value of a variable), then the task is verified and a feedback is given if the task was completed successfully.

Demo


What is LiveCodes?

LiveCodes is a feature-rich, open-source, client-side, code playground that supports 80+ languages and frameworks. Playgrounds can be embedded in any web pages. A powerful, yet easy-to-use, SDK allows creating and communicating with playgrounds.

LiveCodes is free with no limits to usage, no ads and no account required. It is MIT-licensed which allows its use, distribution and modification even for commercial projects.

Read more in the announcement.

Enough talk, let's get started!

The Playground

The LiveCodes SDK makes it easy to create code playgrounds. The SDK can be loaded from CDN or can be installed from npm. To keep things simple, we will just use the CDN.

The SDK is available for vanilla JavaScript/TypeScript, React, Vue and Svelte. Again, for simplicity, we will use vanilla JavaScript.

You can find the full source code (also in React) below.

A new playground can be created using the createPlayground function. It takes the container HTML element (or its selector) and optionally embed options.

This creates a playground that the user can interact with and see the result immediately, without having to install or build anything (not even for any of the supported languages).

It is generally a good practice to specify the SDK version and a permanent URL for the app to avoid later potential breaking changes with updates to the SDK or the app.

We now have an empty playground. Let's fill it with code.

Adding Content

When creating the playground, embed options can be used to add content (e.g. using the options config, params, import or template).

This loads the playground prefilled with the code you specified.

Adding Tutorial Steps

Now the student can see the playground, interact with it and see the result. So let's add some tutorial steps, where new code is added to the playground in different editors (HTML, CSS and JavaScript). This can gradually introduce new concepts to the student without losing context or having to move to a new playground.

The createPlayground function returns a promise that resolves to the playground instance. This instance has some useful methods that allows interaction with the already running playground (e.g. run, getConfig, setConfig, getCode, format, show, runTests, onChange, getShareUrl).

Now we have defined an array (steps) that will hold the data of each step in the tutorial. The first step has a config object that is to be sent to the playground.

A button with the id "next" controls moving between steps. An event handler for its click event sends the current step config to the playground using the method setConfig.

More Steps

So, let's add some more steps. Clicking the button should move us to the next step.

The variable step keeps track of the step index. The corresponding data is read from the steps array and sent to the playground.

However, there is now a lot of duplication in the code of each step. Also, if the student makes some changes to the HTML, the next step will overwrite them.

So, let's read the current config object from the playground (using the method getConfig) and then merge the changes for the new step. Now, we can remove the duplication in steps.

Changing Layout

We still have a problem!

Although we were able to add CSS code, the style editor was not activated. We can correct this by using the show method.

We can even control the position of the cursor, so that your student is guided where to start typing.

Now, let's add some steps for JavaScript code. We can even open the integrated console to show logs. Let's also change the text of the "next" button with each step.

Assignments

So, we are progressing very well!

Now we want our student to complete a task. We then need to validate if the task was completed successfully. This can be achieved by running automated tests.

LiveCodes supports running automated tests using Jest and Testing Library. You may test the page DOM or test the code exported from the script editor.

Let's write some tests for our tutorial.

import { screen } from "@testing-library/dom";
import "@testing-library/jest-dom";

test("Should display title", async () => {
expect(screen.getByText("Hello", { exact: false })).toHaveTextContent(
"Hello, LiveCodes!"
);
});

test("Title should be blue", async () => {
const style = window.getComputedStyle(document.querySelector("#title"));
expect(style.color).toBe("rgb(0, 0, 255)");
});

We test that the student was indeed able to complete the task (by changing the value of the variable newTitle to become "LiveCodes"). Also we are testing the color of the title.

These tests can be added to tests property of the config object.

We can then run the tests using the SDK method runTests. This method returns a promise that resolves to the test results, which we can then show to our student.

Final Result

This is the final result after adding tests, styles and some final touches.

Full Code

Show full code (vanilla JS)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LiveCodes Tutorial Demo</title>
<style>
body {
display: flex;
flex-direction: column;
font: 0.9em sans-serif;
justify-content: center;
margin: 10px 10%;
}
h1 {
text-align: center;
font-size: 1.5em;
color: rgb(72, 72, 72);
}
#container {
height: 60vh;
margin: auto;
}
#controls {
list-style: none;
display: flex;
gap: 1em;
justify-content: center;
flex-wrap: wrap;
max-width: 100%;
margin: 1em;
padding: 0;
}
#controls button {
width: 15em;
height: 2em;
background-color: rgb(243, 243, 243);
border: 1px solid grey;
border-radius: 2px;
cursor: pointer;
color: darkslategrey;
display: flex;
justify-content: center;
align-items: center;
}
#result {
margin: 1em;
text-align: center;
}
#result.success {
color: green;
}
#result.fail {
color: red;
}
</style>
</head>
<body>
<div id="container"></div>
<div id="controls">
<button id="next">Start Tutorial</button>
<button id="test">Check My Solution</button>
</div>
<div id="result"></div>
<script type="module">
import { createPlayground } from 'https://unpkg.com/[email protected]';

const steps = [
{
btnText: 'Next',
panel: 'markup',
position: { line: 2, column: 20 },
config: {
markup: {
language: 'html',
content: '<h1>Hello,\n <span id="title">World</span>!\n</h1>',
},
},
},
{
btnText: 'Next',
panel: 'style',
position: { line: 2, column: 10 },
config: {
style: {
language: 'css',
content: '#title {\n color: blue;\n}',
},
},
},
{
btnText: 'Next',
panel: 'script',
position: { line: 2, column: 19 },
config: {
script: {
language: 'javascript',
content:
'// change this value to "LiveCodes"\nconst newTitle = "???";\ndocument.querySelector("#title").textContent = newTitle;\nconsole.log(newTitle);',
},
},
},
{
btnText: 'Final Result',
panel: 'console',
},
{
btnText: 'Restart Tutorial',
panel: 'result',
},
];

const options = {
appUrl: 'https://v25.livecodes.io/',
loading: 'eager',
config: {
markup: {
language: 'html',
content: 'Welcome to the tutorial!<br>Start by clicking the button below.',
},
tests: {
language: 'javascript',
content:
'import { screen } from "@testing-library/dom";\nimport "@testing-library/jest-dom";\n\ntest("Should display title", async () => {\n expect(screen.getByText("Hello", { exact: false })).toHaveTextContent(\n "Hello, LiveCodes!"\n );\n});\n\ntest("Title should be blue", async () => {\n const style = window.getComputedStyle(document.querySelector("#title"));\n expect(style.color).toBe("rgb(0, 0, 255)");\n});',
},
},
};

createPlayground('#container', options).then((playground) => {
let step = 0;
const nextBtn = document.querySelector('#next');
const testsBtn = document.querySelector('#test');
const testResult = document.querySelector('#result');

nextBtn.addEventListener('click', async () => {
const lastStep = steps.length - 1;
const currentStep = steps[step];
const prevConfig = step === 0 ? options.config : await playground.getConfig();

await playground.setConfig({ ...prevConfig, ...currentStep.config });
await playground.show(currentStep.panel, currentStep.position);

nextBtn.textContent = currentStep.btnText;
step = step < lastStep ? step + 1 : 0;
});

testsBtn.addEventListener('click', async () => {
testResult.textContent = 'Checking your solution...';
testResult.classList.remove('success');
testResult.classList.remove('fail');

const { results } = await playground.runTests();
if (results.some((result) => result.status === 'fail')) {
testResult.textContent = 'Wrong answer, try again!';
testResult.classList.add('fail');
testResult.classList.remove('success');
} else {
testResult.textContent = 'Correct answer, well done!';
testResult.classList.add('success');
testResult.classList.remove('fail');
}
});
});
</script>
</body>
</html>
Show full code (React TSX)
import React, { useState } from 'react';
import LiveCodes from 'livecodes/react';
import type { Config, EmbedOptions, Playground } from 'livecodes';
import styles from './styles.module.css';

interface Step {
btnText: string;
panel?: 'markup' | 'style' | 'script' | 'console' | 'result';
position?: { line: number; column: number };
config?: Partial<Config>;
}

export default function () {
const steps: Step[] = [
{
btnText: 'Next',
panel: 'markup',
position: { line: 2, column: 20 },
config: {
markup: {
language: 'html',
content: '<h1>Hello,\n <span id="title">World</span>!\n</h1>',
},
},
},
{
btnText: 'Next',
panel: 'style',
position: { line: 2, column: 10 },
config: {
style: {
language: 'css',
content: '#title {\n color: blue;\n}',
},
},
},
{
btnText: 'Next',
panel: 'script',
position: { line: 2, column: 19 },
config: {
script: {
language: 'javascript',
content:
'// change this value to "LiveCodes"\nconst newTitle = "???";\ndocument.querySelector("#title").textContent = newTitle;\nconsole.log(newTitle);',
},
},
},
{
btnText: 'Final Result',
panel: 'console',
},
{
btnText: 'Restart Tutorial',
panel: 'result',
},
];

const options: EmbedOptions = {
appUrl: 'https://v25.livecodes.io/',
loading: 'eager',
config: {
markup: {
language: 'html',
content: 'Welcome to the tutorial!<br>Start by clicking the button below.',
},
tests: {
language: 'javascript',
content:
'import { screen } from "@testing-library/dom";\nimport "@testing-library/jest-dom";\n\ntest("Should display title", async () => {\n expect(screen.getByText("Hello", { exact: false })).toHaveTextContent(\n "Hello, LiveCodes!"\n );\n});\n\ntest("Title should be blue", async () => {\n const style = window.getComputedStyle(document.querySelector("#title"));\n expect(style.color).toBe("rgb(0, 0, 255)");\n});',
},
},
};

const [playground, setPlayground] = useState<Playground>();
const [step, setStep] = useState(0);
const [nextBtnText, setNextBtnText] = useState('Start Tutorial');
const [testResult, setTestResult] = useState('');
const [testResultClass, setTestResultClass] = useState('');

const onReady = (sdk: Playground) => {
setPlayground(sdk);
};

const next = async () => {
const lastStep = steps.length - 1;
const currentStep = steps[step];
const prevConfig =
step === 0 ? (options.config as Partial<Config>) : await playground?.getConfig();

if (currentStep.config) {
await playground?.setConfig({ ...prevConfig, ...currentStep.config });
}
await playground?.show(currentStep.panel, {
...currentStep.position,
});

setNextBtnText(currentStep.btnText);
setStep((s) => (s < lastStep ? s + 1 : 0));
};

const runTests = async () => {
setTestResult('Checking your solution...');
setTestResultClass('');

const { results } = await playground?.runTests();
if (results.some((result) => result.status === 'fail')) {
setTestResult('Wrong answer, try again!');
setTestResultClass(styles.fail);
} else {
setTestResult('Correct answer, well done!');
setTestResultClass(styles.success);
}
};

return (
<>
<LiveCodes sdkReady={onReady} {...options} />
<div className={styles.controls}>
<button onClick={next}>{nextBtnText}</button>
<button onClick={runTests}>Check My Solution</button>
</div>
<div className={styles.result + ' ' + testResultClass}>{testResult}</div>
</>
);
}

Conclusion

That was an interesting project to build!

Using LiveCodes SDK, we have built a very basic tutorial where the student can learn how to add HTML elements, style them with CSS and change the text content with JavaScript. We were even able to check if the assignment was completed successfully.

During our work, we had an idea about the LiveCodes SDK and how to use it. Please refer to the SDK documentations for more details.

What's Next?

You can now make your tutorials a lot more interactive and fun with LiveCodes. These tutorials can use any of the supported languages and frameworks. Tutorials may also cover your own libraries (with full editor IntelliSense).

You may want to create full courses and even sell them. LiveCodes is MIT-licensed.

Your imagination is the limit!

Tell Us What You Think

Please let us know what you think. If you have a case that the SDK does not cover, tell us about it.

If you do use LiveCodes for your tutorials/courses, we would love to know about them (we may even link to them!). And if you find LiveCodes to be useful, please give us a star on GitHub and please consider becoming a sponsor.

What will you learn/build/teach today?