-
Notifications
You must be signed in to change notification settings - Fork 34
Plan for v4 release #203
Description
An outline of what I think needs to be done for a v4 release:
- Update the types to use the latest (see Bump @types/ember from 3.16.5 to 4.0.0 #187 and Bump @types/ember-qunit from 4.0.0 to 5.0.0 #194: Dependabot is, quite reasonably, not smart enough to do this itself): Update to latest
@types/ember(*)and@types/qunit#209 - Convert this to an Embroider v2 addon (Convert to v2 addon #296)
- Update the class-based modifier API to bring it in line with the rest of the framework
- In class-based modifiers, make the
elementand args (positionalandnamed) arguments to the relevant methods, making the modifier APIs basically identical to the helper APIs, just with the addition of theelementargument, and do not eagerly consume args in the new API: Introduce newmodifyhook #217 - Deprecate using legacy lifecycle hooks Deprecate didInstall, didReceiveArguments, and didUpdateArguments #220
- Deprecate accessing
this.elementorthis.args(depends on previous step) Deprecatethis.elementandthis.argson class-based modifiers #221 - Remove
this.elementandthis.args(at release) Remove all deprecations targeting v4 #244
- In class-based modifiers, make the
- Update function-based modifiers not to always consume args eagerly
- introduce an
eageroption tomodifier()Introduceeageroption to function-based modifiers #222 - deprecate calling
modifier()without{ eager: false }Deprecatemodifier()without{ eager: false }#223
- introduce an
- Update the "signature" for modifiers in general to match what we're doing in RFC: Glimmer component
Signaturetype emberjs/rfcs#748- introduce a
Signaturetype with anArgsfieldNamedandPositional: SupportSignaturetypes for modifiers #210 - deprecate use of the signature which uses the classic
Argsform withnamedandpositional(depends on previous step) - drop support for the previous signature (at release) Remove all deprecations targeting v4 #244
- introduce a
- Switch from
willDestroyto using the destroyables API- deprecate
willDestroyin favor of usingregisterDestructorDeprecatewillDestroy()andisDestroy(ing|ed)#212 - update docs and examples accordingly
- remove
willDestroyhook (at release) Remove all deprecations targeting v4 #244
- deprecate
- Drop Node 12 Drop support for Node 12 #238
There are two big switches here:
-
Switch away from having
elementandargsset 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 anElementas 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!
-
Stop consuming
argseagerly. 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
didReceiveArgumentsanddidUpdateArguments? My own opinion is no (and the outline of the class below is updated accordingly) - Given that
didInstallis called afterdidReceiveArguments, 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 aboutdidReceiveArgumentsanddidUpdateArguments, 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();
}
}
);