6
\$\begingroup\$

for clarity: moved here from stackoverflow after being pointed to Code Review being the better place for this question

I love async/await and Promises since I got my hands on them. And I might be overdoing it, but it feels like there should be a good and readable way to utilize async/await to get a little closer to a flow like, functionalISH programming style.

I would love to not have to only use async/await to wait for web resources to come back but also to wait for user input when I await it.

So far i have some code working similar to this shortened demo where I wrap a one time only EventListener into a Promise:

//// MAIN ///////

(async function(){

    //...do some async await stuff here... fetching stuff from a server
    // let services = await fetch(...) for example
    let services = [{url:"x",label:"1"},{url:"y",label:"2"},{url:"z",label:"3"}]


    let service_chosen = await showServicesToUserAndAwaitInput(services); 
    console.log("service_chosen:",service_chosen);
    // ... go on....
})()

//// END MAIN /////


async function showServicesToUserAndAwaitInput(services){
    
    if (services.length < 1){return null}

    let choice = null;
    
    let serviceList = document.querySelector("#serviceList");
    
    // show list element
    serviceList.classList.remove("hidden") 

    // create some elements for the user to interact with
    for (let service of services){
        let button = document.createElement("BUTTON");
        button.innerHTML = service.label;
        button.addEventListener("click",function(){
          document.dispatchEvent(
            new CustomEvent('serviceChosen', { detail:service })
          )
        });
        serviceList.appendChild(button);
    }
    
    // returns promise with one time only event listener
    return new Promise((resolve,reject)=>{ 
        document.addEventListener("serviceChosen",function(e){
            serviceList.classList.add("hidden") // hide again for we are done
            resolve(e.detail)
        },{ once: true })
    })
}
.hidden{
  visibility: hidden
}
<div id="serviceList" class="hidden">

</div>

But something about this use of the EventListener bugs me. Also: I use a promise that always resolves, which also seems strange.

On the upside: I get to read the code top to bottom and can follow the user flow within the MAIN without having to chase down events, callbacks and so on.

Yet, it feels like I am reinventing something somebody else might have already normed. So:

Is there a better way to achieve this? Are there best practices to work with user interactions or other DOM events in a async and/or thenable way?

\$\endgroup\$
4
  • 2
    \$\begingroup\$ Usually events are not a good match for one-shot promises. If trying to make them work together, then you should be removing your event handler after it fires and you resolve the promise so it won't just accumulate and won't run this event handler code again. \$\endgroup\$
    – jfriend00
    Oct 8 '20 at 7:05
  • \$\begingroup\$ thank you! Good to know, yet: as mentioned in my comment to MauriceNino's answer: What is a good-match for DOM events and trying a more functional(ish) style code? Promises always seemed to me kinda better in that regard. e.g. in order to avoid too many side effects (as in cb hell or multiple EventListeners being open. Also: I thought the { once:true } parameter in my EventListener would do what you describe: close the listener after first call. Am I wrong with that, too? oh my... \$\endgroup\$ Oct 8 '20 at 9:12
  • \$\begingroup\$ Asynchronous event driven things do not translate to functional code. They just don't. And, trying to force a square peg into a round hole will just create a mess. Learn to use event driven programming for handling event driven stuff. \$\endgroup\$
    – jfriend00
    Oct 8 '20 at 20:01
  • \$\begingroup\$ @jfriend00 i understand your comments trajectory. But am left with a: What does learning event driven programming mean when I have to keep track with a very distinct and clear user flow (aka one thing after another) but have to solve it in an environment like javascript that is all about handling things async? Any suggestions, references what would be an orderly approach to event based programming? \$\endgroup\$ Oct 9 '20 at 7:00
4
\$\begingroup\$

You should use observables and the myriad of functions available for them. It's called reactive programming, but I think it might fall into the category of functionalish.

An example from memory:

import { fromEvent, takeUntil } from 'rxjs';

...

const eventListener = document.addEventListener('serviceChosen', () => console.log('chosen'));

const unsubscribe = new Subject();

fromEvent(eventListener)
    .pipe(takeUntil(unsubscribe))
    .subscribe({
        next: event => {
            serviceList.classList.add('hidden');
            unsubscribe.next();
            unsubscribe.complete();
        }
    });

It may look a little bit overkill for a one-time action, however I think it's better than a promise, and in general you should use observables over promises. I would describe the difference as promises being "active" listeners as they expect to complete, while observable subscriptions are more "passive" since they may never get fired and don't expect to be, they're just setup to fire when they are needed.

\$\endgroup\$
11
  • 1
    \$\begingroup\$ wow. thank you so much. i never heard about reactive programming but it indeed seems to be what I am actually looking for and trying to describe with "flow", "await", "functional(ish)". that is indeed very helpful. Is rxjs the only implementation of observables in js? I think i stumbled upon the term first in the vue landscape but am not sure if it means the same thing or just happens to be a similar terminology (also never got it within using vue) \$\endgroup\$ Oct 8 '20 at 10:38
  • 1
    \$\begingroup\$ @gauguerilla I think Observables were originally made by rxjs. At this point rxjs is practically synonymous with javascript for any medium-sized to enterprise application. Even small applications benefit from some of its features. Many (all?) of the popular frameworks use rxjs under the hood in some capacity. \$\endgroup\$
    – Dom
    Oct 8 '20 at 13:02
  • 1
    \$\begingroup\$ checking rxjs now (and loosing a lot of time on it, oups), I must say: This is EXACTLY what I was looking for (and a lot more I dont quite grasp yet). So I will accept this as the answer to my question yet hoping that there are more people who have tipps/ best practises on this. \$\endgroup\$ Oct 8 '20 at 14:54
  • 3
    \$\begingroup\$ @gauguerilla To answer the question: Functional Reactive Programming was created by Conal Elliott and Paul Hudak for the Fran Functional Reactive Animation system in Haskell in 1997. Flapjax was a language for Functional Reactive Ajax that compiled to JavaScript. There were a couple of other languages and frameworks as well, but what we now understand as "Reactive X" or "RxX" (sort of "OORP" instead of "FRP") was first implemented by Erik Meijer and his team at Microsoft as "Rx.NET", shortly thereafter came RxJS (not sure if that also came out of Microsoft), and then many other. \$\endgroup\$ Oct 8 '20 at 17:01
  • 1
    \$\begingroup\$ And here's the original paper on FRP and Fran: conal.net/papers/icfp97/icfp97.pdf \$\endgroup\$ Oct 8 '20 at 17:06
5
\$\begingroup\$

First of all, I would not use Promises to do event based programming. It is not used like that, so your code will get harder to follow.

Also, I would advise you to split up your functions more and omit some comments that way.

//// MAIN ///////

let services = [
    { url: "x", label: "1" },
    { url: "y", label: "2" },
    { url: "z", label: "3" },
];

showServicesToUser(services);

//// END MAIN /////

const serviceList = document.querySelector("#serviceList");

function createButtonFromService(service) {
    let button = document.createElement("BUTTON");
    button.innerHTML = service.label;
    button.addEventListener("click", function () {
        document.dispatchEvent(
            new CustomEvent("serviceChosen", { detail: service })
        );
    });
    serviceList.appendChild(button);
}

function showServicesToUser(services) {
    if (services.length < 1) return;

    serviceList.classList.remove("hidden");

    for (let service of services) {
        createButtonFromService(service);
    }
}

function chooseService(service) {
    console.log("service_chosen:", service);
    // ... go on....
}

document.addEventListener("serviceChosen", function (e) {
    serviceList.classList.add("hidden"); // hide again for we are done
    chooseService(e.detail);
}, { once: true });
.hidden{
  visibility: hidden
}
<div id="serviceList" class="hidden">

</div>

My proposed code above is written in a scripted kind of way, with a global variable (serviceList), but you could also write this all in a class (especially if you have more code), to make it more readable and reusable.

class ServiceChooser {
    serviceListSelector;
    serviceList;
    services;

    constructor(serviceListSelector, services) {
        this.serviceListSelector = serviceListSelector;
        this.services = services;

        this.serviceList = document.querySelector(serviceListSelector);
        
        document.addEventListener(`serviceChosen${this.serviceListSelector}`, (e) => this.chooseService(e.detail), { once: true });
    }

    createButtonFromService(service) {
        let button = document.createElement("BUTTON");

        button.innerHTML = service.label;
        button.addEventListener("click", () => {
            const event = new CustomEvent(`serviceChosen${this.serviceListSelector}`, { detail: service });
            document.dispatchEvent(event);
        });

        this.serviceList.appendChild(button);
    }

    chooseService(service) {
        this.serviceList.classList.add("hidden");
        console.log("service_chosen:", service);
        // ... go on....
    }
    
    showServicesToUser() {
        if (this.services.length < 1) return;

        this.serviceList.classList.remove("hidden");

        for (let service of this.services) {
            this.createButtonFromService(service);
        }
    }
}

//// MAIN ///////

let services = [
    { url: "x", label: "1" },
    { url: "y", label: "2" },
    { url: "z", label: "3" },
];

const sc = new ServiceChooser('#serviceList', services);
sc.showServicesToUser();

// now you could add 2 service choosers, sperate from each other
const sc2 = new ServiceChooser('#serviceList2', services);
sc2.showServicesToUser();

//// END MAIN /////
.hidden{
  visibility: hidden
}
<div id="serviceList" class="hidden">

</div>
<div id="serviceList2" class="hidden">

</div>

\$\endgroup\$
2
  • \$\begingroup\$ thank you. the splitting up makes a lot of sense to me. the issue with the readability and clarity of the code does as well in some way but i wonder: How do I force myself within javascript to a more or less functional style (i mean: being able to read the main part of the code top to bottom with no side effects) considering DOM events being a thing in html and js? \$\endgroup\$ Oct 8 '20 at 9:08
  • 1
    \$\begingroup\$ it really depends on the size of the site you are working on and the kind of framework. I rarely use vanilla JS to write sites, but if I would, I would organize them into smaller scripts and build them together into one script. Then you would maybe have your event-handlers in one script and your functions for the service chooser in another one. I also really like the angular approach, where everything is bundled together per component. You could also emulate this, but with a bit more template code. @gauguerilla \$\endgroup\$ Oct 8 '20 at 9:22
4
\$\begingroup\$

I agree it doesn't make sense to use promises with event-based programming and using observables is a good solution.

On another note, it is recommended to default to using const instead of let for all variables as it can cause bugs. When you determine re-assignment is necessary (mostly for loop/iterator variables) then use let.

Another suggestion is to use a linter - e.g. ESLint, JSLint, etc. For example: lines like this would be caught for the ESLint rule key-spacing:

new CustomEvent('serviceChosen', { detail:service })
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.