Responding to filter checkboxes in a view (SOLVED)

Currently, I am working on a view containing product list and I have some trouble with filtering this list.

My view has a fetch method, which should be called to fetch the filtered product list from my backend API. I first created a general search field that calls the fetch method in combination with debounce in a keyup event delegate: <input type="text" id="search" value.bind="search" keyup.delegate="fetch() & debounce">. The viewmodel’s search variable is marked as @observable and the searchChanged callback function updates the view’s URL in the browser as well. This all works splendid.

So far so good. Next, I created some additional filter options (checkboxes and radios) and that’s where the trouble starts. I want the product list to update immediately when a filter option is changed, but I seem to get stuck in tricky data binding behavior in combination with event delegates.

I tried to isolate the tricky behavior in a small app:

app.html

<template>
  <h3>Filters</h3>
  <div>
    <input type="checkbox" id="option1" checked.bind="option1" click.delegate="fetch()">
    <label for="option1">Filter option 1</label>
  </div>
  <div>
    <input type="checkbox" id="option2" checked.bind="option2" click.delegate="fetch()">
    <label for="option2">Filter option 2</label>
  </div>

  <h3>Results</h3>
  <div>
    <span>${result}</span>
  </div>
</template>

app.ts

export class App {
  option1 = false;
  option2 = false;
  result = 'No results';

  async fetch() {
    if (this.option1) {
      if (this.option2) {
        this.result = await Promise.resolve('Showing results for both filter options set');
      }
      else {
        this.result = await Promise.resolve('Showing results for only filter option 1 set');
      }
    }
    else if (this.option2) {
      this.result = await Promise.resolve('Showing results for only filter option 2 set');
    }
    else {
      this.result = await Promise.resolve('Showing results for no filter options set');
    }
  }
}

After checking/unchecking options, the result message is incorrect. It seems that the result message always reflects the previous state, not the current state. So it seems that the data binding updates take place after handling the click event delegate. Inside the fetch method, the this.option1 and this.option2 variables still hold the old values.

Note that that seems to be in contrast with the vanilla HTML/Javascript behavior: the actual checkbox input elements themselves already have the updated state when the fetch() method starts executing. I verified this using a ref binding and checking the element state inside the fetch() method.

I imagine there are several solutions to fix this behavior. I already recognized the following possible solutions:

  1. Removing the click delegates from the HTML markup, marking the option1 and option2 backing variables as @observable and calling the fetch() method from the option1Changed and option2Changed callback functions.
  2. Adding the debounce binding behavior with a delay of 0 ms to the binding expression in HTML: click.delegate="fetch() & debounce:0".

Are these the only straightforward solutions or are there any better and/or more elegant solutions available? Regarding the above solutions, the first one seems to be acceptable, but I consider the second one to be pretty ‘hacky’: it abuses the debounce binding behavior to force a setTimeout call (or something alike) so that the data binding mechanism gets a chance to execute before fetch() is called.

I tend to settle with the first solution. Do you agree? Or are there any other solutions that would be recommended in a scenario like this?

1 Like

@bigopon . . . This might perhaps be a very interesting case for using the @watch decorator instead of the @observable decorator, right? It would result in just a single decorated method instead of many callback methods for each observed backing variable. :slight_smile: Reason for me to switch to v2 as soon as possible. :wink:

1 Like

Reading your post, I immediately thought I would implement exactly what you outlined in option 1), if that’s any help! Agree that (from my limited read) @watch would be a great fit for something like this.

The other thing to consider is from a UX point of view, sometimes it can be better for a user to be able to select multiple options, then hit submit. Obviously not applicable everywhere, and I know that’s not what your asking - I just know in the past I’ve done this a had to change it because of the refresh of data every time. Works great in a lot of scenarios though!

1 Like

I think click.delegate is the problem. I would get rid of both of them. Then I would make option1 and option2 observable and add option1Changed and optio2Changed methods both of which call your fetch method.

1 Like

@dnkm and @rhassler, thank you very much for your quick replies. I agree with both of you and I went for option 1 indeed. :slight_smile:

I also refactored the free text search box implementation so that it matches the other filter fields. In the HTML markup, I now use <input type="text" id="search" value.bind="search & debounce"> and I call the fetch() method within the searchChanged callback function.

@dnkm, I initially thought that a separate submit button wouldn’t be such a bad idea indeed. But the application under development will live in my company’s internal network, so the user experience will probably be fine due to relatively high network and server performance and the limited number of concurrent users. However, if it turns out that an explicit submit button would indeed improve user experience, the implementation that I currently have is simple and flexible enough to easily introduce such a button at any time in the future. But thanks for pointing me to this option again.

2 Likes

Good job - nice work! :+1:

1 Like

So how would you go about this if your checkbox was part of a table row, i.e. you got more than one checkbox so a backing variable might not be a viable solution. In my case the checkbox is bound to a property on the instances bound to the rows. Just as @Bart I also need to call a function using click.delegate. However, as he observed the click-handler interferes with checked.bind so that the checkbox never gets its value set and the bound property is not updated either. Using & debounce:0 works, but it seems like a (ugly) hack.

Suggestions anyone?

ps: Aurelia v.1

1 Like

Hi @norgie,

In such a case, I would consider to add an additional button like @dnkm proposed instead of handling each checkbox’s click event separately.

Or - if I really need such behavior - I would try to wrap a checkbox in a custom element, expose a @bindable boolean backing variable (probably called checked) inside that custom element and bind that variable internally to the wrapped checkbox’s checked attribute. The custom element should correctly handle click events for the checkbox by blocking the original event and letting the backing variable’s observer method dispatch a “custom” click event instead. With such a custom element in place, I could then use it in my table’s repeated rows. I guess it should work properly that way.

Normally I would try it out myself in a small proof of concept, but sadly I have no time for that right now.

2 Likes