Skip to content

Plan for v4 release #203

@chriskrycho

Description

@chriskrycho

An outline of what I think needs to be done for a v4 release:

There are two big switches here:

  1. Switch away from having element and args set on the backing class, and aligning it with the way that class-based helpers work. This is intentional: modifiers are very similar to helpers in how they work—in a real sense, they are just a different kind of helper: one that receives an Element as an argument, and has different constraints about when and how it gets run. The point of the API changes here is to reflect that.

    This allows us to make the API surface of a class-based modifier much more minimal, and to then guide users to simply make use of (both tracked and untracked) state within the modifier class—just like they would with helpers or component backing classes!

  2. Stop consuming args eagerly. Instead, behave like autotracking does in general: only entangle what the end user actually uses.

There are also two open questions:

  • Is there any value to having both didReceiveArguments and didUpdateArguments? My own opinion is no (and the outline of the class below is updated accordingly)
  • Given that didInstall is called after didReceiveArguments, with the same arguments, is there any reason to maintain that hook, rather than just guiding users to do standard first-time-installation behavior (the same as we would otherwise)? My opinion here is also no, but I don't feel quite as strongly about that as I do about didReceiveArguments and didUpdateArguments, so I’ve left it there for now.

The new signature interface (which can be straightforwardly expanded into an Invokable signature per the Component signature RFC):

interface ModifierSignature {
  Args?: {
    Named?: {
      [argName: string]: unknown;
    };
    Positional: unknown[];
  };
  // defaults to `Element` if not supplied
  Element?: Element;
}

The updated class-based modifier signature (assuming some type helpers which we will make available):

export default class ClassBasedModifier<S> {
  constructor(
    owner: unknown,
    args: {
      positional: PositionalArgs<S>;
      named: NamedArgs<S>;
    }
  );

  modify(
    element: ElementFor<S>,
    positional: PositionalArgs<S>,
    named: NamedArgs<S>
  ): void;
}

The updated function-based modifier signature:

function modifier<
  S,
  E extends ElementFor<S> = ElementFor<S>,
  P extends PositionalArgs<S> = PositionalArgs<S>,
  N extends NamedArgs<S> = NamedArgs<S>
>(
  fn: (el: E, pos: P, named: N) => void
): InvokableModifier<{
  Element: E,
  Args: {
    Named: N;
    Positional: P;
  };
}>;

The thing I have written as InvokableModifier here is an opaque type representing the result of creating a modifier this way, and is useful to e.g. Glint for capturing the type of the resulting modifier. The reason for writing it this way is that it lets you define a modifier either with an exported Signature interface or directly inline.

With a signature:

interface PlaySig {
  Args: {
    Named: {
      when: boolean;
    };
  };
  Element: HTMLMediaElement;
}

const playWithSig = modifier<PlaySig>((el, _, { when: shouldPlay }) => {
  if (shouldPlay) {
    el.play();
  } else {
    el.pause();
  };
});

Inline:

const playInline = modifier(
  (
    el: HTMLMediaElement,
    _: [],
    { when: shouldPlay}: { when: boolean }
  ) => {
    if (shouldPlay) {
      el.play();
    } else {
      el.pause();
    }
  }
);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions