I'm working on a new project-based JavaScript course for beginners and aspiring developers.
In addition to a ton of projects, I wanted to include little mini-challenges with each lesson. And to do that, I want to include a code sandbox (like CodePen) right in the tutorial.
Rather than loading buckets of React like most libraries for this require, I decided to build my own. Let's look at how!
An example challenge
For example, imagine that you've just learned about the Array.prototype.filter()
method. Then, you're presented with this challenge (newsletter subscribers, click through to view this part).
Try it! Use the Array.prototype.filter()
method to create a new array that contains just wizards
with three or more spells.
Anything you type in the code sandbox above automatically runs in its own isolated container, and anything you log to the console is displayed in the UI.
See how I solved this challenge…
In this example, I've made the console very prominent since it's a JavaScript-heavy exercise, but it can also be configured to display a rendered UI with HTML and CSS.
So… how do build something like this?
Like so many things code-related, the basic implementation was shockingly simple, and the details were annoyingly complicated.
Today, I'm going to look at how to get a basic sandbox environment setup. In a future article, if you're interested, I can talk through how I got it setup with syntax highlighting and a nicer UI.
Let's dig in!
Creating the input elements
To start, you add a label
and textarea
for each type of input you're supporting. In our case, let's create ones for html
, css
, and js
.
<label for="html">HTML</label> <textarea id="html"></textarea> <label for="css">CSS</label> <textarea id="css"></textarea> <label for="js">JavaScript</label> <textarea id="js"></textarea>
When typing code in these, a lot of browsers will try to autocorrect words or capitalize things for you automatically. We don't want.
Let's disable autocorrect
, autocapitalize
, and translate
on each of our fields.
<label for="html">HTML</label> <textarea id="html" spellcheck="false" autocorrect="off" autocapitalize="off" translate="no"></textarea>
To keep our code encapsulated and protect ourselves from cross-site scripting attacks, we want to render whatever the user types into an iframe
. Let's create that now, too.
<p><strong>Result</strong></p> <iframe id="result"></iframe>
Now, we have a place for users to type code, and a place to render and run it.
Rendering code into the sandbox
First, let's get all four of our elements using the document.querySelector()
method.
// Get elements let html = document.querySelector('#html'); let css = document.querySelector('#css'); let js = document.querySelector('#js'); let result = document.querySelector('#result');
Next, we'll use event delegation to listen for input
events in our document, and run an inputHandler()
method when they happen.
// Listen for input events document.addEventListener('input', inputHandler);
In the inputHandler()
method, we'll check if the event.target
, the field that was updated, was the html
, css
, or js
field.
If it's none of them, we'll return
early to end the function. Otherwise, we'll run an updateIframe()
function that will actually render and run the code for us.
/** * Handle input events on our fields * @param {Event} event The event object */ function inputHandler (event) { // Only run on our three fields if ( event.target !== html && event.target !== css && event.target !== js ) return; // Render code into the sandbox updateIframe(); }
Normally, you can't modify the code in an iframe
. But… because our iframe
is hosted from the same domain as the site it's being loaded and modified on, we can!
The contentWindow.document
property gives us access to the actual DOM inside our iframe
element.
We'll use the iFrame.contentWindow.document.open()
method to open it up, and the Element.writeln()
method to inject HTML into the iframe
.
In our case, we want to render the value
's of the html
, css
, and js
fields. We'll wrap the css
and js
in style
and script
elements, respectively. Then, we'll use the iFrame.contentWindow.document.close()
method to close the iframe
back up.
/** * Update the iframe */ function updateIframe () { result.contentWindow.document.open(); result.contentWindow.document.writeln( `${html.value} <style>${css.value}</style> <script type="module">${js.value}<\/script>` ); result.contentWindow.document.close(); }
Now, whenever the user types, there code is rendered into the result
element in real time!
A quick performance fix
As you may have already guessed, constantly updating and repainting the iframe
in real time can be a performance pit.
Let's add a 500
millisecond delay (half a second) from when the user stops typing to when we actually render, using a technique called debouncing.
First, we'll add a debounce
variable that will track the current render set to happen.
// Store debounce timer let debounce;
Inside the inputHandler()
, we'll pass our updateIframe()
function into the setTimeout()
method, with a 500
millisecond delay. We'll assign the returned timeout ID to the debounce
variable.
Before setting up our function to run, we'll pass the debounce
variable (and any existing scheduled render) into the clearTimeout()
method, which will cancel it.
Now, the UI won't render until half a second after the user stops typing.
/** * Handle input events on our fields * @param {Event} event The event object */ function inputHandler (event) { // Only run on our three fields if ( event.target !== html && event.target !== css && event.target !== js ) return; // Debounce the rendering for performance reasons clearTimeout(debounce); // Set update to happen when typing stops debounce = setTimeout(updateIframe, 500); }
Another quirk: JavaScript bugs
One other little gotcha is around JavaScript.
Let's say you typed this into the #js
field…
let wizards = ['Merlin', 'Ursula', 'Gandalf'];
Then, a few seconds later, you added some more code…
let wizards = ['Merlin', 'Ursula', 'Gandalf']; console.log(wizards);
Because we're rendering the entire contents of the textarea
into the iframe
, the browser rendering engine in the iframe
thinks you're trying to redeclare the wizards
variable, and will throw an error.
To get around that, we'll clone our iframe
, replace it with the new one, and render into that, giving us a "fresh start" each time.
/** * Update the iframe */ function updateIframe () { // Create new iframe let clone = result.cloneNode(); result.replaceWith(clone); result = clone; // Render result.contentWindow.document.open(); result.contentWindow.document.writeln( `${html.value} <style>${css.value}</style> <script type="module">${js.value}<\/script>` ); result.contentWindow.document.close(); }
This is the one part of this whole thing that feels a little janky to me, but it's far easier and more resilient than trying to diff and modify JS outputs before rendering them.
Play with this code!
Want to try it yourself? Download the source code here and start hacking away at it.
If you have any questions or want me to dig into how I added syntax highlighting, let me know!
I have a favor to ask. If you enjoyed this email, could you forward it to someone else who you think might also like it, or share a link to my newsletter on your favorite social media site?
Cheers,
Chris
Want to share this with others or read it later? View it in a browser.
0 Komentar untuk "[Go Make Things] How to create your own CodePen clone with vanilla JavaScript"