3

I've made a mistake. I paired my functionality to .on('click', ...) events. My system installs certain items and each item is categorized. Currently, my categories are [post, image, widgets], each having its own process and they are represented on the front-end as a list. Here's how it looks:

enter image description here

Each one of these, as I said, is paired to a click event. When the user clicks Install a nice loader appears, the <li> itself has stylish changes and so on.

I also happen to have a button which should allow the user to install all the items:

enter image description here

That's neat. Except...there is absolutely no way to do this without emulating user clicks. That's fine, but then, how can I wait for each item to complete (or not) before proceeding with the next?

How can I signal to the outside world that the install process is done?

It feels that if I use new CustomEvent, this will start to become hard to understand.

Here's some code of what I'm trying to achieve:

const installComponent = () => {

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve();
    }, 1500);
  });
};

$('.item').on('click', (event) => {
  installComponent().then(() => {
    console.log('Done with item!');
  });
});

$('#install-all').on('click', (event) => {
  const items = $('.item');
  items.each((index, element) => {
    element.click();
  });
});
ul,
ol {
  list-style: none;
  padding: 0;
  margin: 0;
}

.items {
  display: flex;
  flex-direction: column;
  width: 360px;
}

.item {
  display: flex;
  justify-content: space-between;
  width: 100%;
  padding: 12px 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  margin: 0;
}

.item h3 {
  width: 80%;
}

.install-component {
  width: 20%;
}

#install-all {
  width: 360px;
  height: 48px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul class="items">
  <li class="item" data-component-name="widgets">
    <h3>Widgets</h3>
    <button class="install-component">Install </button>
  </li>
  <li class="item" data-component-name="post">
    <h3>Posts</h3>
    <button class="install-component">Install </button>
  </li>
  <li class="item" data-component-name="images">
    <h3>Images</h3>
    <button class="install-component">Install </button>
  </li>
</ul>
<button id="install-all">Install All</button>

As you can see, all clicks are launched at the same time. There's no way to wait for whatever a click triggered to finish.

  • It's hard to know without seeing you code but you could look into callback functions. – Word Rearranger Nov 17 '19 at 04:17
  • @Edit I can post some code, but frankly, this is an extremely common issue, I thought adding code would not serve any purposes. I have a `li`, in each `li` I have a `button` which I click. My goal is to, by code, click each of these buttons BUT not before the one before finished. – Daniel Smith Nov 17 '19 at 04:20
  • @Edit Added code. – Daniel Smith Nov 17 '19 at 04:32
  • 1
    "*Except...there is absolutely no way to do this without emulating user clicks.*" - stop there and reconsider. In the example code you've shown, you could just as easily call `installComponent()` from the install-all event handler as you could call it from the install-one event handler. – Bergi Nov 17 '19 at 04:36
  • This SO question might help: https://stackoverflow.com/q/21518381/7919626 – Word Rearranger Nov 17 '19 at 04:39
  • @Bergi Yes, but I meant *in the way I wanted*, that is, to wait for each event to give me something back before launching the next. – Daniel Smith Nov 17 '19 at 04:41
  • @Edit I mean, that's clearly off the topic because my question is not whether Bergi will tell me I should avoid the Promise constructor anti-pattern (:P) or how to wait for functions in a simple way. My question is about coupling DOM events with functions and resolving that event with the function's return. – Daniel Smith Nov 17 '19 at 04:42
  • 1
    @DanielSmith I don't think event handlers can `return` anything. A hack would be to have them store their results in a variable that you access afterwards. But really, the proper way is to just call the same underlying controller function from multiple event handlers, not to trigger click events. – Bergi Nov 17 '19 at 04:49
  • You absolutely should not need to emulate clicks. Decouple the async operations from the event handlers, and then have the "install all" invoke each of those async operations in turn. – Alnitak Nov 17 '19 at 04:58
  • @Alnitak I understand. It means I need to marry my UI with functionality in a really, really bad way, though, which means my functionality is heavily dependent on the UI / markup tiself and can't be easily decoupled. – Daniel Smith Nov 17 '19 at 05:48

3 Answers3

1

This is simple architectural problems with your application that can be solved by looking into a pattern that falls into MVC, Flux, etc.

I recommend flux a lot because it’s easy to understand and you can solve your issues by separating out your events and UI via a store and Actions.

In this case you would fire an action when clicking any of these buttons. The action could immediately update your store to set the UI into a loading state that disables clicking anything else and show the loader. The action would then process the loader which can be monitored with promises and upon completion the action would finalize by setting the loading state in the store to false and the UI can resolve to being normal again. The cool thing about the proper separation is the actions would be simple JS methods you can invoke to cause all elements to install if you so desire. Essentially, decoupling things now will make your life easier for all things.

This can sound very complicated and verbose for something as simple as click load wait finish but that’s what react, angular, flux, redux, mobx, etc are all trying to solve for you.

In this case I highly recommend examining React and Mobx with modern ECMaScript async/await to quickly make this issue and future design decisions much easier.

Diniden
  • 1,005
  • 6
  • 14
  • Trust me, I know and I hate my existence every day, **but I can't.** What can I do to patch this in the mean time? I have separated my endpoint functions, this...I did very well. My UI is nowhere connected to my back-end processes. I have ~3500 LoC of UI code that I can always replace, for the time being, I'm stuck here. – Daniel Smith Nov 17 '19 at 04:39
  • Bergi has the best patch solution. The methods for the events are essentially methods you can call in the JS without emulating click events. If you MUST emulate a click event just use jquery to query for and el.click() to emulate the click on each item. – Diniden Nov 17 '19 at 04:42
  • Yes, but, again, as I've shown in my example **the problem is that I can't for each one to finish before firing the next.** – Daniel Smith Nov 17 '19 at 04:44
  • Just make a variable scoped outside of the method and assign the installCompknent promise to the variable. Then both methods can wait for that promise to resolve before moving onto the next. Just know it may produce some gotchas you have to be careful with. – Diniden Nov 17 '19 at 04:48
  • 1
    Another option is to make a processing queue where the method adds a command to the queue instead of processing immediately. The processing queue can then require each command one at a time in the order injected. This may fail if the method being called requires the user action context to be valid. – Diniden Nov 17 '19 at 04:52
0

What you should do is to declare a variable which will store the installation if it's in progress. And it will be checked when you are trying to install before one installation is complete.

var inProgress = false;
    const installComponent = () => {
      inProgress = true;
      return new Promise((resolve, reject) => {
        if(inProgress) return;
        else{
          setTimeout(() => {
            inProgress = false;
            return resolve();
          }, 1500);
        }
      });
    };
Mahbub Moon
  • 491
  • 2
  • 8
  • The `return` in `return resolve()` is useless. You _cannot_ `return` anything from an async callback function. – Alnitak Nov 17 '19 at 04:56
0

I'd be looking to implement something like this:

let $items = $('.items .item');
let promises = new Array($items.length);

// trigger installation of the i'th component, remembering the state of that
function startInstallOnce(i) {
    if (!promises[i]) {
        let component = $items.get(i).data('component-name');
        promises[i] = installComponent(component);
    }
    return promises[i];
}

// used when a single item is clicked
$items.on('click', function(ev) {
    let i = $(this).index();
    startInstallOnce(i);
});

// install all (remaining) components in turn
$('#install-all').on('click', function(ev) {
    (function loop(i) {                      // async pseudo-recursive loop
       if (i === components.length) return;  // all done
       startInstallOnce(i).then(() => loop(i + 1));
    })(0);
});
Alnitak
  • 334,560
  • 70
  • 407
  • 495