Skip to content

Instantly share code, notes, and snippets.

@renoirb
Last active April 7, 2022 06:04
Show Gist options
  • Save renoirb/e8f48e4b54f16f9fb8f0bf38b6bc9c8d to your computer and use it in GitHub Desktop.
Save renoirb/e8f48e4b54f16f9fb8f0bf38b6bc9c8d to your computer and use it in GitHub Desktop.
Renoir's references to books and chapters

ECMAScript

Async/Await

We should not forget await

Quoting We should not forget the await (Exploring ECMASCript 2016-2017; 5.3.1 Don’t forget await)

[out value] is set to a Promise, which is usually not what you want in async functions. await can even make sense if an async function doesn’t return anything.

Source: Exploring ECMASCript 2016-2017, see-also (Returned Promises are not wrapped, Common Promise chaining mistakes, Tips for error handling)

Returning a promise from a promise, the engine will unpack and flatten it

Returning a promise from a promise, the engine will unpack and flatten it (Exploring ES6; 25. Promises for asynchronous programming)

Read more about async/await

Read more about setTimeout and clearTimeout

export const setTimeout: ReturnType<typeof setTimeout> = (
  // ^ Because dom.d.ts has a typing for setTimeout, and we're overloading
  // This might be creating issues
  callback,
  intervalMs,
) => getWindow().setTimeout(callback, intervalMs)

setTimeout exported directly might shadow the actual global scope window.setTimeout (but might not be too bad because we’re explicitly getting a Window object with getWindow()), also, the return signature of setTimeout is missing, because it returns a number, and that number can be used for window.clearTimeout

(TODO: Do more review, and drill a bit more in dom.d.ts in relation to setTimeout return type) and this StackOverflow answer

Let's refer to WHATWG HTML current living spec at *#Timers and its earlier W3C HTML WG counterpart (now deprecated to use WHATWG's)

Also, might be useful related links:

Private fields

See Gist WeakMap and class private fields transpiled for different runtimes


TypeScript

Enum’s caveats

Consider

// List of possible calendar Custom Event
// Notice it's not exported
const CALENDAR_EVENT_LIST = ['disconnect'] as const

export type CalendarCustomEventType = typeof CALENDAR_EVENT_LIST[number]

Comment:

setting an array as const doesn't prevent the array from being mutated apparently

source

True. "as const" is TypeScript specific. It's part of TypeScript’s design goals NOT to create more code (enum being one regretful occurence of the opposite).

But. notice it's not "export const CALENDAR_EVENT_LIST = []"

This is a data type that allows creating a type, and in this use-case, it's best not exporting it.

The minute we want a checker and leverage that data, we can do something like this.

export const isCalendarCustomEventType = (input: unknown): input is CalendarCustomEventType => {
  return CALENDAR_EVENT_LIST.includes(input as CalendarCustomEventType)
}

Which is a better way than using enums. Something that's known to be a regret by TypeScript's core team.

The point of as const here is so that TypeScript can trust that the list won't change so it can, at compile time, know that list won't change and then take the opportunity to make a type. Which is another way for what we do with tuple where we don't tell values, it uses indexes.

Also, in the argument const enum "doesn't prevent the array from being mutated apparently", that article is explaining about as const in the context of data collections. It's alwo refereted to as "tuples" or "record". That's coming up in TC39 (see use-case examples), where we basically want to say that this know length list of items etc. That's immutable, etc, so that the JavaScript parser/AST/etc can avoid dynamicaly shifting its handling in case things changed.

That is a different situation than here, where we are describing what applicable events that can happen in our package and exporting "manifests" of the possibilities and validation for it.

Related

More thourough example, try it out in TypeScript Playground

const DATA_MODEL_EVENTS = ['connect', 'disconnect', 'drink coffee'] as const

/**
 * Take runtime value, convert as type.
 */
export type ThisDataModelEventName1 = typeof DATA_MODEL_EVENTS[number]

/**
 * Create a map and a type.
 * 
 * Both are exported when compiled.
 * 
 * TypeScript's creators and core contributors confessed during TSConf 2020 that
 * this is among their regrets.
 * Because it is known to be confusing, and hard to use, and extend, when projects grows.
 */
export enum ThisDataModelEventName2 {
  'connect',
  'disconnect',
  'drink coffee',
}

/**
 * No runtime output
 */
export const enum ThisDataModelEventName3 {
  'connect',
  'disconnect',
  'drink coffee',
}

/**
 * Notice the check leverages the data, and helps ensuring for type validity.
 * And data store of types is not exported.
 */
export const isThisDataModelEvent1 = (input: unknown): input is ThisDataModelEventName1 => {
  return DATA_MODEL_EVENTS.includes(input as ThisDataModelEventName1)
}

/**
 * Notice how quickly it can grow
 */
export const isThisDataModelEvent2 = (input: unknown): input is ThisDataModelEventName2 => {
  switch (input) {
    case ThisDataModelEventName2.connect:
    case ThisDataModelEventName2.disconnect:
    case ThisDataModelEventName2['drink coffee']:
      return true;
    default:
      return false;
  }
}

export const isThisDataModelEvent3 = (input: unknown): input is ThisDataModelEventName3 => {
  switch (input) {
    case ThisDataModelEventName3.connect:
    case ThisDataModelEventName3.disconnect:
    case ThisDataModelEventName3['drink coffee']:
      return true;
    default:
      return false;
  }
}

Using read-only array of strings as source for type

Instead of doing

export interface IPerson {
  gender: 'm' | 'f' | 'nb';  // Hard-coded strings
}

We can do

const GENDERS = ['m', 'f', 'nb'] as const
export interface IPerson {
  gender: typeof GENDERS[number];
}

Using Enum and user-defined discriminator

On the same idea as above, one can use Enum and utility functions expanded from Enums, Gist: "TypeScript type Discriminator factory"

Create a type based on the property of another

We can create a type from picking a property from an existing nested one.

Say we want gender from IPerson, because tat type doesn't exist

const GENDERS = ['m', 'f', 'nb'] as const
export interface IPerson {
  likesChocolate: boolean;
  gender: typeof GENDERS[number]
}

In another file we can want the same property Without re-creating it, say we're OK that IEmployee has completely separate properties ... but we want the type from the gender property

import type { IPerson } from '../elsewhere'
export type IPersonGender = IPerson['gender'] // Boom!
export const IEmployee {
  gender: IPersonGender;
}

We're basically piggy-backing on TypeScript's Indexable Type, it's also described in an older issue about "bracket notation"

We can also reuse GENDERS for a validator too

const GENDERS = ['m', 'f', 'nb'] as const
export type IPersonGender = typeof GENDERS[number]
export interface IPerson {
  likesChocolate: boolean;
  gender: IPersonGender;
}

// But that function isn't telling what it does
export const isGender = g => new Set([...GENDERS]).has(g)

// If we want to make this more useful, so we can use it in if blocks or get type hints

export type IGenderValidator = (gender: string) => gender is IPersonGender
export const isValidGender = isGender as IGenderValidator

Those are This is an "User-Defined" assertion (next)

User-defined type assertion functions

A type guard is some expression that performs a runtime check that guarantees the type in some scope

Source User-defined type guards and TypeScript handbook about user-defined type guards

const LOGGER_MUST_HAVES = ['log', 'warn', 'info', 'error', 'debug'] as const

export type IAcceptableLogger = Pick<Console, typeof LOGGER_MUST_HAVES[number]>

export const isAcceptableLogger = (input: any): input is IAcceptableLogger => {
  // TYPING =>                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // FUNCTION DEFINITION =>        ^^^^^^^^^^                              ^^
  // The Typing is PART of the function definition
  const missingMethods = new Set([...LOGGER_MUST_HAVES].filter(x => !Object.keys(input).includes(x)))
  // Use set and includes ^
  // https://2ality.com/2015/01/es6-set-operations.html
  // MUST return a boolean
  return missingMethods.size === 0
}

Alternatively, you can use TypeScript 3.7’s Assertion Functions

import { strict as assert } from 'assert'
// Source: microsoft/TypeScript issue 34523
// https://github.com/microsoft/TypeScript/issues/34523#issuecomment-700491122
export const assertAcceptableLogger: (input: unknown) => asserts input is IAcceptableLogger = (input) => {
  // TYPING =>                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // FUNCTION DEFINITION =>                                                                   ^^^^^^^ ^^
  // The Typing is NOT PART of the function definition.
  // IN CONTRAST TO a "x is Foo", we should throw, read more:
  // https://github.com/microsoft/TypeScript/issues/34523#issuecomment-700491122
  const missingMethods = new Set([...LOGGER_MUST_HAVES].filter(x => !Object.keys(input).includes(x)))
  const m = missingMethods.join(', ')
  // Throw an exception instead.
  assert.equals(missingMethods.size, 0, `Invalid Logger, we are missing the following methods ${m}`)
  // MUST return undefined
}

Because the mechanics is a bit different from returning a boolean (it returns void), we can make it clear in the code that we're aren't sure it's there (hence unknown) and then add the assertion function.

export class SomeService {
  readonly #logger?: IAcceptableLogger
  get logger(): IAcceptableLogger {
    const logger: unknown = this.#logger
    assertAcceptableLogger(logger)
    // is guaranteed to throw ^
    // and logger here is *GUARANTEED* to be IAcceptableLogger
    return logger
  }
}

User-defined Assertion functions to ensure we have "window"

// This would be useful to MAKE SURE (i.e. throw if not) we have Window
export const assertsIsWindow: (w: unknown) => asserts w is WindowProxy = (w) => {
  // TYPING =>              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // FUNCTION DEFINITION =>                                               ^  ^^
  // The Typing is NOT PART of the function definition.
  // IN CONTRAST TO a "x is Foo", we should throw, read more:
  // https://github.com/microsoft/TypeScript/issues/34523#issuecomment-700491122
  let message = 'We cannot be certain this is a Window object'
  let out = false; // Is it OK? No, it's not, unless...
  try {
    if (w && typeof w === 'object' && 'document' in w) {
      if ((<WindowProxy>w).document && 'createDocumentFragment' in <WindowProxy>w) {
        // If we have document and createDocumentFragment, we should have a proper window object
        out = true // Unless we are certain
      }
    }
  } catch (e) {
    message += e
  }
  if (!out) {
    // If we're not OK, throw.
    throw new TypeError(message)
  }
  // IN CONTRAST TO a "x is Foo", we should throw
  // And return undefined
}

This can be useful alongside collection of functions that takes an input object from which we can extract "coerce" Window or Document out from.

Angular and other frameworks does this, not always hand-in-hand with TypeScript though.

One use-case for such "coercers" is to avoid pulling from global globalThis, or window, or global, or document, but rather encapsulate to the appropriate DOM Window object where the code runs. Because code written once, may run in more than one context, and assuming global can cause issues.

Vue.js does this for their runtime @vue/runtime-dom in Vue.js v3.0.5, where they encapsulate DOM manipulation using coercion, but from a class that has many utility methods, where we pass a known data shape, and it does DOM manipulation on the configured global window.

There's also:

Ensure we have a Document and coercing to a document

/**
 * Ensure we have a valid Document object before accessing it.
 *
 * @param node - any object that might be a valid Document
 */
export const assertsIsDocument: (node: unknown) => asserts node is Document = node => {
  let mustBeTrue = false
  let message = 'We could not confirm we received a Document object'
  try {
    coerceOwnerDocument(node ?? null)
    // Above must throw if is not a Document node
    mustBeTrue = true
  } catch (e) {
    message += ` ${e}`
  }
  if (!mustBeTrue) {
    throw new TypeError(message)
  }
  // Since it's a TypeScript user defined assertion function it either throws or return void
}

export const coerceOwnerDocument: (node: unknown) => Document = node => {
  let d: Document | undefined
  if (node && typeof node === 'object' && 'nodeType' in node && 'ownerDocument' in node && 'nodeName' in node) {
    const unkownElement: { ownerDocument?: Document } = node
    d = unkownElement.ownerDocument ?? void 0
    if (d && typeof d === 'object' && 'defaultView' in d && 'body' in d && 'nodeType' in d) {
      const { nodeType = 0, body } = d as Document
      if (nodeType === 9 && body.nodeType === 1) {
        return d
      }
    }
  }
  const message = 'We did not receive a valid DOM node'
  throw new TypeError(message)
}

export const coerceGlobalWindow: (node: unknown) => Window = node => {
  const d = coerceOwnerDocument(node)
  // Above must throw
  if ('defaultView' in d && d.defaultView) {
    return d.defaultView
  }
  const message = 'We could not get to this context’s root Window'
  throw new TypeError(message)
}

Comment syntax

There is a comment structure of API Extractor that is a "subset" of JSDoc because we now can leverage TypeScript's reflexion and make comments simpler. Why having a comment saying there's a parameter name "name" that is a string on a Person class, with a comment saying the same thing, those comments are redundant and subject to "rot".

So, instead, we can simplify the comments to tell things that matters:

  • Is this method public private or internal or deprecated?
  • Any remarks about that block or references to read about or link to another

Related, API-Extractor declaration reference


Architecture

Enforcing and validating runtime configuration

See Gist Convict example setup

Related Introducing Env a better way to read environment variables in JavaScript


DOM

Batching changes using Fragments

DOM manipulation is expensive, and we should avoid touching it too often, rather let the rendering scheduler handle when it feels is best.

The DocumentFragment interface represents a minimal document object that has no parent. It is used as a lightweight version of Document that stores a segment of a document structure comprised of nodes just like a standard document. The key difference is due to the fact that the document fragment isn't part of the active document tree structure. Changes made to the fragment don't affect the document (even on reflow) or incur any performance impact when changes are made.

Source developer.mozilla.org Web/API/DocumentFragment

Also, your code might run in wildly different contexts. Not only from Client Side, it might run from a Jest virtual dom (JSDOM, or in a service worker, or a different frame, or from a server-side javascript — with equivalent of JSDOM again).

So what's best is to have functions where we provide a Document (or Window) to use, so in test run, we can mock it, and in the Front-End, we can use inheritance to provide it from the component's inheritance tree.

All in all, don't go global, don't mutate the DOM, batch changes!

To do so, here's a contrived example.

  1. Have a script tag factory from which the HTMLScriptElement is returned
  2. Have a DocumentFragment provider from which we can do the manipulation

Have a look at Jake Archibald's talk about the Event loop between 1:47 and 14:00 where he explains well about paint steps and heavy tasks.

// Provide the document, guarantee an element. Don't mutate the document(!)
export const createUserReportExternalDependencyTag = (d: Document): HTMLScriptElement => {
  // Or any other DOM Node creation pattern, one function per pattern.
  const el = d.createElement('script')
  el.src = 'http://cdn.userreport.com/userreport.js'
  el.async = true
  // To avoid running this function and load more, 
  // we can use the window['userreport-launcher-script']'s existence to see if needed again
  el.id = 'userreport-launcher-script'
  return el
}

Or, better yet, provide a Window object, return a DocumentFragment

See assertsIsWindow function in User-defined Assertion functions to ensure we have "window" above.

export const createUserReportExternalDependencyTag = (w: WindowProxy): DocumentFragment => {
  // Anything after this is GUARANTEED to be OK
  assertsIsWindow(w); // See User-Defined Assertion function example above
  const el = createUserReportExternalDependencyTag(w.document)
  // Normally, we do more complex structures than one node on a fragment
  // so that we can batch big DOM changes in one shot.
  const fragment = w.document.createDocumentFragment()
  fragment.appendChild(el)
  return fragment
}

Then later on, use this, either from a JSDOM instance in Jest tests (see this link's examples)

Or at initialization time hook, we call and document.appendChild(fragment);

See also that other comment I've made about mocking the DOM (below)

Mocking the DOM during tests

In tests, please, __mocks__ the DOM, please!

Either mock things that doesn't exist. Which should make more sense, because _urq is window spoiled by userReport script. We could mock in manual mock the UserReport provider. That thing should be tested already.

But talking about that, what if the script is blocking and or broken, would it break our stuff too.

How about we provide our own tracking method, that then passes it (because we can listen to those events) and delegate them internally to that external tracker.

// file __mocks__/dom.ts
import { JSDOM } from 'jsdom'
const dom = new JSDOM()
global.document = dom.window.document
global.window = dom.window
// So that calling document, or window during tests will use that instance of JSDOM

See how to mock the DOM with Jest manual mocks.

Avoid top level function to global window and realm crossing

Please, let’s not do this;

// From a module file
export const getWindow = () => window
export const getDocument = () => document

This comment below should be a new section under User-defined Assertion functions to ensure we have "window"

Is a smell. We should instead have decomposed functions that we can bind at the proper place to be picked up by the DOM, without having to have such functions.

Having exported function that returns window like above is an anti-pattern.

Doing this goes against what I've been talking about, what I've been talking was to make the window as a function argument so when we call that function, we can enforce (even use assertion and strict type checks) from the call site where we know we'll have window.

I am instead recommending to write function where we can assert and use window without calling from global, for example have a look at the idea in a Vue.js project (notice that this block of code could be in any JavaScript project than in this Vue Single File Component and still work!).

The issue is that window and document are globals, and it's because of its history, and "do not break the Web".

I would rather do something like accessing window or window.document from a function that has a reference to it.

Event handlers can be written outside of a component tree, yet be imported in, from which we can get the right window or document instance. When I say "right" I mean explicitly the one from which it's been triggered.

When it comes to shadow DOM, or cross frame, or service worker, the window is not the same. Calling global might be on the wrong one.

// Attach this to an HTMLFormElement
export const handleFormSubmit =  (event: HTMLElementEventMap['submit']) => {
  // event is of type SubmitEvent
  if (event && event.view && event.view && event.view.document) {
      // event.view is the Window
      const d = evt.view.document
      //    ^ is Document!
  }
}

See HTMLFormElement DOM node attach an event handler (normally directly on that element) that has a SubmitEvent DOM Event, since that event inherits Event, we can pick target. EventTarget, where we can eventually drill into getting the window (called view, but IS a Window instance, exactly the one from which the event is emitted from)

Also, we'd leverage proper TypeScript event typing, for free

Not doing this is making us not having to pay this as a debt once we'll want to re-use code in service workers, or server-side rendering, etc.

Besides, when using ECMAScript Symbol, Symbol("foo") from one window"realm" will not be compared as the same in another window realm. (a symbol with the same name, in different window will not be the same).

This pattern is not the first time we talk about it (the web development industry, when exchanging with Web standard bodies) and frameworks. For example Angular has a nice way of registering keyboard shortcuts, besides the fact that we can't use that handler without importing Angular, that extracts hows a pattern where it makes reference to window from a function that is known to have access to window. That example is not doing what I'm talking about, but is done in a way not so different than what I'm talking here.

"crossing realms"

Service worker, or iframe, or another tab, has their own respective window instance ("realm").

When it comes to using Symbol, and anything that is assuming it WILL ALWAYS be in the EXACT same realm, errors occurs. Creating a problem we'll have to fix (a.k.a. debt).

In the case of Symbol, it's one of those things where it's the case.

A code realm (short: realm) is a context in which pieces of code exist. It includes global variables, loaded modules and more. Even though code exists “inside” exactly one realm, it may have access to code in other realms. The problem is that each realm has its own global variables where each variable Array points to a different object, even though they are all essentially the same object. Similarly, libraries and user code are loaded once per realm and each realm has a different version of the same object.

Objects are compared by identity, but booleans, numbers and strings are compared by value. Therefore, no matter in which realm a number 123 originated, it is indistinguishable from all other 123s. That is similar to the number literal 123 always producing the same value.

Source: Exploring ES6, chapter: 7. Symbols

Making functions leveraging known call contexts (such as DOM Event Handlers) with known members that already include the window and document globals, we should avoid calling window directly altogether.

Instead of doing .click()

Sometimes in tests we see many variants of the following spreaded around.

describe('...', () => {
  // ...
  beforeEach(() => {
    component = (await fixture(
      html`<app-unauthenticated-error
        .login=${() => loginActionSpy()}
        .support=${() => supportActionSpy()}
      ></app-unauthenticated-error>`,
    )) as AppUnauthenticatedError
  })
  // ...
  // Some many lines below
  const clickChameleonButton = (elementIndex: number) =>
    (component.shadowRoot.querySelectorAll('chameleon-button') as NodeListOf<ButtonComponent>)[elementIndex].click()
// !!  --------------------------------------------------------------------------------------------------->  ~~~~~~~
})

In this case, we want to test clicking on a CustomElement, and inside it, emit a DOM event..

One occurence of this, and re-used many times is fine.

But as soon as this much of code is copy-pasted around, we can improve this.

When you think of it, the DOM has methods and we can piggy-back on them, and remain type safe. More about this is described below in "Re-Usable Test DOM walking".

It's a bit verbose, and that pattern is sometimes copy-pasted more than a handful of times and adds to noise.

Instead of doing()[0].click(), let's make it safer, and avoid leaving behind typing errors such as Property 'click' does not exist on type 'Element'.

So, here are a few helpers that I've added as commit suggestions

/**
 * Function returning a function that we can call with the value at the scope it's called 
 * and avoid leaky references
 */
const createGetElement = (cssSelectorString: string) => (host: Element): Element[] => {
  const nodes: Element[] = Array.from(host.shadowRoot.querySelectorAll(cssSelectorString))
  return nodes
}

/**
 * Instead of prototype chain, act like the DOM
 *
 * https://noriste.github.io/reactjsday-2019-testing-course/book/jest-101/jsdom.html#interacting-with-the-dom
 */
const clickOn = (host: Element): boolean => {
  return host.dispatchEvent(new MouseEvent('click', { bubbles: true }))
}

/**
 * Have a "test subject" that returns the component.
 * Simplifies the `beforeEach`
 */
const createTestSubject = async (): Promise<AppAvatarPopoverMenuDnd> =>
  (await fixture(html`<app-avatar-popover-menu-dnd></app-avatar-popover-menu-dnd>`)) as AppAvatarPopoverMenuDnd

// ----
// Later-on, in a test suite
// ----

const getTestSubject = createGetElement('app-avatar-popover-menu-dnd')

describe('...', () => {
  let component: AppAvatarPopoverMenuDnd
  beforeEach(() => {
    component = createTestSubject()
  })
  test('...', () => {
    // .. later in test, say you want the second
    const [first,second] = getTestSubject(component)
    // .. anc click on it
    clickOn(second) // Boom!
  })
})

Re-Usable Test DOM walking

Sometimes in test we have number of helper utilities that aren't really flexible, and there's many of them.

describe('...', () => {
  // ...
  describe('when the scaffolding is setup', () => {
-   const getAppBanner = () => document.getElementById('app--banner-notification')
-   const getAppSnackbar = () => document.getElementById('app--snackbar-notification')
-   const getAppContent = () => document.getElementById('app--content')
+   const getAppBanner = () => document.getElementById(APP_BANNER_NOTIFICATION_ID)
+   const getAppSnackbar = () => document.getElementById(APP_SNACKBAR_NOTIFICATION_ID)
+   const getAppContent = () => document.getElementById(EXPERIENCE_CONTAINER_ID)

    // Later on, with many lines related or re-reproducing this idea
    
    describe('...', () => {
      it('...', () => {
        // One line like this many times
+       expect(getAppBanner()).toBeTruthy()
      })
    })
})

In that PR change, to make it flexible, it's been added lines with APP_ROOT_ID constants instead. In that case, it would be best to make that more configurable.

It's a bit verbose, and is not that flexible and that pattern is sometimes copy-pasted more than a handful of times and adds to noise.

We could instead have an utility to walk the DOM

+ const getNodeListFor = (cssSelectorString: string): Element[] => {
+   const nodes: Element[] = Array.from(document.querySelectorAll(cssSelectorString))
+   return nodes
+ }

describe('...', () => {
  // ...
  describe('when the scaffolding is setup', () => {
-   const getAppBanner = () => document.getElementById('app--banner-notification')
-   const getAppSnackbar = () => document.getElementById('app--snackbar-notification')
-   const getAppContent = () => document.getElementById('app--content')
    // Later on
    describe('...', () => {
      it('...', () => {
        // Instead take the first item of the received array and test it out!
+       const [ subject ] = getNodeListFor('app--banner-notification')
+       expect(subject).toBeTruthy()
      })
    })
  // ...
})

That would make the test suite have less code (some in example above was omited)

Expanding on the idea of writing complex logic to read the DOM and do actions, we could do something like the following;

So, we could have the same for HTML CustomElement

// In the case of plain DOM
export const getNodeListFor = (cssSelectorString: string): Element[] => {
  const nodes: Element[] = Array.from(document.querySelectorAll(cssSelectorString))
  //                                  ~~~~~~~~
  return nodes
}

// In the case of something inside a Shadow DOM
export type DocumentWithShadowRootNode = Pick<Element, 'shadowRoot'>
export const getCustomElementNodeListFor = (
  host: DocumentWithShadowRootNode,
  querySelectorString: string,
): Element[] =>
  Array.from(host.shadowRoot.querySelectorAll(querySelectorString))

// Then, to pick one nth node
export const pickNthNodeOf = <T extends Element = Element>(
  nth: number,
  nodeList: T[] = [],
): T | never => {
  if (nodeList && nth in nodeList) {
    return nodeList[nth] as T
  } else {
    throw new Error('oops I did it again')
  }
}

Then using would be like;

const btns = getCustomElementNodeListFor<ButtonComponent>(component, 'chameleon-button')
const btn = pickNthNodeOf(1, btns)
// What's nice here is that this ^ is almost a test in itself, it'd throw if it doesn't find
// PS: let's discuss how we could make this even less verbose.
btn.click();
// Or use DOM event trigger instead of method call. (Best choice!)
btn.dispatchEvent(new MouseEvent('click', { bubbles: true }))

CustomElement and content data dependencies with Context API

See Context API on github.com/ webcomponents/community-protocols

The context-request event and data model

export type Context<T> = {
  name: string;
  initialValue?: T;
};

interface ContextEvent<T extends Context<unknown>> extends Event {
  /**
   * The name of the context that is requested
   */
  readonly context: T;
  /**
   * A boolean indicating if the context should be provided more than once.
   */
  readonly multiple?: boolean;
  /**
   * A callback which a provider of this named callback should invoke.
   */
  readonly callback: ContextCallback<T>;
}

Creating context per component and factories for their initial state

export type UnknownContext = Context<unknown>;

export type ContextType<T extends UnknownContext> = T extends Context<infer Y>
  ? Y
  : never;

export function createContext<T>(name: string, initialValue?: T): Context<T> {
  return {
    name,
    initialValue,
  };
}

Using Context

Then, in a component

//
// Step 1, create a "context", basically it is a name and an expected
// set of values it contains.
// We can also tell what are the default or empty values for it.
//
 
/**
 * What is the shape of this context
 */
export type IPersonContext {
  firstName: string | ''
  lastName: string | ''
}

/**
 * Declare the default/fallback values or its state
 * This can be useful for knowing within the component
 * consuming this context if that is empty or filled.
 */
const PERSON_CONTEXT_DEFAULT: IPersonContext = {
  firstName: '',
  lastName: '',
}

/**
 * Give a name and pass the default state.
 *
 * Take this as if it is a symbol or a "foreign key" so we can
 * tell which "Context" we're talking about, and what are the default
 * and/or empty state values
 */
export const PersonContext = createContext('person', Object.freeze(PERSON_CONTEXT_DEFAULT))

// ... in another file

/**
 * This is a Person Context consumer say for displaying an avatar
 */
class PersonAvatar extends HTMLElement {
  // Those properties will be filled by the Context
  // We could use those to know if it's loading (use a "skeleton" loader pattern?)
  private firstName = ''
  private lastName = ''
 
  connectedCallback() {
    this.dispatchEvent(
      new ContextEvent(
        PersonContext,
        (value, dispose) => {
          // protect against changing providers
          if (dispose && dispose !== this.personDisposer) {
            this.personDisposer();
          }
          this.personDisposer = dispose;
          // Update the properties locally
          const {
            firstName = '',
            lastName = ',
          } = value
          this.firstName = firstName;
          this.lastName = lastName;
        },
        true // we want this event multiple times (if the properties changes changes)
      )
    );
  }
  disconnectedCallback() {
    if (this.personDisposer) {
      this.personDisposer();
    }
    this.personDisposer = undefined;
  }
}

Monitoring

Real User Monitoring

Collect the user's web browser Performance Timing, collect and use it for analysis.

Read more perf timing primer

One of the pioneer on the subject is Phillip Tellis the original author of Boomerang.

Boomerang

Boomerang is a JavaScript library for Real User Monitoring (commonly called RUM).

Boomerang measures the performance characteristics of real-world page loads and interactions.

The documentation on this page is for mPulse’s Boomerang.

source


LitHTML

Prefer stateless pure functions

  1. One can use a static property getter instead of @property is an alias do doing by hand like this link shows
  2. One can use a static style property for CSS, see
export interface HotkeyGroup {
  label: string
  keys: Hotkey[]
}

// https://github.com/lit/lit-element/blob/2b398727/src/test/lib/decorators_test.ts#L99
// see hasChanged, but below is done differently for this example here
const hasChangedSections = (current?: HotKeyGroup[], previous: HotKeyGroup[]) =>
  previous === undefined || current?.length !== previous?.length

const whatMakesHotKeyGroupUnique = (current: HotKeyGroup) => current.label

const createHotKeyItem = ({ label = '', keys = [] }: HotKeyGroup) => html`<li><app-hotkey-section
  .hotkeySectionLabel=${t(label)}
  .hotkeySectionList=${keys.slice()}
></app-hotkey-section></li>`

import style from './style.scss'

class Example extends LitElement {

  // BEGIN: static styles property for CSS
  static style = style
  // OR...
  static styles = [
    css`div {
      border: 2px solid blue;
    }`,
    css`span {
      display: block;
      border: 3px solid blue;
    }`
  ];
  // END: static styles property for CSS
  
  // BEGIN: static property getter
  @property({ hasChanged: hasChangedSections })
  sections: HotKeyGroup[] = [];
  // OR...
  static get properties() {
    return {
      sections: { hasChanged: hasChangedSections },
    };
  }
  // END: static property getter ...

  render() {
    return html`<div>
      <h1>Keyboard stuff things</h1>
      <ul>${repeat(this.sections, whatMakesHotKeyGroupUnique, createHotKeyItem}</ul>
    </div>`
  }
}

@renoirb
Copy link
Author

renoirb commented Nov 29, 2021

Leveraging Lit's Controller

IDEA, this is not a request for refactor. I'm just dropping notes here for future reference.

I have an idea, it works in my head, but I can't find a better way to illustrate but by showing bits of code I've seen that could be good pattern for this. I'll have to experiment more to give a better illustration of what I have in mind.

When I look at this file, it reminds me of how the lit team does in the upcoming @lit-labs/context ContextAPI, to leverage Lit's "ReactiveController" (interface), see usage of WeakMap in lit-labs/motion AnimateController

Mostly the following part at this part.

See the mechanics in a separate repo:

But I would have to experiment more with that idea.

@renoirb
Copy link
Author

renoirb commented Apr 6, 2022

HTTP Caching

There is a way to tell the server you previously received something from it, as a way to ask if you need to “download again”.

If a server sends “Last-Modified” in its response header, when you query the second time, use that value in a If-Modified-Since request header the next time.

The first time, it sends the payload. Whatever it is.

The second time, it can return 304 Not Modified and nothing. Then, the client, will just use what it received previously.

The browser natively does this.

For a JavaScript client-side HTTP client (Axios, fetch, ky, …), we have to keep track of the URL and the Last-Modified. When we make another call to a previously made URL, add the Last-Modified value we received and add it as a If-Modified-Since request header. Then the HTTP Client (i.e. fetch, internally) may pick that up too. (To be confirmed exactly how to setup)

Example

The example shows a file requested. Web servers do add those headers automatically with them. But a backend service can do that too.

First request

Image of an HTTP Request to an image
Notice the Last-Modified response header

2nd request

Image of another HTTP Request, but with If-Modified-Since

Notice it just sends 304 Not Modified, no data.

Server-Side

While for files, web servers do that automatically. For Backend generated response, its an exercise left to the reader.

It's basically having the server to keep track of “what makes a request unique”, and store it somewhere for a short period of time. There are software solutions that you can send data, and add an expiration date, and that entry gets removed after that time.

That server then while it sends response can track a few properties and store them at that place and add the Last-Modified when it got it. (PS: this is just the bare minimum, it works. But for a more complete, refer to Mark Notthingham’s Cache docs)

Say the backend is always called with an Authorization header. That its a Bearer 1111.2222.3333. HTTP Server treats requests with Cookie and Authorization header as private and don't cache. But on the server that manages the returning data already validated that, it can do that logic. In the case of a JWT token, all we can use is the last part of the 111.222.333. The signature. And a part of it. That way it's not filling up memory. And the signature is unique to the rest anyway. (TODO: Double check some more about that — but thus far wasn't said insecure in the context I'm proposing this)

The things we keep in memory temporarily can be:

  • The URL (Just make sure the URL is always the same, including the ?query part)
  • The Authorization header's (or Cookie) snippet of it (because in either case, most of that regularily change, but some part of it changes less frequently and uniquely identify the person)
  • The time it happened

So that when the server sees a request without If-Modified-Since (and all rest validated), store the above, and pass Last-Modified.

The complying client will be able to do the same, and pass If-Modified-Since when they request again, including passing their currently applicable Authorization headers anyway.

So the client can notify it saw. The server can say it's the same. Nothing to download. 304 Not Modified

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment