Lit-html: Observables?

Created on 23 Sep 2017  路  12Comments  路  Source: Polymer/lit-html

First off, congratulations on the current progress of this project!

I'm coming from an Angular background, and am trying to better understand how new Polymer will work with lit-html compared to that.

Specifically, Angular 2+ came with this async pipe resolving Promises/Observables in the view. Fortunately lib-html has already clarified how it works with Promises, so currently I'm curious particularly how it might deal with Observables.

Angular itself used RxJS, although the Observable proposal appears about to advance to stage 2. In the Angular context this was relevant notably as a way to ensure trigger re-renders when (and only when) a value had changed, i.e. their ChangeDetectionStrategy OnPush. (In fact, their state management solution meant to facilitate that, ngrx, was inspired by redux-observable.)

How might Observables work over here?

Most helpful comment

Observable support should be easily added via directives.

I have some in-progress directives for async iterables, which might be easily adopted to work with observables (note: it's a shame there are so many stream APIs). I made them directives instead of built-in because there's likely to be some opinion on how they should behave. My first built-in support for async iterables incrementally appended new content, like a streaming version of iterables, but when moving to directives I created two: one that appends, the other that replaces.

I'll create a PR soon.

All 12 comments

Observable support should be easily added via directives.

I have some in-progress directives for async iterables, which might be easily adopted to work with observables (note: it's a shame there are so many stream APIs). I made them directives instead of built-in because there's likely to be some opinion on how they should behave. My first built-in support for async iterables incrementally appended new content, like a streaming version of iterables, but when moving to directives I created two: one that appends, the other that replaces.

I'll create a PR soon.

I just found a bit more info on Observables as used elsewhere here.

Since the async iterables support is now merged, and I think Observables can be converted to async iterables, I'm going to close this. Let's open a new issue if something specific comes up.

@justinfagnani but they can't. They are push based, and async iterables are pull based

They're both push based. A new value is pushed to an async iterator subscriber by resolving the current Promise.

For those who don't want to convert observables to async iterables, writing a directive to handle them it's quite straight-forward. I tried to simply take the asyncReplace directive and tweak it to make it handle observables and it seems to work great, check it out here.

EDIT:
Now also available as an npm package: lit-rx
But keep in mind that it's pretty much an experiment.

@Dabolus will have a look at your package, thanks for the work! I believe there is still a memory leak issue in asyncReplace so that won't be solved if you use that internally.

@yorrd what memory leak issue ? Can you please provide some more detals about it ? Is it still valid ?

By the way @Dabolus your package is currently in working version of https://github.com/rxdi/starter-client-lit-html :))) I decided to hard fork the code and export also little version of 'subscribe' as 'async' since it is more like the Angular sintax which i am familiar to.Regards for the good work with this package ! :))

@Stradivario it's still valid, observables are not unsubscribed when a part is unloaded so everything is kept in memory. There's another issue on the lit-html trackers where unmounting of parts is discussed. We have monkey patched lit-html in the meanwhile for our internal needs

@yorrd @Dabolus check this out
@yorrd thanks for confirming

Since i am working with Decorators and i can apply some things when component is mounted and unmounted i decided to implement logic which collects all observables onConnectedCallback from THIS(representing Component decorated with @customElement() decorator) then onDisconnectedCallback i am unsubscribing from all observables.

Define new Map() inside the component

cls.subscriptions = new Map();

When component is mounted you could do:

  cls.prototype.connectedCallback = function() {

    // Override subscribe method so we can set subscription to new Map() later when component is unmounted we can unsubscribe
    Object.keys(this).forEach(observable => {
      if (isObservable(this[observable])) {
        const original = this[observable].subscribe.bind(this[observable]);
        this[observable].subscribe = function(cb, err) {
          const subscribe = original(cb, err);
          cls.subscriptions.set(subscribe, subscribe);
          return subscribe;
        };
      }
    });
  }

Then when component is unmounted

  cls.prototype.disconnectedCallback = function() {
    // Disconnect from all observables when component is about to unmount
    cls.subscriptions.forEach(sub => sub.unsubscribe());
  };

Working version https://github.com/rxdi/lit-html/commit/f081b10f763710ed4b3183da44f43fdb9edbb623#diff-94a4d8acf1c6ed64d5392eaeec2daf3dR133
Regards!

@Stradivario this assumes that you have all observables as class fields, right? Not really an option for us, but a really cool approach if that's doable for you!

EDIT: not an option because we create observables inline and if we can't do that, a lot of our framework's fanciness goes away

@yorrd correct it works only for private members of the class and not for static ones and static outside class inheritance. It is a bit of a an ugly solution now since it is working with only one case but anyway it is good that it works!

I am thinking also that using static observables inside a template can give you an really unpredicted results and it is not easy testable.I really like this approach:

import { html, Component, async, LitElement } from '@rxdi/lit-html';
import { timer } from 'rxjs';
import { map } from 'rxjs/operators';

/**
 * @customElement about-component
 */
@Component({
  selector: 'about-component',
  template(this: AboutComponent) {
    return html`
      <header>
        <h1>About</h1>
      </header>
      ${async(this.timer)}
      <p>
        <img
          src="https://www.w3schools.com/html/pic_trulli.jpg"
          alt="Italian Trulli"
        />
      </p>
    `;
  }
})
export class AboutComponent extends LitElement {
  private timer = timer(1, 1000).pipe(map(v => v));
}

Since you can test it really easy

import 'jest';
import { Container, createTestBed } from '@rxdi/core';
import { Observable } from 'rxjs';
import { AboutComponent } from './app.component';

describe('Component', () => {
  let aboutComponent: AboutComponent;
  beforeAll(async () => {
    await createTestBed({
      components: [AboutComponent]
    }).toPromise();
   aboutComponent = Container.get(AboutComponent);
  });

  it('should be defined', done => {
    expect(aboutComponent).toBeTruthy();
    expect(
    done();
  });

  it('timer property should be observable', done => {
    expect(aboutComponent.timer).toBeInstanceOf(Observable);
    done();
  });
});

Regards!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AndyOGo picture AndyOGo  路  3Comments

dflorey picture dflorey  路  4Comments

pmkroeker picture pmkroeker  路  5Comments

Christian24 picture Christian24  路  4Comments

RyanAfrish7 picture RyanAfrish7  路  4Comments