0

I'm trying to create an HTML form that updates some of its values based on what is selected in a dropdown. The view model looks something like this:

function RoomViewModel() {
    var self = this;

    self.companyOptions = @Html.Raw({ ... });
    self.companyValue = ko.observable().extend({ rateLimit: 5000 });
    self.companyInfo = ko.observable();
    ko.computed(function() {
        if (self.companyValue()) {
            $.getJSON('@Html.GenerateActionLinkUrl("GetCompanyAndPlans")', {}, self.companyInfo);
        }
    });
}

ko.options.deferUpdates = true;
ko.applyBindings(new RoomViewModel());

I then bind my select dropdown to companyValue, and if I change the selection a bunch of times, only after 5 seconds does the computed kick in and display the currently-selected value. This comes close to doing what I want, but the one problem is that the first time you change the dropdown, you shouldn't have to wait 5 seconds - it should make the JSON call immediately. The rate limiting is to stop further JSON requests in between the first change and 5 seconds later. So how can I make it immediately do the JSON request and update for the first change?

Jez
  • 27,951
  • 32
  • 136
  • 233
  • Here is a great article about Knockout's observable rate limiting: http://knockoutjs.com/documentation/rateLimit-observable.html ~ Hope this helps. – Nerdi.org Jul 15 '18 at 19:29

2 Answers2

0
var ratelim = 0; // global rate limit 

function RoomViewModel() {
    var self = this;

    self.companyOptions = @Html.Raw({ ... });

    if(ratelim == 0){
      self.companyValue = ko.observable().extend({ rateLimit: ratelim }); // trigger the change without a delay 
      ratelim = 5000; // update the ratelim so the next request has a 5 second delay 
    } else { // ratelimit is not 0 (not first request), go ahead with regular rate-limited change: 
      self.companyValue = ko.observable().extend({ rateLimit: ratelim }); // extend the observable, with current ratelim
    }

    self.companyInfo = ko.observable();
    ko.computed(function() {
        if (self.companyValue()) {
            $.getJSON('@Html.GenerateActionLinkUrl("GetCompanyAndPlans")', {}, self.companyInfo);
        }
    });
}

ko.options.deferUpdates = true;
ko.applyBindings(new RoomViewModel());

I believe this should do the trick. I used a global variable (ratelim) to allow your function to detect whether it's the first request or not... Really you should be changing a variable value from true/false for whether a request is ongoing or not, so that there is 0 rate limit if the user has been 'idle' for a bit. Aka, if the 2nd request happens 20 seconds after the first, there's no need to still have a 5 second delay.

Nerdi.org
  • 895
  • 6
  • 13
  • Huh??? How will this work? It will just run once, during the construction of the new RoomViewModel. – Jez Jul 15 '18 at 20:24
  • Well, if you're only running it once then you would just need to update the ratelimit after the first time instead. {rateLimit: 0} should be default, and then you would need to update that to 5000 once it's been done once. – Nerdi.org Jul 15 '18 at 20:44
0

Interesting problem. I started playing with it and the conclusion I reached was that you need a custom extender for this. I found one which simulates the rateLimit and with some changes it does seem to address your need.

With this you should be able to do:

self.companyValue = ko.observable().extend({ customRateLimit: 5000 });

And have the initial change be instant and any subsequent be rate limited.

Here is the fiddle

Here is the runnable code snippet:

ko.extenders.customRateLimit = function(target, timeout) {
  var writeTimeoutInstance = null;
  var currentValue = target();
  var updateValueAgain = false;
  var interceptor;
  var isFirstTime = true

  if (ko.isComputed(target) && !ko.isWriteableObservable(target)) {
    interceptor = ko.observable().extend({
      customRateLimit: timeout
    });
    target.subscribe(interceptor);
    return interceptor;
  }

  return ko.dependentObservable({
    read: target,
    write: function(value) {
      var updateValue = function(value) {
        if (isFirstTime) {
          target(value);
          isFirstTime = false;
        } else {
          if (!writeTimeoutInstance) {
            writeTimeoutInstance = setTimeout(function() {
              writeTimeoutInstance = null;
              if (updateValueAgain) {
                updateValueAgain = false;
                updateValue(currentValue);
              }
              target(value);
            }, timeout);
          }
        }
      }
      currentValue = value;
      if (!writeTimeoutInstance)
        updateValue(currentValue);
      else
        updateValueAgain = true;
    }
  });
}

function AppViewModel() {
  this.text = ko.observable().extend({
    customRateLimit: 1000
  });
  this.rateLimited = ko.computed(this.text).extend({
    customRateLimit: 1000
  });
}

ko.applyBindings(new AppViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-debug.js"></script>

<h4>Change value is default so move the focus out of the input to change values.</h4>

<div>
  Enter Text: <input type='text' data-bind='value: text' />
</div>
<div>
  Rete Limited <small>(after the first change)</small>: <input type='text' data-bind='value: text' />
</div>
<div>
  Rete Limited Computed <small>(after the first change)</small>: <input type='text' data-bind='value: rateLimited' />
</div>

Notice after you enter text in the first text box how the change is immediate in the other ones. However any change after the first one is delayed

With this you can extend observables and computed observables.

Community
  • 1
  • 1
Akrion
  • 18,117
  • 1
  • 34
  • 54