DEV Community

Cover image for Let's Make an Interactive Coding Tutorial
Hatem Hosny for LiveCodes

Posted on • Updated on • Originally published at blog.livecodes.io

Let's Make an Interactive Coding Tutorial

Let's assume we want to make an interactive coding tutorial 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.

This article was originally published at LiveCodes blog.


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

Check a live demo on LiveCodes blog.

The full source code (in vanilla JavaScript and also in React TSX) can be found here.


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.

GitHub logo live-codes / livecodes

Code Playground That Just Works!

Ceasefire_Now-techforpalestine.org

LiveCodes

Code Playground That Just Works!

A feature-rich, open-source, client-side code playground for React, Vue, Svelte, Solid, Typescript, Python, Go, Ruby, PHP and 80+ languages/frameworks.

LiveCodes: uptime status LiveCodes: app version LiveCodes: npm version LiveCodes: npm downloads LiveCodes: languages Codacy Badge license - MIT LiveCodes: GitHub repo

Try it now on livecodes.io

Documentations

What makes LiveCodes different?

LiveCodes list of languages screenshot

Code Playground That Just Works!

  • No servers to configure (or pay for!)
  • No databases to maintain (or pay for!)
  • No installs
  • No configuration files
  • No build steps
  • No subscription fees (free and open-source)
  • No account required *
  • No limits for usage (unlimited private projects)
  • 80+ languages/frameworks/processors
  • Large set of features and integrations
  • Import code from a wide variety of sources
  • Use modules from npm, deno.land/x, jsr, GitHub, and others
  • Easily embed it in your web pages
  • It runs in the browser (client-side)

* GitHub account is required only for features that use GitHub Integration.

Quick Start

Standalone App

  1. Go to livecodes.io

... and enjoy all the features!

Embedded Playground

Add this…

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.

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

<div id="container"></div>

<script type="module">
  import { createPlayground } from 'https://unpkg.com/livecodes';

  createPlayground('#container', { /* embed options */ });
</script>
Enter fullscreen mode Exit fullscreen mode

Preview

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.

<div id="container"></div>

<script type="module">
  import { createPlayground } from 'https://unpkg.com/livecodes@0.4.0';

  const options = {
    // permanent URL avoids later breaking changes
    appUrl: 'https://v25.livecodes.io',
  };
  createPlayground('#container', options);
</script>
Enter fullscreen mode Exit fullscreen mode

Preview

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.

<div id="container"></div>

<script type="module">
  import { createPlayground } from 'https://unpkg.com/livecodes@0.4.0';

  const options = {
    appUrl: 'https://v25.livecodes.io',
    config: {
      markup: {
        language: 'html',
        content: 'Welcome to the tutorial!',
      },
    },
  };
  createPlayground('#container', options);
</script>
Enter fullscreen mode Exit fullscreen mode

Preview

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).

<div id="container"></div>
<button id="next">Next</button>

<script type="module">
  import { createPlayground } from "https://unpkg.com/livecodes@0.4.0";

  // add data for tutorial steps
  const steps = [
    {
      config: {
        markup: {
          language: "html",
          content: '<h1>Hello,\n  <span id="title">World</span>!\n</h1>',
        },
      },
    },
  ];

  const options = {
    appUrl: "https://v25.livecodes.io",
    config: {
      markup: {
        language: "html",
        content: "Welcome to the tutorial!",
      },
    },
  };

  createPlayground("#container", options).then((playground) => {
    const nextBtn = document.querySelector("#next");
    nextBtn.addEventListener("click", async () => {
      const currentStep = steps[0];
      // send the config object to the playground
      await playground.setConfig(currentStep.config);
    });
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Preview

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.

<div id="container"></div>
<button id="next">Next</button>

<script type="module">
  import { createPlayground } from "https://unpkg.com/livecodes@0.4.0";

  const steps = [
    {
      config: {
        markup: {
          language: "html",
          content: '<h1>Hello,\n  <span id="title">World</span>!\n</h1>',
        },
      },
    },
    // add a new step
    {
      config: {
        markup: {
          language: "html",
          content: '<h1>Hello,\n  <span id="title">World</span>!\n</h1>',
        },
        style: {
          language: 'css',
          content: '#title {\n  color: blue;\n}',
        },
      },
    },    
  ];

  const options = {
    appUrl: "https://v25.livecodes.io",
    config: {
      markup: {
        language: "html",
        content: "Welcome to the tutorial!",
      },
    },
  };

  createPlayground("#container", options).then((playground) => {
    let step = 0;
    const nextBtn = document.querySelector("#next");
    nextBtn.addEventListener("click", async () => {
      const lastStep = steps.length - 1;
      const currentStep = steps[step];
      await playground.setConfig(currentStep.config);
      step = step < lastStep ? step + 1 : 0;
    });
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Preview

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.

<div id="container"></div>
<button id="next">Next</button>

<script type="module">
  import { createPlayground } from "https://unpkg.com/livecodes@0.4.0";

  const steps = [
    {
      config: {
        markup: {
          language: "html",
          content: '<h1>Hello,\n  <span id="title">World</span>!\n</h1>',
        },
      },
    },
    {
      config: {
        // remove duplication
        style: {
          language: 'css',
          content: '#title {\n  color: blue;\n}',
        },
      },
    },    
  ];

  const options = {
    appUrl: "https://v25.livecodes.io",
    config: {
      markup: {
        language: "html",
        content: "Welcome to the tutorial!",
      },
    },
  };

  createPlayground("#container", options).then((playground) => {
    let step = 0;
    const nextBtn = document.querySelector("#next");
    nextBtn.addEventListener("click", async () => {
      const lastStep = steps.length - 1;
      const currentStep = steps[step];
      // get previous config
      const prevConfig = step === 0 ? options.config : await playground.getConfig();
      // merge previous with new config
      await playground.setConfig({ ...prevConfig, ...currentStep.config });
      step = step < lastStep ? step + 1 : 0;
    });
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Preview

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.

<div id="container"></div>
<button id="next">Next</button>

<script type="module">
  import { createPlayground } from "https://unpkg.com/livecodes@0.4.0";

  const steps = [
    {
      // active editor and cursor position
      panel: 'markup',
      position: { line: 2, column: 20 },
      config: {
        markup: {
          language: "html",
          content: '<h1>Hello,\n  <span id="title">World</span>!\n</h1>',
        },
      },
    },
    {
      panel: 'style',
      position: { line: 2, column: 10 },
      config: {
        style: {
          language: 'css',
          content: '#title {\n  color: blue;\n}',
        },
      },
    },    
  ];

  const options = {
    appUrl: "https://v25.livecodes.io",
    config: {
      markup: {
        language: "html",
        content: "Welcome to the tutorial!",
      },
    },
  };

  createPlayground("#container", options).then((playground) => {
    let step = 0;
    const nextBtn = document.querySelector("#next");
    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 });
      // set layout
      await playground.show(currentStep.panel, currentStep.position);
      step = step < lastStep ? step + 1 : 0;
    });
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Preview

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.

<div id="container"></div>
<button id="next">Start Tutorial</button>

<script type="module">
  import { createPlayground } from "https://unpkg.com/livecodes@0.4.0";

  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}',
        },
      },
    },
    // add new steps
    {
      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",
    config: {
      markup: {
        language: "html",
        content: "Welcome to the tutorial!",
      },
    },
  };

  createPlayground("#container", options).then((playground) => {
    let step = 0;
    const nextBtn = document.querySelector("#next");
    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);
      // change the button text for new steps
      nextBtn.textContent = currentStep.btnText;
      step = step < lastStep ? step + 1 : 0;
    });
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Preview

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)");
});
Enter fullscreen mode Exit fullscreen mode

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

Check the final result after adding tests, styles and some final touches. See it in action on LiveCodes blog.

Final result for tutorial demo


A screenshot for the final result of the tutorial demo

Full Code

The full code in vanilla JavaScript and also in React TSX can be found here.

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?

Top comments (0)