2020-06 hey.com

A little bit of plain Javascript can do a lot

scroll through the page with .scrollIntoView

The last fun thing I learned about is .scrollIntoView – I wanted to scroll down to the next question automatically when someone clicked “next question”. Turns out this is just one line of code:

row.classList.add('revealed');
row.scrollIntoView({behavior: 'smooth', block: 'center'});

another vanilla JS example: peekobot ( https://github.com/peekobot/peekobot )

Another small example of a plain JS library I thought was nice is peekobot, which is a little chatbot interface that’s 100 lines of JS/CSS.

Looking at its Javascript, it uses some similar patterns – a lot of .classList.add, some adding elements to the DOM, some .querySelectorAll.

I learned from reading peekobot’s source about .closest which finds the closest ancestor that matches a given selector.

That seems like it would be a nice way to get rid of some of the .parentElement.parentElement that I was writing in my Javascript, which felt a bit fragile.

plain Javascript can do a lot !

I was pretty surprised by how much I could get done with just plain JS.

I ended up writing about 50 lines of JS to do everything I wanted to do, plus a bit extra to collect some anonymous metrics about what folks were learning.

Javascript code

  1let app = {}
  2function start() {
  3    let answers = document.querySelectorAll('.answer');
  4    app.rows = Array.from(document.querySelectorAll('.row'));
  5    /* add "next question" button to each row */
  6    for (row of app.rows) {
  7        var div = document.createElement("div");
  8        div.id = 'buttons-under'
  9        div.innerHTML = `
 10            <button class="next-question"><i class="icon-down-small"></i>next question</button>
 11            <button class="learned-something"><i class="icon-lightbulb"></i>I learned something!</button>`
 12        row.appendChild(div);
 13        div.querySelector('.next-question').onclick = function () {
 14            show_next_row();
 15            this.parentElement.parentElement.classList.add('done');
 16            this.parentElement.removeChild(this);
 17        }
 18        div.querySelector('.learned-something').onclick = function() {
 19            learned_something(this);
 20        }
 21        if (row === app.rows[app.rows.length-1]) {
 22            row.classList.add('last-row');
 23            // don't render "next question" for the last question
 24            div.removeChild(div.querySelector('.next-question'));
 25        }
 26    }
 27
 28    $('#button-start').onclick = function() {
 29        show_next_row();
 30        this.setAttribute('style', 'display: none;');
 31    }
 32
 33    $('#button-reveal-all').onclick = function() {
 34        reveal_all();
 35        this.parentElement.setAttribute('style', 'display: none;');
 36    }
 37    $('#send-learned').onclick = function(event) {
 38        event.preventDefault();
 39        let summary = $('#write-learned').value;
 40        metrics_set_learned_summary(summary);
 41        $('#send-learned').classList.add('sent');
 42        $('#send-learned').innerText = "♥ thank you ♥";
 43    }
 44}
 45
 46function reveal_all() {
 47    document.querySelectorAll('body')[0].classList.add('all-revealed');
 48}
 49
 50function learned_something(button) {
 51    button.parentElement.parentElement.classList.add('learned');
 52    button.innerHTML = `<i class="icon-lightbulb"></i>I learned something!
 53        <object data="/confetti.svg" width="30" height = "30"> </object>
 54        `;
 55    metrics_set_learned(button.parentElement.parentElement);
 56}
 57
 58function show_next_row() {
 59    if (app.rows.length > 0) {
 60        let row = app.rows.shift();
 61        row.classList.add('revealed');
 62        row.scrollIntoView({behavior: 'smooth', block: 'center'});
 63        row.querySelector('.answer').onclick = function(event) {
 64            this.classList.add('revealed');
 65            this.onclick = null;
 66            this.parentElement.classList.add('question-revealed');
 67        }
 68    }
 69
 70}
 71
 72/* some anonymous metrics */
 73
 74var firedb;
 75var metrics = {
 76};
 77
 78
 79function metrics_for_path() {
 80    var path = metrics_path();
 81    if (!metrics[path]) {
 82        metrics[path] = {'learned': [], 'summary': []};
 83    }
 84    return metrics[path];
 85}
 86
 87
 88function metrics_set_learned_summary(summary) {
 89    metrics_init();
 90    metrics_for_path()['summary'].push(summary);
 91    metrics_for_path()['latest_timestamp'] = firebase.firestore.FieldValue.serverTimestamp();
 92    metrics_update();
 93}
 94
 95function metrics_set_learned(row) {
 96    metrics_init();
 97    let timestamp = firebase.firestore.FieldValue.serverTimestamp();
 98    var question = row.querySelectorAll('.question')[0].children[0].innerHTML;
 99    metrics['latest_timestamp'] = timestamp;
100    metrics_for_path()['latest_timestamp'] = timestamp;
101    metrics_for_path()['learned'].push(question);
102    metrics_update();
103}
104
105function metrics_path() {
106    return window.location.pathname.substr(1).split('.')[0]
107}
108
109function metrics_document() {
110    if (!localStorage.getItem('uuid')) {
111        localStorage.setItem('uuid', metrics_uuidv4());
112    }
113    let uuid = localStorage.getItem('uuid');
114    return firedb.collection("questions").doc(uuid)
115}
116
117
118function metrics_uuidv4() {
119    return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
120        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
121    );
122}
123
124function metrics_update() {
125    metrics_document().update(metrics).catch(() => metrics_document().set(metrics));
126}
127
128function metrics_init() {
129    if (firedb) {
130        return;
131    }
132    var firebaseConfig = {
133        apiKey: "AIzaSyCEbH_2zf9dDzh6Muq-DdK3byHMUIsHfCA",
134        authDomain: "wizard-sql-school.firebaseapp.com",
135        databaseURL: "https://wizard-sql-school.firebaseio.com",
136        projectId: "wizard-sql-school",
137        storageBucket: "",
138        messagingSenderId: "779622015462",
139        appId: "1:779622015462:web:b0ca93a1d5fbb1ea2d3627"
140    };
141    firebase.initializeApp(firebaseConfig);
142    firedb = firebase.firestore();
143
144    metrics['start_timestamp'] = firebase.firestore.FieldValue.serverTimestamp();
145}
146
147
148/* utilities */
149
150function $(selector) {
151    return document.querySelector(selector);
152}
153
154function $$(selector) {
155    return document.querySelectorAll(selector);
156}
157
158function $e(tag='div', props={}, children=[]){
159    let el=document.createElement(tag);
160    Object.assign(el, props);
161    el.append(...children);
162    return el;
163}

Hey: another technically successful Rails monolith app with simple, server rendered HTML and a bit of JS ?

https://fediverse.org/jonasdowney/status/1275451607488933892?s=20

Check out this new REWORK episode where we go behind the scenes of the 2+ year process of making HEY.

How we took on a tough problem, explored ideas, gradually figured out the vision, and brought in the whole company to make it real https://rework.fm/designing-hey/

https://rework.fm/designing-hey/

Basecamp design lead Jonas Downey was one of the first people to experiment with what would eventually become Hey, Basecamp’s newly launched email service.

Jonas comes on Rework to talk about building software for humans, preserving a sense of fun weirdness as a new product evolves, and managing a big launch during a tumultuous time.

https://fediverse.org/VitalyPushkar/status/1274108941333643272?s=20

While everyone is focused on the David (@dhh) vs Goliath (Apple) saga development, can we take a moment and appreciate the fact that Hey is yet another technically successful Rails monolith app with simple, server rendered HTML and a bit of JS ?

https://fediverse.org/sstephenson/status/1272608706908360710?s=20

So much new stuff! Now we begin the process of refining and documenting all this new tech from HEY into open-source software the rest of the world can benefit from.

I can’t wait to share it with you when it’s ready

alekseykulikov

See also

hey.com is a love letter to the open web

Why ?

  • All pages get 95+ with Lighthouse v6

  • Beautiful CSS (variables, grid)

  • Minimum JS (7 KB per page)

  • No 3rd-party scripts

  • Subsetted Variable Font

  • Responsive lazy loaded images

The app.js for http://hey.com is 7.1 KB. That’s it – no 3rd-party scripts.

Your home page should not be a SPA with hundreds of trackers. When done right, default browser navigation feels smooth and native.

If you are interested in building fast web applications, like HEY, check out @__treo ( https://treo.sh )

https://mxb.dev/blog/the-return-of-the-90s-web/

Always bet on HTML

You don’t need SPAs to create a crazy fast web app that people really like

Stimulusjs (hey)

It really is a remarkably different paradigm. One that I’m sure many veteran JavaScript developers who’ve been used to work with contemporary frameworks will scoff at.

And hey, scoff away.

If you’re happy with the complexity and effort it takes to maintain an application within the maelstrom of, say, React + Redux, then Turbolinks + Stimulus will not appeal to you.

If, on the other hand, you have nagging sense that what you’re working on does not warrant the intense complexity and application separation such contemporary techniques imply, then you’re likely to find refuge in our approach.