Jump to main content

Using JavaScript & contenteditable

  • Published:
  • Category: JS

I’m moving beyond the kiddie pool…

Over the past few months I’ve dived further into learning JavaScript than I ever had before. And by JavaScript I don’t mean ‘jQuery’. I actually mean vanilla, no plug-ins or shortcuts JavaScript.

It’s been fun. A little confusing at times. But, I’m actually surprised at how much JavaScript I knew from meddling around with other people’s code, and from my firm grasp on Sass’s Control Directives. The Control Directives in particular really made some of the basic functions of JavaScript click for me.

I see what you did there… but what about this?

In a lot of the tutorial websites and books that I’ve been reading, there isn’t a whole lot of time spent on actual node manipulation (at least, I haven’t gotten to them yet?). As someone who learns best by seeing the results of what he codes, a lot of the JS that I was learning were more behind the scenes, math functions and variable checks. These are obviously important concepts to master. But returning all values via console.log was doing nothing to help me bridge the gap between what I was learning, and how I would want to implement it.

So, instead of giving up from discouragement of not seeing what I needed to really understand the application of what I wanted to do, I left the tutorials and moved onto Stack Overflow to search for the missing pieces.

Making a Content Editable Element

Making a content editable element in HTML isn’t all that difficult. You just need to add the contenteditable="true" attribute to the appropriate element (a <p> for example).

For a personal project that I am working on (which I will write more about as I work through it), I knew I needed this functionality. However, I also needed to be able to save and potentially undo edited content.

Here’s where I wanted to apply the JavaScript I had been learning. So, I started with what I knew inside and out, the HTML and CSS…

Here’s the basic HTML I was working with:

<div id="container">
  <p contenteditable="true" id="myContent">
    This is the original content.
  </p>
  <button class="btn" id="undo">Undo</button>
  <button class="btn" id="save">Save Content</button>
</div>


And the hastily written CSS to give it some semblance of styling:

html, body {
  font-family: helvetia, arial;
  font-size: 110%;
  margin: 0;
  padding: 0;
}

.container {
  margin: auto;
  padding: 20px;
  position: relative;
  width: 50%;
}

[contenteditable] {
  font-size: 26px;
  padding: 4px;
}

[contenteditable]:focus {
  background: #ccc;
}

.btn {
  background: #fff;
  border: 1px solid #aaa;
  font-size: 18px;
  padding: 6px;
}

#undo, #redo {
  position: absolute;
  right: 0;
  visibility: collapse;
}

#undo.show, #redo {
  visibility: visible;
}


Pretty straight forward. But now I have to make those buttons actually do something.

Here’s where the JavaScript comes in…

(function(){
  var originalContent = getById('myContent').innerHTML;
  var updatedContent = "";
  var btn1 = document.createElement('button');
  var txt1 = document.createTextNode('Redo');

  function getById (id_string) {
    return document.getElementById(id_string);
  }

  btn1.appendChild(txt1);

  getById('save').addEventListener('click', function() {
    updatedContent = getById('myContent').innerHTML;

    if (updatedContent != originalContent) {
      undo.classList.add('show');
    }
  });

  btn1.id='redo';
  btn1.className='btn';

  getById('undo').addEventListener('click', function() {
    getById('myContent').innerHTML=originalContent;
    undo.classList.remove('show');

    getById('container').appendChild(btn1);
  });

  btn1.addEventListener('click', function() {
    getById('myContent').innerHTML=updatedContent;
    this.remove();
    undo.classList.add('show');
  });

})();


What did you just show me?

OK, so that looks like a lot of stuff going on there. Let’s look at each piece individually so we can see what’s going on.

The first thing of note is that I’ve wrapped my code in a Immediately Invoked Function Expression (IIFE).

<script>

    (function(){

      ...

    })();

</script>

Wait, a what?

That’s what I said at first.

In JavaScript, all variables are scoped at the function level. Without scoping variables to a particular function (wrapping them within an IIFE) the variables instead get scoped to the global level.

This is not necessarily a problem if your goal is to create global variables. However, depending on the scale of your project, you may end up with many different functions that could potentially have variables with the same name.

Reusing variable names is fine. It happens a lot.

The problem though is that without scoping variables to a particular function, they will be overwritten if you use that same variable name within a different function.

For Example:

<button id="bananafication">Click me</button>

<script>
  // Here's a global variable of 'b'.
  // Globally, I do not want b to be a banana
  var b = "this is not a banana";

  console.log(b);
  // if you look at the console, you'll see that b is set to
  // "this is not a banana"

  // but in this instance, I am setting var 'b' to be a banana
  // my motivation for this does not need to be explained here
  // perhaps it'd be best to read the product spec?
  document.getElementById('bananafication').addEventListener('click', function() {
    var b = "banana!"
    console.log(b);
  });

  // Now the global variable b has been overwritten to be 'banana!'
  // This may not be what we wanted to have happen?
</script>

Enough about that…

Next up, I declare the variables that will be used within the script.

  var originalContent = getById('myContent').innerHTML,
      undo = getById('undo'),
      updatedContent = "", // creates a placeholder var for updated content
      btn1 = document.createElement('button'),
      txt1 = document.createTextNode('Redo');

  btn1.appendChild(txt1);

And here is what each variable is for:

  • originalContent stores the original content of the editable region
  • updatedContent is a placeholder var that will contain updated content on save
  • btn1 and txt1 are used in creating the redo button after undo is pressed

Now the last line in the code sample above isn’t actually a variable, but it takes the last two variables, btn1 and txt1 and appends them together. This doesn’t actually add any code to the page yet. That comes later.

To cut down on the number of times one would have to write getElementById, I wrote out this function:

  function getById (id_string) {
    return document.getElementById(id_string);
  }

  // Doing this allows me to say:
  // getById('ID')...;
  // rather than
  // document.getElementById(ID)...

Now we look at the heavy lifters of the script

getById('save').addEventListener('click', function() {

  // updates the myContent block to 'save'
  // the new content to updatedContent var
  updatedContent = getById('myContent').innerHTML;

  if (updatedContent != originalContent) {
    // Show the undo button in the case that you
    // didn't like what you wrote and you want to
    // go back to square one
    undo.classList.add('show');
  }

});

What’s going on above is we are attaching an event listener to the button with the id of save. When that button is pressed (clicked) it fires off the function to add the contents of myContent to the placeholder variable updatedContent.

The second part of the code, contained by the if statement, looks to see if the updated content is actually different than the original. If not, there’s nothing to undo so the undo button is kept hidden. But if the content has changed, then it looks for the element with the ID of undo and adds a class of show to it.

Take note that classList isn’t supported prior to Internet Explorer 10.

Finally, in the event the undo button was pressed but then the changed content was needed again, the following adds a redo button to the page, and hides the undo button again.

// If you click the undo button, revert
// the innerHTML of the contenteditable area
// to the original content that was there.

// Then add in a 'redo' button, to bring back the edited content

btn1.id='redo'; // adds the id of #redo
btn1.className='btn'; // adds a class of .btn

getById('undo').addEventListener('click', function() {
  getById('myContent').innerHTML=originalContent;
  undo.classList.remove('show');

  getById('container').appendChild(btn1);

});

btn1.addEventListener('click', function() {
  getById('myContent').innerHTML=updatedContent;
  this.remove();
  undo.classList.add('show');
});

First we listen for if the undo button has been pressed. Then the myContent is reverted to the data stored in the originalContent variable and the class of show is removed from the undo button, hiding it again.

The redo button, created from the combination of the btn1 and txt1 vars is then added to the page by looking for the element with id="container". The CSS then places the button where it should be.

Finally, an on click listener is added to the redo button, and after clicking it the myContent area is reverted to the data saved in the updatedContent var. The redo button (this) is then removed from the page and the undo button has the class of show added back to it.

To be honest, there’s really no reason to be adding and removing the redo button here. It’d be a lot easier to just add the button to the HTML markup and treat is similarly to the undo button and just hide and show it. I simply did it this way to practice adding and removing an element through JavaScript.

So that’s it!

Check out a working demo here.

Now obviously there is no real “saving” here beyond the single browser session. That would require setting up a database to save the content to and a means of talking to said database.

That’ll just have to be explained another day!


A quick shout out to @spmurrayzzz, @wwnjp and @kevincennis who helped me out while learning / writing about this topic. Thanks guys.