Home Page

Dated May 5, 2024; last modified on Sun, 05 May 2024

When a user lands at /home, this UI is shown. A couple of
components are shareable from /browse, e.g., search-bar,
search-results.

When a user lands at /home, this UI is shown. A couple of components are shareable from /browse, e.g., search-bar, search-results.

Sharing Code with /browse

Components initially created for the /browse page are useful in /home as well.

The CardsViewingPage Interface

This functionality can be shared between the two pages:

export class CardsViewingPage extends LitElement {
  @provide({ context: searchResultsContext })
  @state() protected searchResults: CardSearchResult[] = [];
  @state() protected selectedResult: Card | null = null;

  @provide({ context: cardsCarouselContext })
  @state() protected cardsCarousel = new CardsCarousel([]);

  protected cardFetcher: CardFetchEndpoint;

  constructor(cardFetcher: CardFetchEndpoint) {
    super();
    this.cardFetcher = cardFetcher;
    this.addEventListeners();
  }

  render() {
    throw new Error('CardsViewingPage must be subclassed and implement render()');
  }

  // Add event listeners, e.g., search-results, search-result-selected,
  private addEventListeners() {...}

  // Call `this.cardFetcher` and set `this.selectedResult`.
  private updateSelectedCard(cardID: string) {...}

  static styles = css`
    :host {
      display: flex;
      flex-direction: column;
      gap: 10px;
    }
  `;
}

@customElement('home-page')
export class HomePage extends CardsViewingPage {
  constructor() {
    super(trpc.fetchCard.query);
  }
  render() {...}
}

@customElement('browse-page')
export class BrowsePage extends CardsViewingPage {
  constructor() {
    super(trpc.fetchPublicCard.query);
  }
  render() {...}
}

Customizing the <search-bar> Component

The difference between the <search-bar> rendered by /browse and the one rendered by /home is the endpoint used by SearchBar.fetchResults (either trpc.searchPublicCards.query or trpc.searchCards.query). We currently pass a boolean to distinguish, but is it possible to pass the endpoint itself so as to be “closer to the metal”? While this code type-checks:

export type CardSearchEndpoint = typeof trpc.searchCards.query | typeof trpc.searchPublicCards.query;

@customElement('search-bar')
export class SearchBar extends LitElement {
  searchEndpoint: CardSearchEndpoint | null = null;
  // ...
}

@customElement('browse-page')
export class BrowsePage extends CardsViewingPage {
  // ...
  render() {
    return html`
      <search-bar .searchEndpoint=${trpc.searchPublicCards.query}>
      </search-bar>
      ...
    `;
  }
}

… it fails at runtime with Uncaught (in promise) TypeError: nextDirectiveConstructor is not a constructor after a Static values 'literal' or 'unsafeStatic' cannot be used as values to non-static templates. Please use the static 'html' tag function. See https://lit.dev/docs/templates/expressions/#static-expressions warning. It’s possible to pass functions via data attributes , so that’s not what’s happening here. The use case does not match the one described in :

class MyButton extends LitElement {
  tag = literal`button`;

  render() {
    const activeAttribute = getActiveAttribute(); // Must be trusted!
    return html`
      <${this.tag} ${unsafeStatic(activeAttribute)}=${this.active}>
      </${this.tag}>
    `;
  }

Using .searchEndpoint=${(q: CardSearchQuery) => trpc.searchPublicCards.query(q)} works though. Huh, I hope this doesn’t bite back in the future.

References

  1. Expressions – Lit > Static expressions. lit.dev . Accessed May 5, 2024.