Why JavaScript devours HTML: code examples

Web development is constantly evolving. Recently, one trend has become popular, which basically contradicts the generally accepted idea of ​​how to develop web applications. Some have high hopes for him, while others are disappointed. Each has its own reasons for this, which in a nutshell is difficult to explain.



Web page code traditionally consists of three sections, each of which fulfills its duties: HTML-code defines the structure and semantics, CSS-code determines the appearance, and JavaScript-code determines its behavior. In teams involving designers, HTML / CSS developers and JavaScript developers, this separation is natural: designers define visual elements and a user interface, HTML and CSS developers place these visual elements on a page in a browser, and JavaScript developers add user interaction to tie everything together and “make it work.” Everyone can work on their tasks without interfering in the code of the other two categories of developers. But all this is true for the so-called "old style."

In recent years, JavaScript developers began to determine the structure of the page in JavaScript, and not in HTML (for example, using the React js framework ), which helps to simplify the creation and maintenance of user interaction code. Without js frameworks, developing modern websites is much more difficult. Of course, when you tell someone that the HTML code written by him needs to be broken into pieces and mixed with JavaScript, which he is very familiar with, this (for obvious reasons) can be perceived with hostility. At a minimum, the interlocutor will ask why we need this at all, and how we will benefit from this.

As a JavaScript developer in a cross-functional team, this question is sometimes asked to me, and often it is difficult for me to answer it. All the materials I found on this topic are written for an audience that is already familiar with JavaScript. And this is not very good for those who specialize exclusively in HTML and CSS. But the HTML-in-JS pattern (or something else that provides the same benefits) is likely to be in demand for some time, so I think this is an important thing that everyone involved in web development should understand.

In this article I will give code examples for those who are interested, but my goal is to explain this approach so that it can be understood without them.

Likbez: HTML, CSS and JavaScript


In order to maximize the audience of this article, I want to briefly talk about what role these languages ​​play in creating web pages, and what kind of separation between them existed initially. If you all know this, you can skip this section.

HTML: for structure and semantics


HTML code (HyperText Markup Language) defines the structure and semantics of the content that will be placed on the page. For example, the HTML code of this article contains the text that you are reading now, and in accordance with the specified structure, this text is placed in a separate paragraph, after the heading and before pasting CodePen .

For example, create a simple web page with a shopping list:



We can save this code in a file, open it in a web browser, and the browser will display the result. As you can see, the HTML code in this example is a page that contains the heading “Shopping List” (Shopping items (2 items)), an input text box, a “Add Item” button and a two-item list (“Eggs” and “Oil”). The user will enter the address in his web browser, then the browser will request this HTML code from the server, download it and display it. If there are already elements in the list, the server can send HTML with ready-made elements, as in this example.

Try typing something in the input field and click the "Add item" button. You will see that nothing is happening. The button is not associated with any code that could change HTML, and HTML cannot change anything by itself. We will return to this in a minute.

CSS: to change the look


The CSS code (Cascading Style Sheets) defines the appearance of the page. For example, the CSS of this article defines the font, spacing, and color of the text you are reading.

You may have noticed that our shopping list example looks very simple. HTML doesn't allow things like spacing, font sizes, and colors to be specified. That's why we use CSS. We could add CSS code for this page to decorate it a bit:



As you can see, this CSS code changed the size and thickness of the text characters, as well as the background color. A developer can, by analogy, write rules for his own style, and they will be applied sequentially to any HTML structure: if we add section, button or ul elements to this page , the same font changes will apply.
But the Add Item button still does nothing: here we just need JavaScript.

JavaScript: to implement behavior


JavaScript code defines the behavior of interactive, or dynamic, elements on a page. For example, CodePen is written using JavaScript.

For the Add Item button in our example to work without JavaScript, we would need to use special HTML code to force it to send data back to the server ( <form action \ u003d '...'> if you are suddenly interested). Moreover, the browser will reload the updated version of the entire HTML file without saving the current state of the page. If this shopping list were part of a large web page, everything the user did would be lost. It doesn’t matter how many pixels you have moved down while reading the text - when you restart the server will return you to the beginning, it doesn’t matter how many minutes of the video you watched - when you restart, it will start again.

That's how all web applications used to work: every time a user interacted with a web page, he seemed to close his web browser and open it again. This does not matter much for a simple case study, but for a large complex page, which may take some time to load, this approach is not effective for either the browser or the server.

If we want to change something on the page without completely reloading this page, we need to use JavaScript:



Now, when we type the text in the input field and click the Add Item button, our new element is added to the list and the number of elements at the top is updated! In a real application, we would also add some code to send a new item to the server in the background so that it appears the next time the page loads.

Separating JavaScript from HTML and CSS is justified even in this simple example. More complex interactions work like this: HTML is loaded and displayed, and JavaScript is subsequently launched to add something to it and change it. However, as the complexity of the web application grows, we need to carefully monitor what, where and when our JavaScript code changes.

If we continued to develop this app with a shopping list, we might add buttons to edit or remove items from the list. Suppose we write JavaScript code for a button that deletes an element, but forget to add code that updates information about the total number of elements at the top of the page. Suddenly, we get an error: after the user deletes the item, the indicated total number on the page will not match the list!

As soon as we noticed the error, we fixed it by adding the same totalText.innerHTML line from our code for Add Item to the code for Remove Item. Now we have the same code, which is duplicated in several places. Later, let's say, we want to change this code so that instead of “(2 items)” at the top of the page it displays “Items: 2”. We must make sure that we do not forget to update this code in all three places: in HTML, in the JavaScript code for the Add Item button and in the JavaScript code for the Remove Item button. If we do not, we will have another error, because of which this text will change dramatically after interacting with the user.

Already in this simple example, we see how easy it is to get confused in code. Of course. There are approaches and practices to streamline JavaScript code to make it easier to solve this problem. However, as things become more complicated, we will have to continue restructuring and constantly rewrite the project so that it can be easily developed and maintained. While HTML and JavaScript are stored separately, it can take a lot of effort to ensure synchronization between the two. This is one of the reasons new JavaScript frameworks such as React have become popular: they are designed to provide a more formalized and efficient relationship between HTML and JavaScript. To understand how this works, we first need to go a little deeper into computer science.

Imperative vs declarative programming


The key thing to understand is the difference in thinking. Most programming languages ​​allow you to follow only one paradigm, although some of them support two at once. It is important to understand both paradigms in order to appreciate the main advantage of HTML-in-JS from the point of view of the JavaScript developer.

  • . «» , . ( ) : , , . , , . - «». Vanilla JavaScript , jQuery. JavaScript .
    • « X, Y, Z».
    • : , .selected; , .selected.

  • . , . , , «» (), , - . , . (, React), , , . , , , , . , :
    • « XYZ. , , .
    • Example: this element has a .selected class if the user selected it.


HTML is a declarative language


Forget JavaScript for a moment. Here's an important fact: HTML is a declarative language. In the HTML file, you can declare something like:

<section>
  <h1>Hello</h1>
  <p>My name is Mike.</p>
</section>

When the web browser reads this HTML code, it will independently determine the necessary steps and perform them:

  1. Create a section element .
  2. Create a level 1 header element ( h1 ).
  3. Set the title element text to “Hello”.
  4. Place the title element in the section element.
  5. Create a paragraph element ( p ).
  6. Set the text of the paragraph element to "My name is Mike".
  7. Place a paragraph element in a section element .
  8. Place the section element in an HTML document.
  9. Display the document on the screen.

For a web developer, exactly how the browser does these things does not matter: all that matters is what it does them. This is a great example to illustrate the difference between the two programming paradigms. In short, HTML is a declarative abstraction wrapped around an imperatively implemented web browser rendering engine. He cares about how, so you only need to worry about what. You can enjoy life while writing declarative HTML because the good people from Mozilla, Google and Apple wrote imperative code for you when they created your web browser.

JavaScript is an imperative language


We already looked at a simple example of imperative JavaScript in the above shopping list example, and I described how the complexity of application functions affects the effort required to implement them and the likelihood of errors in this implementation.

Now let's look at a slightly more complex task and see how it can be simplified using a declarative approach. Imagine a web page that contains the following:

  • a list of flagged flags, each line of which changes its color when selected;
  • text below, for example, “1 of 4 selected”, which should be updated when the flags are changed;
  • Select All button, which should be disabled if all the checkboxes are already selected;
  • Select None button, which should be disabled if the checkboxes are not selected.

Here is an implementation in plain HTML, CSS, and imperative JavaScript:



const captionText = document.getElementById('caption-text');
const checkAllButton = document.getElementById('check-all-button');
const uncheckAllButton = document.getElementById('uncheck-all-button');
const checkboxes = document.querySelectorAll('input[type="checkbox"]');

function updateSummary() {
  let numChecked = 0;
  checkboxes.forEach(function(checkbox) {
    if (checkbox.checked) {
      numChecked++;
    }
  });
  captionText.innerHTML = `${numChecked} of ${checkboxes.length} checked`;
  if (numChecked === 0) {
    checkAllButton.disabled = false;
    uncheckAllButton.disabled = true;
  } else {
    uncheckAllButton.disabled = false;
  }
  if (numChecked === checkboxes.length) {
    checkAllButton.disabled = true;
  }
}

checkAllButton.addEventListener('click', function() {
  checkboxes.forEach(function(checkbox) {
    checkbox.checked = true;
    checkbox.closest('tr').className = 'checked';
  });
  updateSummary();
});

uncheckAllButton.addEventListener('click', function() {
  checkboxes.forEach(function(checkbox) {
    checkbox.checked = false;
    checkbox.closest('tr').className = '';
  });
  updateSummary();
});

checkboxes.forEach(function(checkbox) {
  checkbox.addEventListener('change', function(event) {
    checkbox.closest('tr').className = checkbox.checked ? 'checked' : '';
    updateSummary();
  });
});

Headache as it is


To implement this function using imperative JavaScript, we need to give the browser some detailed instructions. This is a verbal description of the code from the example above.
In our HTML, we declare the original page structure:
  • There are four row elements (a row of a table, it is a list), each of which contains a flag. The third flag is checked.
  • There is a text “1 of 4 selected”.
  • There is a Select All button that is turned on.
  • There is a Select None button, which is disabled.

In our JavaScript, we write instructions on what needs to be changed when each of these events happens:
When a flag changes from unmarked to marked:
  • Find the row containing the checkbox and add the CSS class .selected to it.
  • Find all the flags in the list and calculate how many of them are marked and how many are not.
  • Find the text and update it, indicating the correct number of selected purchases and their total number.
  • Find the Select None button and turn it on if it was disabled.
  • If all the checkboxes are now checked, find the Select All button and turn it off.

When a flag changes from marked to unmarked:
  • Find the row containing the flag and remove the .selected class from it.
  • Find all the checkboxes in the list and calculate how many of them are checked and how many are not.
  • Find the resume text item and update it with the verified number and total.
  • Find the Select All button and turn it on if it was disabled.
  • If all the boxes are unchecked, find the Select None button and turn it off.

When the Select All button is pressed:
  • Find all the checkboxes in the list and mark them.
  • Find all the rows in the list and add the .selected class to them.
  • Find the text and update it.
  • Find the Select All button and turn it off.
  • Find the Select None button and turn it on.

When the Select None button is pressed:
  • Find all the checkboxes in the list and clear all the checkboxes.
  • Find all the rows in the list and remove the .selected class from them.
  • Find the resume text element and update it.
  • Find the Select All button and turn it on.
  • Find the Select None button and turn it off.

Wow ... That's a lot, right? Well, we better not forget to write code for all possible situations. If we forget or mess up any of these instructions, we will get an error: the totals will not match the checkboxes, or the button will turn on that does nothing when you click on it, or the selected row will get the wrong color or something yet, which we have not thought about and will not know until the user complains.

We really have a big problem here: there is no entity that would contain complete information about the stateof our application (in this case, this is the answer to the question “what flags are marked?”) and would be responsible for updating this information. Flags, of course, know if they are checked, but CSS code for the rows of the table, text, and each button should also be aware of this. Five copies of this information are stored separately throughout the HTML, and when it changes in any of these places, the JavaScript developer must catch this and write imperative code to synchronize with changes in other places.

And this is still a simple example of one small page component. Even if this set of instructions looks like a headache, imagine how complex and fragile a larger web application becomes when you need to implement all this in imperative JavaScript. For many complex, modern web applications, this solution does not scale from the word "completely."

We are looking for the source of truth


Tools like React allow declarative use of JavaScript. Just as HTML is a declarative abstraction over the instructions for displaying in a web browser, so React is a declarative abstraction over JavaScript.

Remember how HTML allows us to focus on the structure of the page rather than implementation details, and how does the browser display this structure? Similarly, when we use React, we can focus on the structure, defining it based on data stored in one place. This process is called one-way binding, and the location of the application state data is called the “single source of truth.” When the source of the truth changes, React will automatically update the page structure for us. He will take care of the required steps backstage, as a web browser for HTML does. Although React is used as an example here, this approach also works for other frameworks such as Vue.

Let's get back to our list of checkboxes from the example above. In this case, the “truth” that we want to know is rather concise: which flags are checked? Other details (for example, text, color of lines, which buttons are turned on) are already information obtained on the basis of the source of truth. And why should they have their own copy of this information? They should simply use a single read-only source of truth, and all page elements should “just know” which checkboxes are checked and behave accordingly. You can say that table rows, text and buttons should be able to automatically respond to a checkbox depending on whether it is checked or not (“see what’s happening there?”)

Tell me what you want (what you really want)


To implement this page with React, we can replace the list with a few simple fact descriptions:

  • There is a list of true / false values ​​called checkboxValues ​​that shows which fields are marked.
    • Example: checkboxValues ​​\ u003d [false, false, true, false]
    • This list shows that we have four flags, only the third is set.

  • For each value in checkboxValues ​​in the table, there is a row that:
    • has a CSS class called .selected if the value is true, and
    • contains a flag that is checked if the value is true.

  • There is a text element that contains the text "{x} of {y} selected", where {x} is the number of true values ​​in checkboxValues, and {y} is the total number of values ​​in checkboxValues.
  • There is a Select All button that is enabled if checkboxValues ​​has false values.
  • Select None, , checkboxValues ​​ true.
  • , checkboxValues.
  • Select All , checkboxValues ​​ true.
  • Select None checkboxValues ​​ false.

You will notice that the last three paragraphs are still imperative instructions (“When this happens, do it”), but this is the only imperative code that we need to write. These are three lines of code, and they all update a single source of truth.

The rest is declarative statements (“there is ...”), which are now built directly into the definition of the page structure. To do this, we write code for our elements using a special JavaScript syntax extension - JavaScript XML (JSX). It resembles HTML: JSX allows you to use HTML-like syntax to describe the structure of the interface, as well as regular JavaScript. This allows us to mix JS logic with the HTML structure, so the structure can be different at any time. It all depends on the contents of checkboxValues.

We rewrite our example on React:



function ChecklistTable({ columns, rows, initialValues, ...otherProps }) {
  const [checkboxValues, setCheckboxValues] = React.useState(initialValues);
  
  function checkAllBoxes() {
    setCheckboxValues(new Array(rows.length).fill(true));
  }
  
  function uncheckAllBoxes() {
    setCheckboxValues(new Array(rows.length).fill(false));
  }
  
  function setCheckboxValue(rowIndex, checked) {
    const newValues = checkboxValues.slice();
    newValues[rowIndex] = checked;
    setCheckboxValues(newValues);
  }
  
  const numItems = checkboxValues.length;
  const numChecked = checkboxValues.filter(Boolean).length;
  
  return (
    <table className="pf-c-table pf-m-grid-lg" role="grid" {...otherProps}>
      <caption>
        <span>{numChecked} of {numItems} items checked</span>
        <button
          onClick={checkAllBoxes}
          disabled={numChecked === numItems}
          className="pf-c-button pf-m-primary"
          type="button"
        >
          Check all
        </button>
        <button
          onClick={uncheckAllBoxes}
          disabled={numChecked === 0}
          className="pf-c-button pf-m-secondary"
          type="button"
        >
          Uncheck all
        </button>
      </caption>
      <thead>
        <tr>
          <td />
          {columns.map(function(column) {
            return <th scope="col" key={column}>{column}</th>;
          })}
        </tr>
      </thead>
      <tbody>
        {rows.map(function(row, rowIndex) {
          const [firstCell, ...otherCells] = row;
          const labelId = `item-${rowIndex}-${firstCell}`;
          const isChecked = checkboxValues[rowIndex];
          return (
            <tr key={firstCell} className={isChecked ? 'checked' : ''}>
              <td className="pf-c-table__check">
                <input
                  type="checkbox"
                  name={firstCell}
                  aria-labelledby={labelId}
                  checked={isChecked}
                  onChange={function(event) {
                    setCheckboxValue(rowIndex, event.target.checked);
                  }}
                />
              </td>
              <th data-label={columns[0]}>
                <div id={labelId}>{firstCell}</div>
              </th>
              {otherCells.map(function(cell, cellIndex) {
                return (
                  <td key={cell} data-label={columns[1 + cellIndex]}>
                    {cell}
                  </td>
                );
              })}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
};

function ShoppingList() {
  return (
    <ChecklistTable
      aria-label="Shopping list"
      columns={['Item', 'Quantity']}
      rows={[
        ['Sugar', '1 cup'],
        ['Butter', '½ cup'],
        ['Eggs', '2'],
        ['Milk', '½ cup'],
      ]}
      initialValues={[false, false, true, false]}
    />
  );
}

ReactDOM.render(
  <ShoppingList />,
  document.getElementById('shopping-list')
);


JSX looks peculiar. When I first encountered this, it seemed to me that it was simply impossible to do so. My initial reaction was: “What? HTML cannot be inside JavaScript code! ” And I was not the only one. However, this is not HTML, but JavaScript dressed up as HTML. In fact, this is a powerful solution.

Remember those 20 imperative instructions above? Now we have three of them. The rest (internal) imperative instructions React itself executes for us behind the scenes - every time checkboxValues ​​changes.

With this code, the situation can no longer occur when the text or color of the line does not match the checkboxes, or when the button is on, although it should be disabled. There is a whole category of errors that now simply cannot occur in our web application. All work is done on the basis of a single source of truth, and we developers can write less code and sleep better at night. Well, JavaScript developers, at least ...

JavaScript defeated HTML: starved


As web applications become more complex, maintaining the classic separation of tasks between HTML and JavaScript is becoming more painful. HTML was originally designed for static web pages. To add more complex interactive functions there, it is necessary to implement the appropriate logic in imperative JavaScript, which with each line of code becomes more and more confused and fragile.

Advantages of the modern approach: predictability, reusability and combination


The ability to use a single source of truth is the most important advantage of this model, but it has other advantages. Defining our page elements in JavaScript code means that we can reuse components (individual blocks of a web page), preventing us from copying and pasting the same HTML code in several places. If we need to change a component, just change its code in only one place. In this case, changes will occur in all copies of the component (inside one or even in many web applications, if we use reusable components in them).

We can take simple components and put them together like LEGO cubes, creating more complex and useful components without making their logic too confusing. And if we use components created by other developers, we can easily roll up updates or bug fixes without having to rewrite our code.

The same JavaScript, only in profile


These benefits have a downside. There are good reasons why people appreciate the separation of HTML and JavaScript. As I mentioned earlier, abandoning regular HTML files complicates the workflow for someone who hasn't worked with JavaScript before. Those who previously could independently make changes to the web application, now must acquire additional complex skills in order to maintain their autonomy, or even place in the team.

There are also technical flaws. For example, some tools, such as linters and parsers, accept only normal HTML as input, and working with third-party JavaScript plug-ins may be more difficult instead. In addition, JavaScript is not the best language, but it is a uniform standard accepted for web browsers. New tools and features make it better, but there are still some pitfalls in it that you need to know about before you work with it.

Another potential problem: when the semantic structure of the page is broken into abstract components, the developer may stop thinking about which HTML elements will be generated as a result. Specific HTML tags such as section and aside, have their own semantics, which are lost when using general-purpose tags such as div and span - even if they visually look the same on the page. This is especially important for making the web application accessible to different categories of users.

For example, this will affect the behavior of the screen reader for visually impaired users. Perhaps these are not the most interesting tasks for the developer, but JavaScript developers should always remember that preserving the semantics of HTML in this case is the most important task .

Conscious need vs unconscious trend


Recently, the use of frameworks in every project has become a trend. Some people believe that the separation of HTML and JavaScript is outdated, but it is not. For a simple static website that does not require complicated user interaction, this is just right. More ardent React fans may disagree with me here, but if all your JavaScript does is create a non-interactive web page, you should not use it. JavaScript doesn't load as fast as regular HTML. Therefore, if you do not set the task to gain new development experience or improve the reliability of the code, JavaScript here will do more harm than good.

In addition, there is no need to write your entire website in React. Or Vue! Or what else is there ... Many people do not know this, because all the tutorials basically show how to use React to develop a site from scratch. If you have only one small complex widget on a simple website, you can use React for one component . You don’t always have to worry about webpack, Redux, Gatsby or anything else that someone recommends as “best practices” there.

But if the application is quite complex, using a modern declarative approach is absolutely worth it. Is React the best solution? No. He already has strong competitors. And then more and more will appear ... But declarative programming will not go anywhere, and in some new framework, this approach will probably be rethought and implemented even better.


All Articles