0

Does there exist a good binding implementation for use with Promises? Everything I try seems to end up giving me an error about being unable to apply bindings multiple times to the same element (which I understand, and am trying to find a way around). I found this, but it's rather old and only seems to work with bindings that don't control descendant bindings for the same reason.

I have also tried writing my own implementation that attempted to remove children, and only re-attach/bind them after the promise resolves, but got the same result.

As an alternative, it's possible that I create an async computed observable to bind against, but then typing (I'm using TypeScript) becomes a little murky, since I would be returning a promise, but the value read from the observable would be something else. I could just type it as (effectively) "Promise | T", but that might be confusing, as it will only ever actually be "T".

Edit: This is the typing problem I'm talking about. Keep in mind that I'm using a method to hide observables behind getters/setters so that my properties look like regular javascript properties. My thought was to use a decorator to sorta turn a getter that returns a promise into a getter that returns the value

export class Foo {
  @promise get bar(): int {
    return new Promise<int>((resolve, reject) => {
      setTimeout(() => { resolve(1) }, 100);
    });
  }
}

Doing this would look alright, except that TypeScript is going to complain about the return type. I could cast it to any, but that's just not right. Alternatively, I could cast the getter return value to Promise | T, but that misrepresents the actual return type, because it would always be T.

In the way of the binding, I'd prefer not to rebind everything, but it appears, in cases like the "if" binding (which is actually what I'm trying to use), there's not really a way around it.

Edit 2: just in case, here's my current "promise" binding handler incarnation:

import * as ko from "knockout";

ko.bindingHandlers["promise"] = {
  init(element: HTMLElement, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    let first = true;

    function apply(bindingName: string, val: any) {
      ko.applyBindingsToNode(element, { [bindingName]: val }, bindingContext);
    }
    ko.computed(() => {
      let bindings = ko.unwrap(valueAccessor());

      if(bindings) {
        ko.tasks.schedule(() => {
          for(let bindingName in bindings) {
            let promise = bindings[bindingName] as Promise<any>;

            if(promise && promise.then) {
              promise.then(val => {
                apply(bindingName, val);
                first = false;
              });
            } else {
              apply(bindingName, bindings[bindingName]);
              first = false;
            }
          }
        });
      }

    }, null, { disposeWhenNodeIsRemoved: element })();

    return {
      controlsDescendantBindings: false
    };
  }
}


ko.virtualElements.allowedBindings["promise"] = true;

I've tried setting "controlsDescendantBindings" to false, which works for some things, but seems to cause a bit of havoc when promise bindings are nested.

Edit 3: As for what I'm trying to do, it's something along the lines of this (note: using Bootstrap).

<ul class="nav navbar-nav">
  <!-- ko promise: { if: canAccessFooBar } -->
  <li class="dropdown">
    <a class="dropdown-toggle" data-toggle="dropdown">Foobar</a>
    <ul class="dropdown-menu>
      <!-- ko promise: { if: canAccessFoo } -->
      <li>
        <a href="/foo">Foo</a>
      </li>
      <!-- /ko -->
      <!-- ko promise: { if: canAccessBar } -->
      <li>
        <a href="/bar">Bar</a>
      </li>
      <!-- /ko -->
    </ul>
  </li>
  <!-- /ko -->
</ul>

Where canAccessFoo, canAccessBar, and canAccessFooBar are promises that resolve to a boolean value.

Ixonal
  • 616
  • 8
  • 18
  • Why are you applying bindings more than once? You can update existing observables asynchronously as long as you create them up front and don't replace them with new instances. – Jason Spake Apr 26 '17 at 20:58
  • updating the observables after the fact is one option I listed, but that brought up the typing issue with possible confusion for people coming in later. – Ixonal Apr 26 '17 at 21:00
  • What are you trying to accomplish? – Roy J Apr 26 '17 at 21:05
  • Using an observable to hold your viewmodel is okay. Check out http://stackoverflow.com/a/27453338/1287183 or http://stackoverflow.com/a/12316626/1287183 – Michael Best Apr 26 '17 at 21:15
  • @MichaelBest That's not really what I'm trying to do in this case (and really, I'm already doing effectively that elsewhere). – Ixonal Apr 27 '17 at 13:23
  • @RoyJ I really want to bind against the result of a promise in a "good" way. Right now, I have a binding that attempts to set a particular binding when a particular promise resolves. – Ixonal Apr 27 '17 at 13:23
  • 1
    I'm not really understanding the typing issue you mention. Why does it have to return a promise instead of simply updating the strongly typed observables? Can you post some example code? – Jason Spake Apr 27 '17 at 13:44
  • @JasonSpake That has more to do with some other bits and pieces of things. I could use functions that generate the promises and update properties on resolution, but that feels a little off. What I was thinking would be to use a decorator to take a getter and effectively do that (just covered up), but the getter would return a Promise, but the property would be listed as the value type of the Promise – Ixonal Apr 27 '17 at 14:01
  • What you are trying to accomplish is not described by "bind against the result of a promise". That is *how* you hope to accomplish something. *What* are you trying to accomplish? Simplify from your real-world case as much as possible, preferably with an [example](https://stackoverflow.com/help/mcve) – Roy J Apr 27 '17 at 14:22
  • @RoyJ I'm adding/removing things based on a user's permissions. Realistically, that data should be there immediately, but it's not 100% deterministic, so I have it returning as a promise. – Ixonal Apr 27 '17 at 14:26
  • That makes a lot more sense now. I tried your current binding out and it seems to work in the simple test case I used. What kind of issues did you run into with the nested promise bindings? – Jason Spake Apr 27 '17 at 19:03
  • @JasonSpake As far as I can tell, the problem has to do with whether or not the promised binding(s) control their descendent bindings. Though, I just tried a version of the binding that would take that into account with no luck. As I change that setting, some promised bindings work, while others fail – Ixonal Apr 27 '17 at 19:24

1 Answers1

0

I think I found a solution. The key is the return value of "ko.applyBindingsToNode", which is an object with a single property, "shouldBindDescendants". the applyBindingsToNode call's return value is listed as "any" in the typescript declarations, so I had no idea what was in it until I got curious and logged it. At any rate, here is the current incarnation of the binding handler that (at least so far as I'm writing this) works.

import * as ko from "knockout";

ko.bindingHandlers["promise"] = {
  init(element: HTMLElement, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    let bindings = ko.unwrap(valueAccessor());

    function apply(bindingName: string, val: any) {
      let result = ko.applyBindingsToNode(element, { [bindingName]: val }, bindingContext);
      if(result.shouldBindDescendants) ko.applyBindingsToDescendants(bindingContext, element);
    }

    if(bindings) {
      for(let bindingName in bindings) {
        let promise = bindings[bindingName] as Promise<any>;

        if(promise && promise.then) {
          promise.then(val => {
            apply(bindingName, val);
          });
        } else {
          apply(bindingName, bindings[bindingName]);
        }
      }
    }

    return {
      controlsDescendantBindings: true
    }
  }
}


ko.virtualElements.allowedBindings["promise"] = true;
Ixonal
  • 616
  • 8
  • 18