jbanes / versus-js

A comparison between jQuery and ES6 approaches to web interface development
18 stars 9 forks source link

Alternate solution #2

Open Merri opened 1 month ago

Merri commented 1 month ago

Since we're dealing with the web:

  1. Include lang="en"
  2. Include charset
  3. Use <a href> for the pagination links
  4. Use aria-current="page" to indicate selection
  5. Content in <main>

And then tech upgrades / changes:

  1. type="module" so no more DOMContentLoaded needed.
  2. Use single click event listener instead of event handler per element.
  3. Using links added keyboard support for free.

The rest is just my own coding style balancing for a minimalistic but readable approach since this doesn't really need to go for full throttle on abstractions.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>ES6 Demonstration</title>
    <style>
    .pager { margin: 5px 10px; font-family: sans-serif; }
    .pager .page { color: inherit; display: inline-block; padding: 0px 5px; text-decoration: none; }
    .pager .page:active { color: red; }
    .page[aria-current="page" i] { font-weight: bold; color: red; }
    .paged-content { font: bold 250% sans-serif; padding: 25px 10px; }
    </style>
    <script type="module">
        let currentPage = 1;

        function renderPagers(pageCount) {
            const pagers = document.querySelectorAll('.pager');
            let html = `Page: `;
            for (let i = 1; i <= pageCount; i++) {
                const current = currentPage === i ? ` aria-current="page"` : '';
                html += `<a href="#content" class="page" data-page="${i}"${current}>${i}`;
            }
            pagers.forEach(pager => void (pager.innerHTML = html));
        }

        function updatePage(page) {
            const pageCount = 5;
            currentPage = Number(page) || currentPage;
            renderPagers(pageCount);
            document.querySelector('#content').textContent = `Page ${currentPage}`;
        }

        document.addEventListener('click', function(event) {
            const page = event.target.closest('[data-page]')?.dataset.page;
            if (page) updatePage(page);
        });

        updatePage();
    </script>
</head>
<body>
    <div id="top_pager" class="pager"></div>
    <main id="content" class="paged-content"></main>
    <div id="bottom_pager" class="pager"></div>
</body>
</html>

I guess you'll need a more complex / real lifey example because I don't feel like this code would need jQuerifying. It might get more interesting if you enforced some usability improvements like "focus must remain where it is" as focus is one of the things that gets lost when using innerHTML (or jQuery's .empty()). That single limitation would force a lot more complexity by the need of reusing existing DOM.

Danny-Engelman commented 1 month ago

Seems a bit weird; jQuery is all about optimizing DOM handling.
To then compare it to ES6 code that just always redraws everything is... apples and oranges.

It is 2024 (but this has worked for at least since january 2018 , when Edge switched to using the Chromium engine)

Use a modern native Web Component.

Librairies and Frameworks CREATE HTML, Web Components ARE HTML

So

<nav-pager>
  <nav>Page 1</nav>
  <nav>Page 2</nav>
  <nav>Page 3</nav>
  <nav>Page 4</nav>
  <nav>Page 5</nav>
</nav-pager>

<nav-pager>
  <nav title="Foo">Page 1</nav>
  <nav title="Bar" selected>Page 2</nav>
  <nav title="Baz">Page 3</nav>
</nav-pager>

Is shown as:

Defining the Web Component once and ANYTIME you want; at the start of your load, at end, or anytime that suits you All existing (but undefined) Web Components in the DOM will automagically be upgraded.

LIVE: https://jsfiddle.net/WebComponents/c6rvm5tq/

  customElements.define('nav-pager', class extends HTMLElement {
    get selectednav() {
      return this.querySelector("nav[selected]");
    }
    get navs() {
      return [...this.querySelectorAll("nav")]; // convert NodeList to Array
    }
    connectedCallback() {
      setTimeout(() => this.render()); // wait till <nav-pager> innerHTML is parsed
    }
    show(nav = this.selectednav || this.navs[0]) {
      this.shadowRoot.querySelector("slot").assign(nav); // manual slot assigment
      this.spans.forEach(sp => sp.removeAttribute("selected")); // remove highlighted page numbers
      nav.spans.forEach(sp => sp.setAttribute("selected", true)); // highlight current page numbers
    }
    render() {
      this.render = () => {} // render once
      this.spans = [];
      const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props);
      const createPageSelector = () => this.navs.map((nav, idx) => {
        let span = createElement("span", {
            part: "pagenr",  // allow styling from Global CSS, what the other half of the blogs whine about
            textContent: nav.title || ++idx, // use .title or new page number
            onclick: evt => this.show(nav)
        }); // createElement <span>
        this.spans.push(span); // store all <span> on <nav-pager>
        (nav.spans = nav.spans || []).push(span); // store all <span> on matching <nav>
        return span;
      }); // createPageSelector()
      this.attachShadow({
        mode: "open",
        slotAssignment: "manual" // not often used, half of the blogs that whine about shadowDOM can use this
      }).append(
        createElement("style", {
          textContent: `:host{display:block}`+
            `:host{margin:5px 10px;user-select:none; -webkit-user-select: none;font-family: sans-serif}` +
            `span{border:1px solid #ccc; padding:5px;display:inline-block}` +
            `span[selected]{font-weight:bold;color:red}` +
            `::slotted(*){display:block;background:pink;font:bold 250% sans-serif;padding:15px 10px}`
        }),
        "Page: ",
        ...createPageSelector(), // spread all <span>
        createElement("slot"), // for manual slot assignment
        "Page: ",
        ...createPageSelector(), // spread all <span>
      ); // append
      this.show();
    } // render
  });// defined <nav-pager>

Now available on GitHub

https://nav-pager.github.io

Load it with <script src="https://nav-pager.github.io/element.min.js"></script>

Then use:

<nav-pager>
  <nav title="Foo">Page 1</nav>
  <nav title="Bar" selected>Page 2</nav>
  <nav title="Baz">Page 3</nav>
</nav-pager>

LOC is a great metric for managers, if we deliver code; GZip size is what the server delivers and counts

Merri commented 4 weeks ago

There is a more relevant PR to discuss Web Components.

Seems a bit weird; jQuery is all about optimizing DOM handling. To then compare it to ES6 code that just always redraws everything is... apples and oranges.

The jQuery implementation throws DOM away, it doesn't re-use it. Changing to a link proves this:

            function createPage(page, selected)
            {
                return $("<a>")
                            .addClass("page" + (selected ? " selected" : ""))
                            .attr('href', '#')
                            .text(page)
                            .click(function() {
                                content.pagedContent("page", page);
                                that.pager(content);
                            });
            }

After clicking focus is no longer in the link.

LOC is a great metric for managers, if we deliver code; GZip size is what the server delivers and counts

The code that needs to be parsed and executed is what counts. You can have a very tiny gzip file, but if it decompresses to a massive blob of JS you will see a performance hit. So: minified/uglified JS size is probably what you want to compare against if you aim for fair comparisons.

Code clarity and understandability could be it's own metric. And to me coding style / formatting convention is irrelevant if the code is readable and gets the message through. Usually the most boring code wins the day.

Although in the end this doesn't matter a lot at least in this case. For me this jQuery vs ES6 as a rather silly fun exercise than anything actually serious, so I mostly wanted to point out some of the crucial missing things even in the "minimal" implementation - which in my opinion isn't minimal but broken due to all the missing web basics.