Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1449 lines (1043 sloc) 45.9 KB

jQuery - DOM Manipulation

Learn how to use the jQuery library for manipulating the DOM of a WebPage and, thus, creating interactivity.

You will need

  • Google Chrome (recommended, any browser with developer tools will do)
  • Sublime Text (recommended, any code editor will do... except Notepad)
  • Live-Server (should already be installed)

Recommended reading

What is jQuery

jQuery is a JavaScript (hereafter JS) library created in 2006 by John Resig, and originally designed to ease the creation of client-side JS script, especially regarding DOM manipulation.

This slide-deck is based on the 3.1.1 version of jQuery.

As such, some examples could be out-of-date.

Example file

The rest of this slide-deck will rely on this index.html file as context.

Be sure to download it and place it in a new project directory (e.g. jquery-course), if you want to try and follow with the examples.

Note that this example file includes Bootstrap through a CDN. Feel free to change that to a local link if you'd prefer (see here).

Learn by example

For the rest of this course, we are going to discover how to do things with jQuery by implementing features on the example template.

These features are:

  • Select another discussion in the left list, and reset the unread indicator

  • Change the alignment of the "New message" area, using the three buttons

  • Add a new message to the discussion when clicking on the "Send" button

  • Remove messages from the discussion when clicking on the trash button

Include jQuery

To add jQuery in your project, you can include it via a CDN link, in your index.html file:

<body>
  ...
  <script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
</body>

You can also download the complete file, and save the file in a js directory in your project directory. Then, include the file in your index.html:

<body>
  ...
  <script type="text/javascript" src="js/jquery-3.1.1.min.js"></script>
</body>

Wait... am I not supposed to put my <script> tags in the <head>?

What are they doing right before the closing </body> tag?

Script inclusions

It's considered a good practice to include JS scripts at the end of your HTML page.

When your browser loads the JS, it doesn't just load the file. It also parses it.

Parsing the JS files pauses the load of all other resources, effectively blocking everythig until the JS has been completely parsed.

This can result in slow loading pages, especially when you have multiple or big scripts.

Plus, loading JS files while the DOM is not forces you to start all your JS scripts with the window.onlad = function {} syntax.

With the <script> tag at the end of the HTML file, you can be sure that your JS is loaded after the DOM is.

Add custom script

We will write our own JS code in a custom script file.

In the js directory inside your project directory, create a new file script.js and include it at the bottom of your index.html page:

<body>
  ...
  <script type="text/javascript" src="js/script.js"></script>
</body>

Be extra-sure to include your custom script.js file after the inclusion of the jQuery file.

Test everything

Add the following line in your script.js file, and save it:

console.log($("body").jquery);

Start your project with live-server and access your browser console. You should see the following lines:

3.1.1
Live reload enabled.

If it's the case, you're good to go. jQuery and your custom script are both correctly include in your project.

jQuery documentation

Everything that is presented in this slide-deck can also be found in the jQuery documentation, along with lot of examples and information.

We highly recommend that you check it out.

jQuery Documentation

The $ object

The complete jQuery library is accessible in your JS code through the use of the global $ variable.

Some other libraries also offers a $ variable as there main entry-point. That could be the cause for conflict between jQuery and those librairies.

If it's the case, you can use the global jQuery variable instead.

To be sure to remove all possible conflicts, you can use the special .noConflict() jQuery method at the top of your JS file:

$.noConflict();
// Code that uses other library's $ can follow here.
// But use the jQuery variable to access jQuery's method.

This shouldn't be the case in this course.

Selecting things

Being a library designed to easily handle DOM manipulation, jQuery allows you to... easily select DOM elements.

The selection functionnality is quite broad and powerful, and is based on the same syntax as CSS selectors.

To select DOM elements and receive jQuery objects matching the selected elements, use the $() function, passing it a selector as parameter:

Selector CSS example jQuery Result
Element p $("p") All <p> elements in the page
Id #username $("#username") Unique element with the username id
Class .row $(".row") All elements with a row class

Using a jQuery selector will always return you an array of jQuery objects, even with an Id selector (that should return one element).

Good selecting practice

Selecting elements in jQuery being very vast, you can easily write selectors that aren't optimals, performace-wise.

Traversing the DOM is a costly operation. Your jQuery selectors (and your CSS ones as well) should try to be as to-the-point as possible.

There's many good practice regarding jQuery selectors. We are only going to see a few of them.


Note that the following example contains some jQuery method.

Don't mind them now, we will explicit these methods later on the course.

Storing jQuery object

When you know or discover that your are going to use the same jQuery selector several time, you should cache its result in a JS variable, for future reference.

Bad!

$("article > p:first-child").addClass("catch-phrase");
$("article > p:first-child").text("This is a very pertinent article");
$("article > p:first-child").append("<span>CLICK ME!</span>");

Way better!

var `$firstSentences` = $("article > p:first-child");
`$firstSentences`.addClass("catch-phrase");
`$firstSentences`.text("This is a very pertinent article");
`$firstSentences`.append("<span>CLICK ME!</span>");

For better code-reading, it's a good practice to precede caching variable's name with the $ character, to indicate that it contains jQuery object(s).

Use id selectors

HTML id attribute allows you to define unique identifier in an HTML page.

Thanks to this uniqueness, element with id attributes are extremly fast to retrieve in the DOM, even with older browsers.

As much as possible, you should add id attributes to HTML elements that you'll select.

<div class="panel-body">
  <p><!-- content --></p>
  <p id="the-one"><!-- content --></p>
  <p><!-- content --></p>
</div>

This...

$("div.panel-body p:nth-of-type(2)")

...will be way slower than this.

$("#the-one")

Don't over do it

If you can't define id attributes in your HTML template, you'll need to use more complete selectors.

When doing so, you might be tempted to be as exhaustive as possible, in order to be sure you'll get the right element(s).

Like this for example:

$("html body main.container div.list-group a.list-group-item h4")

This is completely unnecessary and a waste of resources.

You need to be precise with your selector to avoir selecting things you don't want. But being over-precise is as bad as being too vague.

Knowing your HTML structure is mandatory when writing jQuery selectors.

We could simplify the precedent example like this:

$("a.list-group-item h4")

Using class selector

CSS classes being shared among elements, it's a great way to quickly select all related elements.

// Will select all items from list-group lists.
$(".list-group-item")

But there's a drawback.

With class selector, jQuery will parse all the DOM and test every single node to see if it has the given class (or classes). This is possibly very inefficient.

Thus, you should try to be as precise as possible when using class selector, by qualifying it with a tag name, for example.

In our example, we know that the .list-group-item should only be applied to <li> or <a> element. So:

// Will select only <li> and <a> that have the class
$("li.list-group-item, a.list-group-item")

Selecting order

When jQuery encounters a selector like $("a.list-group-item h4"), how do you think it's going to execute it ?

You might think that jQuery will fetch all the a.list-group-item first, then fetch all h4 inside them.

This is not the case. In fact, it's the complete opposite.

To begin with, jQuery will fetch all <h4> in the page, and store them in an array.

Then, it will examine each of these <h4>, and reject the ones that don't have an a.list-group-item as parent.

If you have many <h4> in your page or a far-too-precise selector, jQuery will take unnecessary time to process the selector.

To solve this problem, you have to options:

  • Use the .find() method on the parent object.
  • Narrow the context by using the second parameter of the $() function.
Try to .find() me

The .find() method allows you to search for elements in the DOM, but limited to the context of the jQuery object on which you called the method.

If we use the previous example again (the $("a.list-group-item h4") case), we would use the method like this:

$("a.list-group-item").find("h4")

We first fetch the a.list-group-item, then we search for h4 elements inside the retrieved a.list-group-item elements.

Refine the use of $()

We said before that $() is the function to use to retrieve DOM elements.

Its first parameter is the selector for those elements.

But the function also have a second parameter, that is the context in which the search is conducted.

By default, the context is the complete HTML page, but you can pass it any object representing a subset of the DOM.

For the previous example, we could write our selector like this:

// First, we retrieve all the list-group items
var $listElements = $("a.list-group-item");

// Then we search for <h4> element, but only in the subset of the Dom
// contained in the previously retrieved elements 
$("h4", $listElements)

In a more compressed style, it would resemble this:

$("h4", $("a.list-group-item"))

Feature : "Select discussion item"

Template update N°1

For better UI behavior, we need to tell our browser to show the click pointer icon when passing over our discussion list items.

Add this line in the <style> tag, after the main style:

.list-group-item { cursor: pointer; }

Events

Most of the functionnalities we'll implement will rely on things being clicked.

jQuery can handle more events than just click, see here.

Use the .click() method to add a click event on an element.

The .click() method needs a callback function as its argument, that will be called with one argument: the fired event

In order to select another discussion, we need to add a click event to the list elements.

Add this code in your script.js file:

$("a.list-group-item").click(function(event) {
  console.log(event);
});

Now, go to your page, open your console and try to click on one of the list item.

You should see the object representing the event.

this? $(this)?

When writing event callback functions you might want to retrieve the DOM element that triggered the event.

You can do that with the property currentTarget of the event object:

$("a.list-group-item").click(function(event) {
  console.log(`event.currentTarget`);
});

More simply, use the this keyword to achieve the same purpose:

$("a.list-group-item").click(function(event) {
  console.log(`this`);
});

Since we are using jQuery, we'd prefer retrieving a jQuery object representing this DOM element.

Do that by passing this to the $() function:

$("a.list-group-item").click(function(event) {
  console.log(`$(this)`);
});

Add a CSS class

On the page, there is one list item that has a different style.

This is the currently selected item, and the effect is achieved using the .active class from the Bootstrap framework.

When another list item will be clicked we want to switch the active state from the previous list item to the one being activated.

This means adding the .active CSS class to the currently clicked element.

Use the .addClass() method to do that, and pass it a string with the name of the classes to be added, separated by a space.

$("a.list-group-item").click(function(event) {
  // Add the 'active' class to the clicked list item
* $(this).addClass("active");
});

Go on your page, and click on your list items.

The class is correctly added. Next step is to remove this class from the previsouly active element.

Remove a CSS class

To remove a CSS class from an element, simply use the .removeClass() method.

Pass it a string argument with the names of the classes to be removed, separated by a space.

Now, we want to remove the active state from the previously selected list element.

This element is a a.list-group-item with the .active class.

$("a.list-group-item").click(function(event) {
  // Remove the 'active' class from the previously selected list item
* $("a.list-group-item.active").removeClass("active");
  // Add the 'active' class to the clicked list item
  $(this).addClass("active");
});

Note how we append the .active class to our selector.

Trying to remove a CSS class from an element that didn't have it in the first time will not raise any error, fortunately.

Change the content

Now that we can change the selected discussion list item, we want to remove the notification about unread messages, that is the badge in the item.

For that we will change the content of the span.badge inside the list item.

Use the .text() method to do so, and pass it as argument a string that represents the new content.

Calling the method with no argument returns the current content.

In our case, since we want to remove the content of the element, we'll use an empty string (""):

$("a.list-group-item").click(function(event) {
  /* Add this after previous code */

  // Remove the unread notification
* $("span.badge", this).text("");
});

Note that we used the this keyword as context when searching for the span.badge element, since this represents the list item.

Complete code

Here's the complete code for this feature:

For better readability and structure, we've splitted the event declaration from the function implementation.

$("a.list-group-item").click(`switchListItem`);

*function switchListItem() {

  // Change the active state to the clicked item
  $("a.list-group-item.active").removeClass("active");
  $(this).addClass("active");
  
  // Clear the unread notification for the clicked item
  $("span.badge", this).text("");
}

Note that we passed the switchListItem function as parameter to the .click() method without parenthesis.

We dont' want to execute the function, we just want to pass its reference.

Feature : "Change message alignment"

Template update N°2

For the next feature, we will need to add some id in our index.html page.

At the line 125, add this id to the <textarea> element:

&lt;textarea id="`message`" [...] &gt;&lt;/textarea&gt;

At the line 127, add this id to the <div> element:

<div class="btn-group btn-group-sm" id="`align-btns`">

Attach the events

Our three alignment buttons are all children of the same element, which is the div#align-btns.

We can thus use this element in our selector to access the buttons:

$("#align-btns button")...

We have seen that this kind of notation can be optimized.

Let's use the second paramter of $() to retrieve the buttons, and then attach them the event:

$("button", $("#align-btns")).click(function(event) {
  console.log(this);
});

"What is this strange behavior?" you might say, "The page is reload each time a button is clicked. Why?"

Buttons in a form

If you look closely to the HTML structure in which the buttons are placed, you'll see that they are placed inside a form:

*<form>
  <!-- textarea element -->
  <div class="btn-group btn-group-sm" id="align-btns">
    <button class="btn btn-default active">
      <span class="glyphicon glyphicon-align-left"></span>
    </button>
    <button class="btn btn-default">
      <span class="glyphicon glyphicon-align-center"></span>
    </button>
    <button class="btn btn-default">
      <span class="glyphicon glyphicon-align-right"></span>
    </button>
  </div>
  <!-- send button -->
*</form>

Clicking on a <button> that's placed inside a <form> will trigger the submission of said <form>.

That's a default behavior that we don't want.

Prevent default behavior

Remember that the callback function called when an event is triggered has one parameter, that is the triggered event object ?

This event object will trigger the default behavior attached to it after our code is executed, unless we say it otherwise.

The .preventDefault() method of the event object is rightly there for this purpose.

Let's call this method at the last line of our callback function:

$("button", $("#align-btns")).click(function(event) {
  console.log(this);
  // Prevent the default submit behavior
* event.preventDefault();
});

Clicking on the button will now no longer reload the page.

Change button state

The align left button is constantly in an active state. This effect is achieved by using the .active class from the Bootstrap framework.

In the same fashion as the state of the discussion list item, we will want to switch the state when a button is pressed.

This is the code that achieve that:

$("button", $("#align-btns")).click(function(event) {
  // Change the active state when a button is clicked
* $("button.active", $("#align-btns")).removeClass("active");
* $(this).addClass("active");
  
  // Prevent the default submit behavior
  event.preventDefault();
});

Go on your page, and click on the alignment button, then click somewhere else, and observe how your button react.

You should see that its style changes. On Chrome, when clicked, a blue border is added, that disappear when you click elsewhere.

Managing the focus

This is related to the focus, that is an indication of the element in the page that you're currently "using".

Focusing in or out of an element is an event to which you can react with JS and/or jQuery.

But you can also force-activate those event on elements within your code.

In our case, we want to focus out of the button once it has been clicked.

This event of focusing our an element is called blur, and can be activated by using the .blur() jQuery method:

$("button", $("#align-btns")).click(function(event) {
  // Change the active state when a button is clicked
  $("button.active", $("#align-btns")).removeClass("active");
  $(this).addClass("active");
* $(this).blur();
  // Prevent the default submit behavior
  event.preventDefault();
});

Chaining method calls

In the previous code example, you might have seen that we called, one after the other, two different methods on the same object:

  $(this).addClass("active");
  $(this).blur();

jQuery allows you to call several methods on the same line.

This is called method chaining.

In our case, we can do that...

  $(this).addClass("active").blur();

...because the addClass() returns the object that called it, here $(this), on which we can call .blur() right away.

You need to be aware of what is returned by each method if you want to use method chaining.

Be cautious

Remember what the .find() method does?

$("#align-buttons").find("button");

The .find() method here will return all the button elements that it's found inside the #align-buttons element.

Thus, methods chained after a call to .find() will not be applied to the $("#align-buttons") object:

$("#align-buttons").find("button")`.addClass("active")`;

The active class will be applied to each objects returned from .find().

If you'd want to apply the .addClass() method to the $("#align-buttons") object first, you'd need to write it like this:

$("#align-buttons")`.addClass("active")`.find("button");

Planning the logic

Now that our buttons behave as expected, let's make them concretly change the text alignment in the "New message" area.

Bootstrap has three utility classes that helps you manage text-alignment:

  • .text-left
  • .text-center
  • .text-right

So, all we need to do, in our callback function, is:

  1. Detect which button has been clicked
  2. Remove any precedent alignment class from the #message element
  3. Add the correct alignment class to the #message element

Detect the button

To detect which button has been click we could add an id to each button.

But, in the HTML, we can see that there is already something that could help us in that task: the icon.

<div class="btn-group btn-group-sm" id="align-btns">
  <button class="btn btn-default active">
*   <span class="glyphicon glyphicon-align-left"></span>
  </button>
  <button class="btn btn-default">
*   <span class="glyphicon glyphicon-align-center"></span>
  </button>
  <button class="btn btn-default">
*   <span class="glyphicon glyphicon-align-right"></span>
  </button>
</div>

We need to retrieve the <span> element inside the button, then check what class this <span> has.

Do you have any class?

The .hasClass() method can detect if the element possesses the class name given as parameter.

This method returns a boolean (true/false).

// Will return true
$("li.active").hasClass("active");

Let's retrieve the <span> inside the this button and check, for example, if it has the .glyphicon-align-right class:

$("button", $("#align-btns")).click(function(event) {
  /* Previous code */
  $(this).addClass("active").blur();

* if ($("span", this).hasClass("glyphicon-align-right")) {
    // The clicked button is the one for align to the right.
  }

  /* Following code */
});

if ... else if ... else

Using a if ... else if ... else structure, we can complete our test:

$("button", $("#align-btns")).click(function(event) {
  // Change the active state when a button is clicked
  $("button", $("#align-btns")).removeClass("active");
  $(this).addClass("active").blur();

  if (`$("span", this).hasClass("glyphicon-align-right")`) {
    // The clicked button is the one to align to the right
    console.log("Align to the right!");

  } else if (`$("span", this).hasClass("glyphicon-align-center")`) {
    // The clicked button is the one to align to the center
    console.log("Align to the center!");

  } else {
    // The clicked button is neither the one to align to the right
    // nor the one to align to the center...
    // It must be the one to align to the left, then!
    console.log("Align to the left!");
  }

  // Prevent the default submit behavior
  event.preventDefault();
});

Go on and try that.

Finally... change the alignment!

When the align-right button has been clicked:

if ($("span", this).hasClass("glyphicon-align-right")) {
*   $("#message").removeClass("text-left text-center").addClass("text-right");
}

When the align-center button has been clicked:

else if ($("span", this).hasClass("glyphicon-align-center")) {
*   $("#message").removeClass("text-right text-left").addClass("text-center");
}

When the align-center button has been clicked:

else {
*   $("#message").removeClass("text-right text-center");
}

We don't have to add the .text-left class because, by default, the text is aligned to the left in HTML elements.

Complete code

Here's the complete code for this feature:

For better readability and structure, we've splitted the event declaration from the function implementation.

$("button", $("#align-btns")).click(changeAlignment);

function changeAlignment(event) {
  // Change the active state when a button is clicked
  $("button", $("#align-btns")).removeClass("active");
  $(this).addClass("active").blur();

  // React to the adequate clicked button
  if ($("span", this).hasClass("glyphicon-align-right")) {
    $("#message").removeClass("text-left text-center").addClass("text-right");
  } else if ($("span", this).hasClass("glyphicon-align-center")) {
    $("#message").removeClass("text-right text-left").addClass("text-center");
  } else {
    $("#message").removeClass("text-right text-center");
  }
  
  // Prevent the default submit behavior
  event.preventDefault();
}

Feature : "Add new message"

Template update N°3

For the next feature, we will need to add some id in our index.html page.

At the line 83, add this id to the <div> element:

<div class="panel-body" id="`dialog`">

At the line 138, add this id to the <button> element:

<button class="btn btn-success pull-right btn-sm" id="`send-btn`">

The basics

The "Send" button is also situated inside the form. So we need to cancel its default behavior when it's clicked:

$("#send-btn").click(function(event) {
  // Do the thing!

  event.preventDefault();
});

Now... we'll have to..:

  1. Get the value inside #message
  2. No value ?
  3. Notify the user
  4. Reject the creation
  5. Value ?
  6. Get the alignment for the new message
  7. Create the new message DOM structure
  8. Append the new message to the discussion
  9. Reset the textearea (error and content)

Get the value inside #message

To get the value of a form <input> or <textarea>, you can use the val() method in one of those elements:

// Will return "" if the textearea is empty
$("#message").val();

But since we will refer to the #message element several time in our code, we might as well cache its reference in a variable:

$("#send-btn").click(function(event) {
* var $message = $("#message");

  event.preventDefault();
});

Now, we can get the value:

$("#send-btn").click(function(event) {
  var $message = $("#message");
* var msgValue = $message.val();
  event.preventDefault();
});

Validation phase

The first thing we need to do, before actually going on with the message insertion, is some validation.

In this case, we want to reject the creation of the new message if there is no new message to create, i.e. when the "New message" text-area is empty.

Let's prepare the logic structure:

$("#send-btn").click(function(event) {
  /* Get the value */
* if (msgValue === "") {
    // No message -> Error handling  
* } else {
    // There's a message, let's go and create it.
  }
  event.preventDefault();
});

Error! Error! Err...

Now, if our new message is empty, we want to notify the user and stop the new message insertion.

There's many ways to notify the user when an error occurs in a form.

As a matter of fact, Bootstrap provides you with some classes for this, that you can apply to .form-group elements:

  • .has-error
  • .has-warning
  • .has-success

The one we'll use is .has-error:

$("#send-btn").click(function(event) {
  /* Preceding code */
  if (msgValue === "") {
*   $("#message").addClass("has-error");
  }
  /* Following code */
});

This does nothing... because #message is not a .form-group element.

I'll notify your parent!

In our HTML structure, the #message textarea is the direct children of a .form-group element.

Let's use the .parent() jQuery method to access this direct parent:

// Will return a jQuery object representing the .form-group element
$("#message").parent();

Now that we can access the right element, let's add the class:

$("#send-btn").click(function(event) {
  /* Preceding code */
  if (msgValue === "") {
    $("#message")`.parent()`.addClass("has-error");
  }
  /* Following code */
});

Go on your page, and try to click on the "Send" button while having an empty textarea.

What's your alignment?

When the message value contains something, we need to retrieve the correct alignment class to apply to the future new message in the discussion.

Let's create a new **function **that will take one argument, a jQuery object, and returns the name of the correct class.

We will use the returned value when creating the new message DOM structure.

// Add this anywhere in your script file.
function getAlignmentClass($ele) {
  if ($ele.hasClass("text-right")) {
    return "text-right";
  } else if ($ele.hasClass("text-center")) {
    return "text-center";
  } else {
    return null;
  }
}

We don't need to return the .text-left class, since it's the by-default alignment if no class is provided.

Use the function

Now, we can use our freshly created function to actually retrieve the desired alignment.

$("#send-btn").click(function(event) {
  var $message = $("#message");
  var msgValue = $message.val();

  if (msgValue === "") {
    $("#message")`.parent()`.addClass("has-error");
  } else {
*   var alignment = getAlignmentClass($message);

    // Next operations...
  }
  event.preventDefault();
});

Create the HTML structure

We now have to create all the HTML structure necessary to add a new message to the dialog panel.

Since we are using Bootstrap, this is the complete structure we'll have to create:

<div class="col-md-8 col-md-offset-4">
  <div class="alert alert-warning">
*   <span class="msg-content">
      <!-- The text goes here -->
*   </span>
    <div class="pull-right">
      <small class="text-info"><!-- The time goes here --></small>
      <button class="btn btn-link btn-xs">
        <span class="glyphicon glyphicon-trash"></span>
      </button>
    </div>
  </div>
</div>

Note we added a span.msg-content to easily pinpoint where the new message content should be placed.

The $() function, again

With jQuery, you can create HTML elements using the $() function.

If you pass a string as argument and this string looks like HTML, .

// Will create a new <p>
$("<p>")

You can even pass more complex HTML string to the function:

// Will create a new <p> and give it some content
$("<p>This is a new paragraph with content</p>")

So... technically, we could create the new message by doing this:

$('<div class="col-md-8 col-md-offset-4"><div class="alert alert-warning"><!-- The text goes here --><div class="pull-right"><small class="text-info"><!-- The time goes here --></small><button class="btn btn-link btn-xs"><span class="glyphicon glyphicon-trash"></span></button></div></div></div>')

This is obviously not a good solution:

  • Extremly long string
  • Unreadable
  • Strong coupling between template and logic

Hackers gonna hack

Ideally, we would like to write our template in our .html file, then retrieve and manipulate it with jQuery.

We can achieve that by using a <script> tag with an unrecognized type propety, text/template for example.

Browser and screen-readers only interpret the content of a <script> tag when they recognize its type.

<!-- Templates -->
<script type="`text/template`">
</script>

Such <script> tags should be placed at the end of your <body> tag, but before your .js inclusions (around the line 156, in our example).

We then give it an id for future reference, and copy our HTML inside:

<!-- Templates -->
<script type="text/template" `id="new-sent-message"`>
  <!-- HTML structure goes here -->
</script>

Recovering the template

To retrieve the templates placed inside <script>, we need to do two operations.

Firstly, get the content of the <script> tag:

var template = $("#new-sent-message").text()

This will return the HTML structure as a string (because unrecognized <script> content is interpreted as a simple string).

Secondly, generate a new DOM based on this template:

var $msgTemplate = $(template);

$msgTemplate now contains a jQuery object representing the desired HTML template.

We are now able to insert the data inside the template, and add it to our page.

Code checkpoint

With the addition of these lines, our JS code for the new feature should be:

$("#send-btn").click(function(event) {
  // Getting the "New message" input
  var $message = $("#message");

  // Getting the new message text
  var msgValue = $message.val();

  if (msgValue === "") {
    // If the new message is emplty, show an error
    $message.parent().addClass("has-error");
  } else {
    // Get the correct alignment
    var alignment = getAlignmentClass($message);
  
    // Get the new message template
    var template = $("#new-sent-message").html();
    var $msgTemplate = $(template);
  }
  event.preventDefault();
});

Fill in the template

Next step is to insert the values for this new message. We have two values to add:

  • The new message content
  • The time at which the message has been sent

Adding the new message is simple; we need to retrieve the span.msg-content, and insert the content of the msgValue variable.

$("#send-btn").click(function(event) {
  /* Previous code */
  else {
    // Get the correct alignment
    var alignment = getAlignmentClass($message);

    // Get the new message template
    var template = $("#new-sent-message").html();
    var $msgTemplate = $(template);

*   $("span.msg-content", $msgTemplate).text(msgValue);

  }
  event.preventDefault();
});

Getting the date

To get the message date, let's create a function that returns just that.

We will use the JS Date feature for that.

  • The Date.now() method returns the current timestamp as an number
  • The Date() function create a new Date object based on a given timestamp
  • The .getHours() methid returns the hours of a Date object
  • The .getMinutes() method returns the minutes of a Date object

Using all that together:

function getCurrentTime() {
  var date = new Date(Date.now());
  return date.getHours() + ":" + date.getMinutes();
}

Now, we get, in the new message template, the small.text-info element that'll contain the time and insert the new value:

/* Previous code */
  $("small.text-info", $msgTemplate).text(getCurrentTime());
/* Following code */

Apply the alignment

Remember we retrieved the correct alignment to apply to the new message? Now's the time to actually apply it.

For that, we need to add the class whose name is stored in the alignment variable, if any.

If the content of the new message should be aligned to the left, alignment contains null.

$("#send-btn").click(function(event) {
  /* Previous code */
  else {
    /* Previous code */
    $("small.text-info", $msgTemplate).text(getCurrentTime());

*   if (alignment) $msgTemplate.addClass(alignment);
  }
  event.preventDefault();
});

Insert in the discussion

We have the complete DOM structure for our new message stored in a jQuery object inside the $msgTemplate variable.

Let's insert this new message in our page. To do this, we'll use the .append() method.

This method append the given jQuery object at the end of the jQuery object that called the method.

$("#send-btn").click(function(event) {
  /* Previous code */
  else {
    /* Previous code */
    if (alignment) $msgTemplate.addClass(alignment);

*   $("#dialog").find("div.row").append($msgTemplate);
  }
  event.preventDefault();
});

Finally, let's clean-up a bit, by adding this at the end of the else block:

// Remove the content from the #message element.
$message.val("");

Complete code - Functions

getAlignmentClass()

// Get the alignment class name applied to the given element.
// If it's the default alignement (left), returns null.
function getAlignmentClass($ele) {
  if ($ele.hasClass("text-right")) {
    return "text-right";
  } else if ($ele.hasClass("text-center")) {
    return "text-center";
  } else {
    return null;
  }
}

getCurrentTime()

// Returns the current time in a HH:MM formatted string
function getCurrentTime() {
  var date = new Date(Date.now());
  return date.getHours() + ":" + date.getMinutes();
}

Complete code - Event callback

$("#send-btn").click(createNewMessage);

function createNewMessage(event) {
  // Getting the "New message" value
  var $message = $("#message");
  var msgValue = $message.val();

  if (msgValue === "") {
    $message.parent().addClass("has-error");
  } else {
    // Get the correct alignment
    var alignment = getAlignmentClass($message);

    // Get the new message template
    var template = $("#new-sent-message").text();
    var $msgTemplate = $(template);

    // Insert the value in the tempalte
    $("span.msg-content", $msgTemplate).text(msgValue);
    $("small.text-info", $msgTemplate).text(getCurrentTime());
    if (alignment) $msgTemplate.addClass(alignment);
    
    // Add the template to the page
    $("#dialog").find("div.row").append($msgTemplate);
    $message.val("");
  }
  event.preventDefault();
}

Feature : "Remove messages"

The .remove() method

Removing something in jQuery is quite simple. Just use the .remove() method on a jQuery object, and the DOM it represents will be removed from the page.

Our event should be attached to all the button elements that contains a span.glyphicon-trash element:

$("span.glyphicon-trash").parent().click(function(event) {
  
});

But since the event is on the button element, we need to travel up the DOM tree to find the buttons parent that correspond to the complete message.

We could use the .parent() method to travel up step by step, which would give a code like this:

$(this).parent().parent().parent().remove();

This works... but it's certainly not a good option.

  • We need to thouroughly analyze the HTML structure to count the number of steps we need to go.
  • What if the HTML structure changes and a new structure level is added? We would have to change our code...

Get closer

In our case, the element that we want to access is the div.col-md-8. But there's multiple div.col-md-8 in our page, so we don't want any div.col-md-8; we want the closest one to our button.

We can then use the .closest() method and give it the selector of the element we want to retrieve:

$("span.glyphicon-trash").parent().click(function(event) {
  console.log($(this).closest("div.col-md-8"));
});

This will correctly return you the <div> that contains the message to remove.

Let's remove it!

$("span.glyphicon-trash").parent().click(function(event) {
  $(this).closest("div.col-md-8").remove();
});

Go on your page, and try to remove a message.

Now... add a new message and try to remove it.

Time paradox

This behavior is caused by the way the JS code is interpreted by the browser.

Actually, the JS code is interpreted and executed when it's firstly loaded by the HTML page.

This means that, when the browser loads the HTML page and found a <script> tag, it:

  1. Loads the file
  2. Parses the code
  3. Executes the code

If your code creates and attaches event handlers to element, the browser will do so... but only on elements that already exists!

  1. Stores functions in memory for future executions
  2. Continues parsing the HTML file

That's it. After that, your JS code will not be executed again.

So... when you create, at a later time, new DOM nodes, events won't be attached to them; the "attach events to elements" phase has already happenned.

Parent's responsibility

To resolve this issue, the solution is to register the event not on the element itself, but on one of its parents that is present at page-load.

In our case, this parent could be the #dialog element.

The .on() method allows you to register an event on an element that can be activated only when triggered by a descendant of this element, not the element itself.

In our case, we want to register on event on the #dialog element, but trigger it only when a button is clicked:

The arguments are : the name of the event, the selector of the descendant element, and the function to activate.

$("#dialog").on("click", "button", function(event) {
  // 'this' still represent the clicked button
  // So we can reuse the same code as before.
  $(this).closest("div.col-md-8").remove();
});

You can register other events than click.

Complete code

Here's the complete code for this feature:

// Feature : "Remove message"
$("#dialog").on("click", "button", removeMessage);

function removeMessage(event) {
  // 'this' still represent the clicked button
  // So we can reuse the same code as before.
  $(this).closest("div.col-md-8").remove();
}

Complete App

The complete code for this example application can be found here.

In this complete code, note that the event declarations have been put at the top of the file, and that the function comes after.

This allows a better file structure.

Resources

Documentation

Further reading

You can’t perform that action at this time.