Giter VIP home page Giter VIP logo

ember-modifier's Introduction

ember-modifier

This addon provides an API for authoring element modifiers in Ember. It mirrors Ember's helper API, with variations for writing both simple function-based modifiers and more complicated class-based modifiers.

NOTE: this is the README for the v4 release. For the v3 README, see here.

Compatibility

  • Ember.js v3.24 or above
  • Ember CLI v3.24 or above
  • Embroider or ember-auto-import v2.0.0 or above (this is v2 addon)

TypeScript

This project follows the current draft of the Semantic Versioning for TypeScript Types proposal.

  • Currently supported TypeScript versions: v4.2 - v4.9
  • Compiler support policy: simple majors
  • Public API: all published types not in a -private module are public

Installation

ember install ember-modifier

Philosophy

Modifiers are a basic primitive for interacting with the DOM in Ember. For example, Ember ships with a built-in modifier, {{on}}:

<button {{on "click" @onClick}}>
  {{@text}}
</button>

All modifiers get applied to elements directly this way (if you see a similar value that isn't in an element, it is probably a helper instead), and they are passed the element when applying their effects.

Conceptually, modifiers take tracked, derived state, and turn it into some sort of side effect related in some way to the DOM element they are applied to.

Whoa whoa whoa, hold on, what's a "side effect"?

A "side effect" is something that happens in programming all the time. Here's an example of one in an Ember component that attempts to make a button like in the first example, but without modifiers:

// πŸ›‘ DO NOT COPY THIS πŸ›‘
import Component from '@glimmer/component';

export default class MyButton extends Component {
  get setupEventHandler() {
    document.querySelector('#my-button').addEventListener(this.args.onClick);

    return undefined;
  }
}
<button id="#my-button">
  {{this.setupEventHandler}}

  {{@text}}
</button>

We can see by looking at the setupEventListener getter that it isn't actually returning a value. Instead, it always returns undefined. However, it also adds the @onClick argument as an event listener to the button in the template when the getter is run, as the template is rendering, which is a side effect

  • it is an effect of running the code that doesn't have anything to do with the "main" purpose of that code, in this case to return a dynamically computed value. In fact, this code doesn't compute a value at all, so this component is misusing the getter in order to run its side effect whenever it is rendered in the template.

Unmanaged side effects can make code very difficult to reason about, since any function could be updating a value elsewhere. In fact, the code above is very buggy:

  1. If the @onClick argument ever changes, it won't remove the old event listener, it'll just keep adding new ones.
  2. It won't remove the old event listener when the component is removed.
  3. It uses a document element selector that may not be unique, and it has no guarantee that the element will exist when it runs.
  4. It will run in Fastboot/Server Side Rendering, where no DOM exists at all, and it'll throw errors because of this.

However, there are lots of times where its difficult to write code that doesn't have side effects. Sometimes it would mean having to rewrite a large portion of an application. Sometimes, like in the case of modifying DOM, there isn't a clear way to do it at all with just getters and components.

This is where modifiers come in. Modifiers exist as a way to bridge the gap between derived state and side effects in way that is contained and consistent, so that users of a modifier don't have to think about them.

Managing "side effects" effectively

Let's look again at our original example:

<button {{on "click" @onClick}}>
  {{@text}}
</button>

We can see pretty clearly from this template that Ember will:

  1. Create a <button> element
  2. Append the contents of the @text argument to that button
  3. Add a click event handler to the button that runs the @onClick argument

If @text or @onClick ever change, Ember will keep everything in sync for us. We don't ever have to manually set element.textContent or update anything ourselves. In this way, we can say the template is declarative - it tells Ember what we want the output to be, and Ember handles all of the bookkeeping itself.

Here's how we could implement the {{on}} modifier so that it always keeps things in sync correctly:

import { modifier } from 'ember-modifier';

export default modifier((element, [eventName, handler]) => {
  element.addEventListener(eventName, handler);

  return () => {
    element.removeEventListener(eventName, handler);
  }
});

Here, we setup the event listener using the positional parameters passed to the modifier. Then, we return a destructor - a function that undoes our setup, and is effectively the opposite side effect. This way, if the @onClick handler ever changes, we first teardown the first event listener we added - leaving the element in its original state before the modifier ever ran - and then setup the new handler.

This is what allows us to treat the {{on}} modifier as if it were just like the {{@text}} value we put in the template. While it is side effecting, it knows how to setup and teardown that side effect and manage its state. The side effect is contained - it doesn't escape into the rest of our application, it doesn't cause other unrelated changes, and we can think about it as another piece of declarative, derived state. Just another part of the template!

In general, when writing modifiers, especially general purpose/reusable modifiers, they should be designed with this in mind. Which specific effects are they trying to accomplish, how to manage them effectively, and how to do it in a way that is transparent to the user of the modifier.

Usage

This addon does not provide any modifiers out of the box. Instead, this library allows you to write your own. There are two ways to write modifiers:

  1. Function-based modifiers
  2. Class-based modifiers

These are analogous to Ember's Helper APIs, helper and Helper.

Function-Based Modifiers

modifier is an API for writing simple modifiers. For instance, you could implement Ember's built-in {{on}} modifier like so with modifier:

// /app/modifiers/on.js
import { modifier } from 'ember-modifier';

export default modifier((element, [eventName, handler]) => {
  element.addEventListener(eventName, handler);

  return () => {
    element.removeEventListener(eventName, handler);
  }
});

Function-based modifiers consist of a function that receives:

  1. The element
  2. An array of positional arguments
  3. An object of named arguments
modifier((element, positional, named) => { /* */ });

This function runs the first time when the element the modifier was applied to is inserted into the DOM, and it autotracks while running. Any tracked values that it accesses will be tracked, including the arguments it receives, and if any of them changes, the function will run again.1

The modifier can also optionally return a destructor. The destructor function will be run just before the next update, and when the element is being removed entirely. It should generally clean up the changes that the modifier made in the first place.

Generating a Function-Based Modifier

To create a modifier (and a corresponding integration test), run:

ember g modifier scroll-top

Example without Cleanup

For example, if you wanted to implement your own scrollTop modifier (similar to this), you may do something like this:

// app/modifiers/scroll-top.js
import { modifier } from 'ember-modifier';

export default modifier((element, [scrollPosition]) => {
  element.scrollTop = scrollPosition;
})
<div class="scroll-container" {{scroll-top @scrollPosition}}>
  {{yield}}
</div>

Example with Cleanup

If the functionality you add in the modifier needs to be torn down when the element is removed, you can return a function for the teardown method.

For example, if you wanted to have your elements dance randomly on the page using setInterval, but you wanted to make sure that was canceled when the element was removed, you could do:

// app/modifiers/move-randomly.js
import { modifier } from 'ember-modifier';

const { random, round } = Math;

export default modifier(element => {
  const id = setInterval(() => {
    const top = round(random() * 500);
    const left = round(random() * 500);
    element.style.transform = `translate(${left}px, ${top}px)`;
  }, 1000);

  return () => clearInterval(id);
});
<button {{move-randomly}}>
  {{yield}}
</button>

Class-Based Modifiers

Sometimes you may need to do something more complicated than what can be handled by function-based modifiers. For instance:

  1. You may need to inject services and access them
  2. You may need fine-grained control of updates, either for performance or convenience reasons, and don't want to teardown the state of your modifier every time only to set it up again.
  3. You may need to store some local state within your modifier.

In these cases, you can use a class-based modifier instead. Here's how you would implement the {{on}} modifier with a class:

import Modifier from 'ember-modifier';
import { registerDestructor } from '@ember/destroyable';

function cleanup(instance: OnModifier) {
  let { element, event, handler } = instance;

  if (element && event && handler) {
    element.removeEventListener(event, handler);

    instance.element = null;
    instance.event = null;
    instance.handler = null;
  }
}

export default class OnModifier extends Modifier {
  element = null;
  event = null;
  handler = null;

  modify(element, [event, handler]) {
    this.addEventListener(element, event, handler);
    registerDestructor(this, cleanup)
  }

  // methods for reuse
  addEventListener = (element, event, handler) => {
    // Store the current element, event, and handler for when we need to remove
    // them during cleanup.
    this.element = element;
    this.event = event;
    this.handler = handler;

    element.addEventListener(event, handler);
  };
}

While this is slightly more complicated than the function-based version, but that complexity comes along with much more control.

As with function-based modifiers, the lifecycle hooks of class modifiers are tracked. When they run, then any values they access will be added to the modifier, and the modifier will update if any of those values change.

Generating a Class Modifier

To create a modifier (and a corresponding integration test), run:

ember g modifier scroll-top --class

Example without Cleanup

For example, let's say you want to implement your own {{scroll-position}} modifier (similar to this).

This modifier can be attached to any element and accepts a single positional argument. When the element is inserted, and whenever the argument is updated, it will set the element's scrollTop property to the value of its argument.

(Note that this example does not require the use of a class, and could be implemented equally well with a function-based modifier!)

// app/modifiers/scroll-position.js
import Modifier from 'ember-modifier';

export default class ScrollPositionModifier extends Modifier {
  modify(element, [scrollPosition], { relative }) {
    if(relative) {
      element.scrollTop += scrollPosition;
    } else {
      element.scrollTop = scrollPosition;
    }
  }
}

Usage:

{{!-- app/components/scroll-container.hbs --}}

<div
  class="scroll-container"
  style="width: 300px; heigh: 300px; overflow-y: scroll"
  {{scroll-position this.scrollPosition relative=false}}
>
  {{yield this.scrollToTop}}
</div>
// app/components/scroll-container.js

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class ScrollContainerComponent extends Component {
  @tracked scrollPosition = 0;

  @action scrollToTop() {
    this.scrollPosition = 0;
  }
}
{{!-- app/templates/application.hbs --}}

<ScrollContainer as |scroll|>
  A lot of content...

  <button {{on "click" scroll}}>Back To Top</button>
</ScrollContainer>

Example with Cleanup

If the functionality you add in the modifier needs to be torn down when the modifier is removed, you can use registerDestructor from @ember/destroyable.

For example, if you want to have your elements dance randomly on the page using setInterval, but you wanted to make sure that was canceled when the modifier was removed, you could do this:

// app/modifiers/move-randomly.js

import Modifier from 'ember-modifier';
import { registerDestructor } from '@ember/destroyable'

const { random, round } = Math;
const DEFAULT_DELAY = 1000;

function cleanup(instance) {
  if (instance.setIntervalId !== null) {
    clearInterval(instance.setIntervalId);
    instance.setIntervalId = null;
  }
}

export default class MoveRandomlyModifier extends Modifier {
  element = null;
  setIntervalId = null;

  constructor(owner, args) {
    super(owner, args);
    registerDestructor(this, cleanup);
  }

  modify(element, _, { delay }) {
    // Save off the element the first time for convenience with #moveElement
    if (!this.element) {
      this.element = element;
    }

    // Reset from any previous state.
    cleanup(this);

    this.setIntervalId = setInterval(this.#moveElement, delay ?? DEFAULT_DELAY);
  }

  #moveElement = (element) => {
    let top = round(random() * 500);
    let left = round(random() * 500);
    this.element.style.transform = `translate(${left}px, ${top}px)`;
  };
}

Usage:

<div {{move-randomly}}>
  Catch me if you can!
</div>

Example with Service Injection

You can also use services into your modifier, just like any other class in Ember.

For example, suppose you wanted to track click events with ember-metrics:

// app/modifiers/track-click.js

import { inject as service } from '@ember/service';
import Modifier from 'ember-modifier';
import { registerDestructor } from '@ember/destroyable';

function cleanup(instance) {
  instance.element?.removeEventListener('click', instance.onClick, true);
}

export default class TrackClick extends Modifier {
  @service metrics;

  constructor(owner, args) {
    super(owner, args);
    registerDestructor(this, this.cleanup);
  }

  modify(element, [eventName], options) {
    this.element = element;
    this.eventName = eventName;
    this.options = options;

    this.cleanup();
    element.addEventListener('click', this.onClick, true);
  }

  onClick = () => {
    this.metrics.trackEvent(this.eventName, this.options);
  };
}

Usage:

<button {{track-click "like-button-click" page="some page" title="some title"}}>
  Click Me!
</button>

API

constructor(owner, args)
Constructor for the modifier. You must call super(...arguments) before performing other initialization.
modify(element, positionalArgs, namedArgs)
The primary hook for running a modifier. It gets called when the modifier is installed on the element, and any time any tracked state it uses changes. That tracked state can be from its arguments, which are auto-tracked, or from any other kind of tracked state, including but not limited to state on injected services.

TypeScript

Both the function- and class-based APIs can be used with TypeScript!

Before checking out the Examples with Typescript below, there is an important caveat you should understand about type safety!

There are, today, two basic approaches you can take to dealing with your modifier's arguments and element in a type safe way:

  1. You can use a type definition which specifies those for the outside world, relying on tooling like Glint to check that the invocation is correct, and treat input as safe accordingly.
  2. You can provide the minimal public interface which all modifiers conform to, and do runtime type checking with assert calls to make your internal implementation safe.

If you have a code base which is strictly typed from end to end, including with template type checking via Glint, then (1) is a great choice. If you have a mixed code base, or are publishing an addon for others to use, then it's usually best to do both (1) and (2)!

To handle runtime checking, for non-type-checked templates (including projects not yet using Glint or supporting external callers), you should act as though the arguments passed to your modifier can be anything. They’re typed as unknown by default, which means by default TypeScript will require you to work out the type passed to you at runtime. For example, with the ScrollPositionModifier shown above, you can combine TypeScript’s type narrowing with the default types for the class to provide runtime errors if the caller passes the wrong types, while providing safety throughout the rest of the body of the modifier. Here, modify would be guaranteed to have the correct types for scrollPosition and relative:

import Modifier from 'ember-modifier';
import { assert } from '@ember/debug';

export class ScrollPositionModifier extends Modifier {
  modify(element, [scrollPosition], { relative }) {
    assert(,
      `first argument to 'scroll-position' must be a number, but ${scrollPosition} was ${typeof scrollPosition}`,
      typeof scrollPosition === "number"
    );

    assert(
      `'relative' argument to 'scroll-position' must be a boolean, but ${relative} was ${typeof relative}`,
      typeof relative === "boolean"
    );

    if (relative) {
      element.scrollTop += scrollPosition;
    } else {
      element.scrollTop = scrollPosition;
    }
  }
}

If you were writing for a fully-typed context, you can define your Modifier with a Signature interface, similar to the way you would define your signature for a Glimmer Component:

// app/modifiers/scroll-position.ts
import Modifier from 'ember-modifier';

interface ScrollPositionModifierSignature {
  Args: {
    Positional: [number];
    Named: {
      relative: boolean;
    };
  };
}

export default class ScrollPositionModifier
    extends Modifier<ScrollPositionModifierSignature> {
  modify(element, [scrollPosition], { relative }) {
    if (relative) {
      element.scrollTop += scrollPosition;
    } else {
      element.scrollTop = scrollPosition;
    }
  }
}

Besides supporting integration with Glint, this also provides nice hooks for documentation tooling. Note, however, that it can result in much worse feedback in tests or at runtime if someone passes the wrong kind of arguments to your modifier and you haven't included assertions: users who pass the wrong thing will just have the modifier fail. For example, if you fail to pass the positional argument, scrollPosition would simply be undefined, and then element.scrollTop could end up being set to NaN. Whoops! For that reason, if your modifier will be used by non-TypeScript consumers, you should both publish the types for it and add dev-time assertions:

// app/modifiers/scroll-position.ts
import Modifier from 'ember-modifier';

interface ScrollPositionModifierSignature {
  Args: {
    Positional: [scrollPosition: number];
    Named: {
      relative: boolean;
    };
  };
  Element: Element; // not required: it'll be set by default
}

export default class ScrollPositionModifier
    extends Modifier<ScrollPositionModifierSignature> {
  modify(element, [scrollPosition], { relative }) {
    assert(,
      `first argument to 'scroll-position' must be a number, but ${scrollPosition} was ${typeof scrollPosition}`,
      typeof scrollPosition === "number"
    );

    assert(
      `'relative' argument to 'scroll-position' must be a boolean, but ${relative} was ${typeof relative}`,
      typeof relative === "boolean"
    );

    if (relative) {
      element.scrollTop += scrollPosition;
    } else {
      element.scrollTop = scrollPosition;
    }
  }
}

The Signature type

The Signature for a modifier is the combination of the positional and named arguments it receives and the element to which it may be applied.

interface Signature {
  Args: {
    Named: {
      [argName: string]: unknown;
    };
    Positional: unknown[];
  };
  Element: Element;
}

When writing a signature yourself, all of those are optional: the types for modifiers will fall back to the correct defaults of Element, an object for named arguments, and an array for positional arguments. You can apply a signature when defining either a function-based or a class-based modifier.

In a function-based modifier, the callback arguments will be inferred from the signature, so you do not need to specify the types twice:

interface MySignature {
  Element: HTMLMediaElement;
  Args: {
    Named: {
      when: boolean;
    };
    Positional: [];
  };
}

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

You never need to specify a signature in this way for a function-based modifier: you can simply write the types inline instead:

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

However, the explicit modifier<Signature>(...) form is tested to keep working, since it can be useful for documentation!

The same basic approach works with a class-based modifier:

interface MySignature {
  // ...
}

export default class MyModifier extends Modifier<MySignature> {
  // ...
}

In that case, the element and args will always have the right types throughout the body. Since the type of args in the constructor are derived from the signature, you can use the ArgsFor type helper to avoid having to write the type out separately:

import Modifier, { ArgsFor } from 'ember-modifier';

interface MySignature {
  // ...
}

export default class MyModifier extends Modifier<MySignature> {
  constructor(owner: unknown, args: ArgsFor<MySignature>) {
    // ...
  }
}

ArgsFor isn't magic: it just takes the Args from the Signature you provide and turns it into the right shape for the constructor: the Named type ends up as the named field and the Positional type ends up as the type for args.positional, so you could write it out yourself if you preferred:

import Modifier from 'ember-modifier';

interface MySignature {
  // ...
}

export default class MyModifier extends Modifier<MySignature> {
  constructor(
    owner: unknown,
    args: {
      named: MySignature['Args']['Named'];
      positional: MySignature['Args']['Positional'];
    }
  ) {
    // ...
  }
}

Examples with TypeScript

Function-based modifier

Let’s look at a variant of the move-randomly example from above, implemented in TypeScript, and now requiring a named argument, the maximum offset. Using the recommended combination of types and runtime type-checking, it would look like this:

// app/modifiers/move-randomly.js
import { modifier } from 'ember-modifier';
import { assert } from '@ember/debug';

const { random, round } = Math;

export default modifier(
  (element: HTMLElement, _: [], named: { maxOffset: number }
) => {
  assert(
    'move-randomly can only be installed on HTML elements!',
    element instanceof HTMLElement
  );

  const { maxOffset } = named;
  assert(
    `The 'max-offset' argument to 'move-randomly' must be a number, but was ${typeof maxOffset}`,
    typeof maxOffset === "number"
  );

  const id = setInterval(() => {
    const top = round(random() * maxOffset);
    const left = round(random() * maxOffset);
    element.style.transform = `translate(${left}px, ${top}px)`;
  }, 1000);

  return () => clearInterval(id);
});

A few things to notice here:

  1. TypeScript correctly infers the base types of the arguments for the function passed to the modifier; you don't need to specify what element or positional or named are unless you are doing like we are in this example and providing a usefully more-specific type to callers.

  2. If we returned a teardown function which had the wrong type signature, that would also be an error.

    If we return a value instead of a function, for example:

    export default modifier((element, _, named) => {
      // ...
    
      return id;
    });

    TypeScript will report:

    Type 'Timeout' is not assignable to type 'void | Teardown'.
    

    Likewise, if we return a function with the wrong signature, we will see the same kinds of errors. If we expected to receive an argument in the teardown callback, like this:

    export default modifier((element, _, named) => {
      // 
    
      return (interval: number) => clearTimeout(interval);
    });

    TypeScript will report:

    Type '(interval: number) => void' is not assignable to type 'void | Teardown'.
    

Class-based

To support correctly typing args in the constructor for the case where you do runtime type checking, we supply an ArgsFor type utility. (This is useful because the Signature type, matching Glimmer Component and other "invokable" items in Ember/Glimmer, has capital letters for the names of the types, while args.named and args.positional are lower-case.) Here’s how that would look with a fully typed modifier that alerts "This is a typesafe modifier!" an amount of time after receiving arguments that depends on the length of the first argument and an optional multiplier (a nonsensical thing to do, but one that illustrates a fully type-safe class-based modifier):

import Modifier, { ArgsFor, PositionalArgs, NamedArgs } from 'ember-modifier';
import { assert } from '@ember/debug';

interface NeatSignature {
  Args: {
    Named: {
      multiplier?: number;
    };
    Positional: [string];
  }
}


function cleanup(instance: Neat) => {
  if (instance.interval) {
    clearInterval(instance.interval);
  }
}

export default class Neat extends Modifier<NeatSignature> {
  interval?: number;

  constructor(owner: unknown, args: ArgsFor<NeatSignature>) {
    super(owner, args);
    registerDestructor(this, cleanup);
  }

  modify(
    element: Element,
    [lengthOfInput]: PositionalArgs<NeatSignature>,
    { multiplier }: NamedArgs<NeatSignature>
  ) {
    assert(
  	  `positional arg must be 'string' but was ${typeof lengthOfInput}`,
  	  typeof lengthOfInput === 'string'
  	);

    assert(
    	`'multiplier' arg must be a number but was ${typeof multiplier}`,
    	multiplier ? typeof multiplier === "number" : true
    );

    multiplier = modifier ?? 1000;

    let updateTime = multiplier * lengthOfInput;
    this.interval = setInterval(() => {
      element.innerText =
        `Behold, a type safe modifier moved after ${updateTime / 1000}s`;
    }, updateTime)
  }
}

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.

Footnotes

  1. As with autotracking in general, β€œchanges” here actually means that the tracked property was setβ€”even if it was set to the same value. This is because autotracking does not cache the values of properties, only the last time they changed. See this blog post for a deep dive on how it works! ↩

ember-modifier's People

Contributors

asakusuma avatar banupriya-bp avatar bartocc avatar bertdeblock avatar boussonkarel avatar chriskrycho avatar dependabot[bot] avatar dfreeman avatar ef4 avatar elwayman02 avatar ember-tomster avatar gitkrystan avatar heroiceric avatar jelhan avatar mrchocolatine avatar mydea avatar nlfurniss avatar patocallaghan avatar pzuraq avatar raido avatar rwjblue avatar sandstrom avatar sandydoo avatar sergeastapov avatar spencer516 avatar tomwayson avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

ember-modifier's Issues

`[email protected]` erroneously reporting deprecations

Just installed the latest version of ember-modifier and converted my {{modifier}} to use the patterns. However, even though I'm not using any of the deprecated methods, I still get a spew of deprecation logs in my console output. Here's an example of one of my modifiers

import { registerDestructor } from '@ember/destroyable';
import Modifier from 'ember-modifier';
import tippy from 'tippy.js';

const cleanup = (instance) => {
  instance.tippy?.destroy();
};

class TippyModifier extends Modifier {
  tippy = null;

  constructor() {
    super(...arguments);
    registerDestructor(this, cleanup);
  }

  modify(element, _, { content }) {
    cleanup(this);

    if (!content) {
      this.tippy = null;
      return;
    }

    this.tippy = tippy(element, {
      content,
    });
  }
}

export default TippyModifier;

As you can see, no deprecated methods. However, I get every single deprecation warning logged to console for every single instance of the modifier on the page (which is a lot)

Simplify creation and management of functional modifier manager

The functional modifier manager does not use the owner at all (the original logic for managerFor was created to support passing services to functional modifiers, but that functionality was removed while merging into ember-modifier), this should be simplified to:

return setModifierManager(() => Manager, fn);

The functional modifier manager file should be updated to do:

export const Manager = new FunctionalManager();

This is an important but non-blocking issue, I think making a new issue and linking with an inline comment is fine for now.

cc @pzuraq

Originally posted by @rwjblue in #23

No way to listen for changes to `this.element`

I built a modifier heavily based on @miguelcobain:s css-transitions modifier.

It lets you animate an element out by cloning it and keeping track of an elements nextElementSibling and insert the clone before it.

In this example I have an ember-dragula list implemented and every item in the list uses my custom-modifier.

I was able to work around it by adding a MutationObserver tracking removed nodes in the list and sending that array of removed nodes as an argument which triggers didUpdateArguments and in that I update nextElementSiblings. This fixes this problem, see video, showing the bug.

In the twiddle I add a green outline for 3 seconds when an element is removed. Notice how when the mutation observer the outline is on the wrong element for the second deleted item:

MutationObserver off

CleanShot.2022-02-14.at.19.41.12.mp4

MutationObserver on

CleanShot.2022-02-14.at.19.41.44.mp4

Maybe we could add something like a didUpdateElement event or have this.element changes trigger didUpdateArguments?

3.2+: Class-based modifiers don't call `modify`?

I'm following the migration guide to transition our modifiers to the new format, and I can't seem to get my class-based modifiers to call modify - not even on initial setup. My modifier is almost identical to one found in the README:

import Modifier, { ArgsFor, PositionalArgs, NamedArgs } from 'ember-modifier';
import { registerDestructor } from '@ember/destroyable';
import { inject as service } from '@ember/service';
import AnalyticsService from 'my-application/services/analytics';

interface TrackClickModifierSignature {
  Args: {
    Positional: [string];
    Named: Record<string, string>;
  };
  Element: Element;
}

export default class TrackClickModifier extends Modifier<TrackClickModifierSignature> {
  @service declare analytics: AnalyticsService;

  clickElement?: Element;
  name?: string;
  properties?: Record<string, string>;

  constructor(owner: unknown, args: ArgsFor<TrackClickModifierSignature>) {
    super(owner, args);
    registerDestructor(this, cleanup);
  }

  modify(
    element: Element,
    [name]: PositionalArgs<TrackClickModifierSignature>,
    properties: NamedArgs<TrackClickModifierSignature>
  ) {
    this.clickElement = element;
    this.name = name;
    this.properties = properties;
    cleanup(this);
    element.addEventListener('click', this.onClick, true);
  }

  onClick = () => {
    let { analytics, name, properties } = this;
    if (name) {
      analytics.trackEvent?.(name, properties);
    }
  };
}

function cleanup(instance: TrackClickModifier): void {
  instance.clickElement?.removeEventListener('click', instance.onClick, true);
}

Any guidance here, or is this a bug? If I put a console.log inside both constructor and modify, the constructor one gets called but not the modify one.

Thanks for the work on the new direction here @chriskrycho. Though it's a notable change, I do find these modifiers easier to understand now. I also appreciate the deprecation-based upgrade path, so I can merge my function-based modifier updates now as I await a resolution here for my class-based ones.

helperCreateClassFeaturesPlugin.injectInitialization is not a function

Hi,

After recreating a yarn.lock file, I'm seeing this:

$ ember serve
Build Error (broccoli-persistent-filter:Babel > [Babel: ember-modifier]) in ember-modifier/-private/class/modifier-manager.ts

/Users/wmattgardner/labs-zola/ember-modifier/-private/class/modifier-manager.ts: (0 , _helperCreateClassFeaturesPlugin.injectInitialization) is not a function

Stack Trace and Error Report: /var/folders/gs/x79h3r4179gbvdh2pry1lcfc0000gn/T/error.dump.6d8f45a9f40b6a10a8abdce76940f82e.log

image

Kinda stumped here. The package.json can be found here: https://github.com/NYCPlanning/labs-zola/blob/develop/package.json

Seems as though it's trying to reach for a file and it's one directory off (see bolded line)?

Not clear that 1.0.3 isn't compatible with ember 3.20

Pretty minor, but I think it would be worthwhile to mention that 1.0.3 isn't compatible with ember 3.20. Upon upgrading to 3.20 started getting container issues in component tests where modifiers were used. I don't mind submitted a PR to add this to the readme, if that is something you would be interested in, if you are, how would you like it formatted?

Deprecation: Versions of modifier manager capabilities prior to 3.22 have been deprecated

After updating https://github.com/rust-lang/crates.io to Ember 3.26 I've started seeing deprecation warnings in the test suite:

deprecate.js:136 DEPRECATION: Versions of modifier manager capabilities prior to 3.22 have been deprecated. You must update to the 3.22 capabilities. [deprecation id: manager-capabilities.modifiers-3-13] See https://emberjs.com/deprecations/v3.x#toc_manager-capabilities-modifiers-3-13 for more details.
at logDeprecationStackTrace (http://localhost:4040/assets/vendor.js:35109:21)
at HANDLERS. (http://localhost:4040/assets/vendor.js:35242:9)
at raiseOnDeprecation (http://localhost:4040/assets/vendor.js:35136:9)
at HANDLERS. (http://localhost:4040/assets/vendor.js:35242:9)
at invoke (http://localhost:4040/assets/vendor.js:35254:9)
at deprecate (http://localhost:4040/assets/vendor.js:35210:28)
at Object.modifierCapabilities$1 [as _modifierManagerCapabilities] (http://localhost:4040/assets/vendor.js:10714:62)
at new FunctionalModifierManager (http://localhost:4040/assets/vendor.js:111676:51)
at Module.callback (http://localhost:4040/assets/vendor.js:111704:18)

deprecate.js:136 DEPRECATION: Versions of modifier manager capabilities prior to 3.22 have been deprecated. You must update to the 3.22 capabilities. [deprecation id: manager-capabilities.modifiers-3-13] See https://emberjs.com/deprecations/v3.x#toc_manager-capabilities-modifiers-3-13 for more details.
at logDeprecationStackTrace (http://localhost:4040/assets/vendor.js:35109:21)
at HANDLERS. (http://localhost:4040/assets/vendor.js:35242:9)
at raiseOnDeprecation (http://localhost:4040/assets/vendor.js:35136:9)
at HANDLERS. (http://localhost:4040/assets/vendor.js:35242:9)
at invoke (http://localhost:4040/assets/vendor.js:35254:9)
at deprecate (http://localhost:4040/assets/vendor.js:35210:28)
at Object.modifierCapabilities$1 [as _modifierManagerCapabilities] (http://localhost:4040/assets/vendor.js:10714:62)
at new ClassBasedModifierManager (http://localhost:4040/assets/vendor.js:111499:51)
at http://localhost:4040/assets/vendor.js:111642:38

It looks like these deprecation warnings are caused by the ember-modifier addon, which we are using in the latest version: 2.1.1

/cc @pzuraq @rwjblue

Local modifiers not working correctly

Firstly, apologies if this is being raised too soon. I heard about local modifiers on the grape vine, and I'm not aware of how far along they officially are.

I've created a very basic demo app that compares local modifiers to global ones, because I think there is something wrong with the install/update/destroy hooks.

Here is a video of the demo app: https://share.getcloudapp.com/7KuPbQLR.

ModifierArgs should be type-only export

export { ModifierArgs } from './-private/interfaces';

I think this line should read:

export type { ModifierArgs } from './-private/interfaces';

To avoid this error from embroider / webpack:

WARNING in ../node_modules/ember-modifier/index.js 3:0-53
export 'ModifierArgs' (reexported as 'ModifierArgs') was not found in './-private/interfaces' (module has no exports)
 @ ../node_modules/ember-ref-bucket/modifiers/create-ref.js 47:0-38 53:54-62
 @ ./modifiers/create-ref.js 1:0-83 1:0-83

Add class modifier blueprints

Currently we have functional modifier blueprints, but not blueprints class modifiers. We should add them to fill out the blueprint API.

Proposal: change `element` availability

Right now, the class-based modifier specifies that the element is not available during the constructor or during willDestroy. The first of these is non-negotiable within the constraints of the current modifier manager spec, and seems unlikely to change. However, the second is a design choiceβ€”I suspect it was made out of a worry that it would cause a memory leak, or perhaps to mirror the API from classic Ember components. Regardless, the modifier manager forces this:

destroyModifier(instance) {
instance.willRemove();
instance.element = null;
if (instance[DESTROYING]) {
return;
}
let meta = Ember.meta(instance);
meta.setSourceDestroying();
instance[DESTROYING] = true;
schedule('actions', instance, instance.willDestroy);
schedule('destroy', undefined, scheduleDestroy, instance, meta);
}
}

After discussing this with @rwjblue and @pzuraq, I propose we stop removing the element during willDestroy: it will be garbage collected properly during teardown of the modifier and element, and having this restriction in place substantially increases the complexity of both the docs and the types (see discussion on #23 for details on the latter), while providing no concrete benefit.

This does raise a question about the utility of having both willRemove and willDestroy. If we leave this.element in place for willDestroy, should we just deprecate willRemove as extraneous, and plan to remove it in a later major?

Named arguments should be more prominent in the API design and docs

Premise

I would posit that named arguments are a more robust architecture for most things, and that designs like the on modifier are the exception where positional is preferred, rather than the rule. Most people should be building custom modifiers using named arguments, only switching to positional params where the use-case is very clearly more ergonomic.

Problem Statement

In the entirety of the README for ember-modifier, there are very few examples of modifiers that use named arguments, and mostly it's just being shown within the context of other features, showing how it interacts with that functionality. Additionally, the fact that the named arguments are passed after positional arguments in the functional modifier design implies that positional arguments are the default.

These aspects of ember-modifier send a very strong signal that developers should be using positional params over named arguments wherever possible. We should rethink the design of ember-modifier and make sure to document it in a way that presents named arguments as the default pattern, with positional arguments as an option when and if you need them.

Type 'typeof import("ember-modifier")' is not a constructor function type.

I have recently created a new addon on Ember 3.25.3, and I added Typescript support via ember-cli-typescript at 4.1.0 (typescript version is 4.2.4).

When consuming this addon in our host app (also using typescript), I can't get it to build, because I'm getting TS errors:

node_modules/my-addon/modifiers/my-modifier.d.ts:5:43 - error TS2507: Type 'typeof import("ember-modifier")' is not a constructor function type.

5 export default class MyModifier extends Modifier {

The addon has run ember ts:precompile, which generates the modifiers/my-modifier.d.ts file where the warning appears.

I'm not sure how to fix this error, is there something wrong with the TS declarations in this addon?

Error in willRemove halts page

I don't know if this is intentional, but we are using an addon that caused an error in the willRemove() hook due to willRemove() was run too late after the route has changed and the new page had rendered. This seems to halt ember completely. No actions or anything works beyond that point. Maybe no errors should ever be thrown in willRemove()?

This is the line causing the issue I believe:

The PR that fixes the error thrown in willRemove for reference: adopted-ember-addons/ember-sortable#374

Consider different names for class modifier "Arguments" hooks

I've been playing around with modifiers a bit recently and specifically ember-modifier (this is an awesome addon by the way, really love it) and I ran into a good but unexpected feature and it caused me a bit of confusion (see this Discourse thread).

Essentially because the didReceiveArguments/didUpdateArguments hooks specifically mention "arguments" I was expecting them to only be fired specifically on argument updates. What I was missing was the note in the README that says:

As with functional modifiers, the lifecycle hooks of class modifiers are tracked. When they run, they any values they access will be added to the modifier, and the modifier will update if any of those values change.

The way it works is delightful and exactly what I wanted, however I think because the names of the hooks include "arguments" it implies the wrong thing about when they are fired (and even in the README it says "... and anytime the arguments are updated").

I think there are probably more intuitive names that would suggest that these hooks will be called when the arguments OR any consumed tracked props are updated and this could reduce some confusion as we look towards shipping this with Ember. I'm not sure exactly what the best name would be, willUpdate makes sense on some level but obviously that drags the component lifecycle hook history in with it... but I think maybe leaving the word "arguments" out would be very helpful regardless.

expected to be able to return a clean up function from did-insert callback

I expected that if I returned a function from the did-insert callback that it would would be called before the element was removed from the DOM the same way that you can do w/ custom modifiers:

https://github.com/ember-modifier/ember-modifier#example-with-cleanup

However, I don't see my clean up function being invoked. I can't tell from the docs in this repo if my expectation is correct or not.

If not, should I be using will-destroy instead of returning a clean up function?

Plan for v4 release

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

  • Update the types to use the latest (see #187 and #194: Dependabot is, quite reasonably, not smart enough to do this itself): #209
  • Convert this to an Embroider 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 element and args (positional and named) arguments to the relevant methods, making the modifier APIs basically identical to the helper APIs, just with the addition of the element argument, and do not eagerly consume args in the new API: #217
    • Deprecate using legacy lifecycle hooks #220
    • Deprecate accessing this.element or this.args (depends on previous step) #221
    • Remove this.element and this.args (at release) #244
  • Update function-based modifiers not to always consume args eagerly
    • introduce an eager option to modifier() #222
    • deprecate calling modifier() without { eager: false } #223
  • Update the "signature" for modifiers in general to match what we're doing in emberjs/rfcs#748
    • introduce a Signature type with an Args field Named and Positional: #210
    • deprecate use of the signature which uses the classic Args form with named and positional (depends on previous step)
    • drop support for the previous signature (at release) #244
  • Switch from willDestroy to using the destroyables API
    • deprecate willDestroy in favor of using registerDestructor #212
    • update docs and examples accordingly
    • remove willDestroy hook (at release) #244
  • Drop Node 12 #238

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();
    }
  }
);

Arguments Missing When Set After Construction

I use ember-modifier as the foundation for the modifiers in ember-popper-modifier. Since [email protected] has been available, the tests against the latest release of Ember have been failing.

For a while I assumed that the issue was in my testing set-up, but upon investigation today, it seems that that is not the case.

The way that the addon is designed, you pass a reference to another DOM element into the modifier as an argument. This means that, since that variable is probably given a value in some way by the did-insert modifier attached to another element, the argument only becomes available on the "tick" after construction. The modifier is written so that this is handled fine.

<button {{popper this.tooltipElement}}>
  I have a tool-tip
</button>
<span {{did-insert (set this 'tooltipElement')}}>
  I am the tooltip
</span>

The sequence of events that I am expecting -- and what happened prior to 3.22 -- was

  1. constructor sets up the initial args value where positional[0] is undefined
  2. didReceiveArguments updates the argument and fills in positional[0] one did-insert finishes calling set

Since 3.22 (and on release since) this doesn't happen; positional[0] stays undefined even after the set helper is called.

One thing that I noticed is that the modifier manager, during the installModifier hook, has an opportunity to set args on the modifier that it does not take. This method

installModifier(instance: ClassBasedModifier, element: Element): void {

Receives args as a third parameter, and in my testing, that copy of args contains the expected values. However, the implementation right now ignores that argument and does not actually use it to update the args on the modifier.

It seems to me that the new series of events is

  1. constructor sets up the initial args value where positional[0] is undefined
  2. installModifier in the manager could update the args to provide the value for positional[0] but does not
  3. didReceiveArguments therefore does not have the new values of the arguments

Is this related to the changes in capabilities with modifier managers in Ember 3.22? It seems like too much of a coincidence that things related to modifier managers changed in the same release that my addon started acting funny. I know that there are PRs open to address these problems that have been open for... a while now. Maybe #63 would help?

strongly typed arguments

How would you strongly type your modifier class arguments?

e.g.

interface InjectLiveDataModifierArgs extends ModifierArgs<unknown> {
  device: string,
  metric: string
}

export default class InjectLiveDataModifier extends Modifier<InjectLiveDataModifierArgs> {
  get device(): string {
    return this.args.named.device; // Type 'unknown' is not assignable to type 'string'
  }
}

failure on install with ember 3.22

Ember version is 3.22. When installing into an addon:

bryan$ ember install ember-modifier

🚧  Installing packages... This might take a couple of minutes.
Yarn: Installed ember-modifier
[ember-cli-version-checker] 'checker.forEmber' has been removed, please use 'checker.for(`ember-source`)'


Stack Trace and Error Report: /var/folders/24/9c9x7j316gbd3z5wg84fghqc0000gn/T/error.dump.11e4de0a394f5c48839384d45a0abae6.log
An error occurred in the constructor for ember-modifier-manager-polyfill at /Users/bryan/Projects/ember-template-editor/node_modules/ember-modifier-manager-polyfill

An error occurred in the constructor for ember-modifier at /Users/bryan/Projects/ember-template-editor/node_modules/ember-modifier

Missing types?

readme has information about typescript, but the whole addon is JS with no type definition :-\

Unknown blueprint: functional-modifier

The yarn install was successful for "ember-modifier" package but when trying to generate a functional modifier, I am getting unknown blueprint error.

$ ember g functional-modifier my-modifier
Unknown blueprint: functional-modifier
this blueprint is not avail. in the list of "Available blueprints" on my machine.

Build Issue with v2.0.0: @ember/destroyable does not have a destroy export

I’m seeing a build issue since the [email protected] upgrade

Build Error (broccoli-persistent-filter:Babel > [Babel: ember-modifier]) in ember-modifier/-private/class/modifier-manager.ts

/home/runner/work/ember-iframe-resizer-modifier/ember-iframe-resizer-modifier/ember-modifier/-private/class/modifier-manager.ts: @ember/destroyable does not have a destroy export
  1 | import { capabilities } from '@ember/modifier';
  2 | import { set } from '@ember/object';
> 3 | import { destroy, registerDestructor } from '@ember/destroyable';
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  4 | 
  5 | import ClassBasedModifier from './modifier';
  6 | import { ModifierArgs } from 'ember-modifier/-private/interfaces';

You can see the results of trying to run ember test on some of my addons here

https://github.com/alexlafroscia/ember-iframe-resizer-modifier/runs/931614651
https://github.com/alexlafroscia/ember-resize-observer-modifier/runs/931619402

Run type tests nightly

We should use Travis' Cron Jobs feature to run just the type tests nightly, so we get the earliest possible signal of regression against upcoming TS versions.

Switch to fully using babel.config.js when possible

In #101, I introduced @babel/eslint-parser to replace the deprecated babel-eslint-parser. It correctly requires you to specify your config explicitly. However, because of the issues ID'd in emberjs/ember-cli-babel#418, we cannot switch to using it throughout, and so instead are using it only for linting presently. Once the upstream design issues are resolved, update to use it directly.

Invalid modifier manager compatibility specified

Hi!

I'm developing an addon and I'm seeing the error:

Invalid modifier manager compatibility specified

It does not happen locally and happens on CI only for ember-release, ember-beta and ember-canary scenarios.

The error seems to originate from this line:

https://github.com/glimmerjs/glimmer-vm/blob/v0.83.1/packages/@glimmer/manager/lib/public/modifier.ts#L25

ember-modifier is a subdependency of my addon, I'm not using it directly. There was a dependency hell, so I've locked the version like this:

{
  "resolutions": {
    "ember-modifier": "^3.0.0"
  }
}

...which produces the following yarn.lock entry:

ember-modifier@^2.1.2, ember-modifier@^3.0.0:
  version "3.0.0"

But it doesn't help, I still see the error.

The addon is open source, here's the CI run:
https://github.com/kaliber5/ember-behave/runs/4417577508?check_suite_focus=true

Here's the addon at the commit that produces that CI output (not linking to a branch because branches are short-living):
https://github.com/kaliber5/ember-behave/tree/89ccc63e10757c0a1795b635e446e6c080997bb8

Any special setup required in user land?

Is there any special setup required for ember-modifier v2 (which uses ember-destroyable-polyfill under the hood) in user land?

Tried to bump ember-modifier to v2 in DockYard/ember-in-viewport#246 but build fails with error

Build Error (broccoli-persistent-filter:Babel > [Babel: ember-modifier]) in ember-modifier/-private/class/modifier-manager.ts
302
303/home/travis/build/DockYard/ember-in-viewport/ember-modifier/-private/class/modifier-manager.ts: @ember/destroyable does not have a destroy export
304  1 | import { capabilities } from '@ember/modifier';
305  2 | import { set } from '@ember/object';
306> 3 | import { destroy, registerDestructor } from '@ember/destroyable';
307    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
308  4 | 
309  5 | import ClassBasedModifier from './modifier';
310  6 | import { ModifierArgs } from 'ember-modifier/-private/interfaces';

quite puzzled as typescript setup mentioned in https://github.com/ember-polyfills/ember-destroyable-polyfill#typescript-usage seem to be already done.

Types - Use interface `ModifierArgs` as a generic

I was creating a modifier in TypeScript and, while playing/digging into the types, I noticed the interface ModifierArgs:
https://github.com/ember-modifier/ember-modifier/blob/a8d4da7914dc989bbaa3aad71991dc48da3e2dae/addon/-private/interfaces.ts

This interface is built as a generic but it is not used as such:

export type FunctionalModifier<
P extends ModifierArgs['positional'] = ModifierArgs['positional'],
N extends ModifierArgs['named'] = ModifierArgs['named']
> = (element: Element, positional: P, named: N) => unknown;

Could we accept another parameter for the positional parameters?

export interface ModifierArgs<P = any, N = unknown> {
    positional: P[];
    named: Record<string, N>;
}

Modifiers and their use with splattributes

Consider the following:

{{! my-component.hbs }}
<div  ...attributes title="Inner"></div>
{{! application.hbs }}
<MyComponent title="Outer" />

This results in:

<div title="Inner"></div>

If I then flip the splattributes to come last:

{{! my-component.hbs }}
<div title="Inner" ...attributes></div>

This results in:

<div title="Outer"></div>

This is a nice feature because it allows default values, and for them to be overridden.


But, now consider the same concept, but with a modifier:

import { modifier } from 'ember-modifier';

export default modifier(function title(element, [title]) {
  element.setAttribute('title', title);
});

Example:

{{! my-component.hbs }}
<div {{title "Inner"}} ...attributes></div>
{{! my-component.hbs }}
<div ...attributes {{title "Inner"}}></div>
{{! application.hbs }}
<MyComponent {{title "Outer"}} />

One might expect, the order of ...attributes to produce the same result. But it does not (The "Inner" modifier always wins)

Am I expecting too much?

Ember 3.15 - Modifier test always imports hbs from htmlbars-inline-precompile

Description

The blueprint for a modifier test hardcoded the hbs import statement.

import hbs from 'htmlbars-inline-precompile';

https://github.com/ember-modifier/ember-modifier/blob/master/blueprints/modifier-test/qunit-files/__root__/integration/__collection__/__name__-test.js#L4

For Ember 3.15, which uses ember-cli-htmlbars ^4.2.0, I believe the blueprint would need to generate the line:

import { hbs } from 'ember-cli-htmlbars';

https://github.com/ember-cli/ember-cli-htmlbars#tagged-template-usage--migrating-from-htmlbars-inline-precompile

Proposed Solution

The blueprint for a component test shows how we can check the version of ember-cli-htmlbars in order to dynamically add the import statement:

https://github.com/emberjs/ember.js/blob/master/blueprints/component-test/index.js#L89-L97

https://github.com/emberjs/ember.js/blob/master/blueprints/component-test/qunit-rfc-232-files/__root__/__testType__/__path__/__test__.js#L4

Please check both QUnit and Mocha.

Getting the previous value for an argument from didReceiveArguments

With the move away from component lifecycle hooks towards using element modifiers, I am investigating how I can port some code over. The existing code uses an addon called ember-did-change-attrs that effectively wraps Component.didReceiveAttrs and is called when the component attributes changed, similarly to what the class modifier is doing with didReceiveArguments.

However with that package it also provides a changes argument that contains both the old and the new argument values so that they can be compared etc.

How can I access the old/previous value within didReceiveArguments? Is this something that could be added to the behavior of the modifier?

edit: I realize I can just track the value manually in the modifier class (e.g. Modifier.lastValue), but was curious if there's another way?

Stop retrieving owner from the factory argument in class modifier manager `createFactory` method

This [destructuring the owner in class/modifier-manager.ts] is kinda wild, we should not be getting the owner from this argument πŸ€”

See the modifier manager API RFC https://github.com/emberjs/rfcs/blob/master/text/0373-Element-Modifier-Managers.md#createmodifier, which says:

The first argument passed to createModifier is the result returned from the factoryFor API. It contains a class property, which gives you the the raw class (the default export from app/modifiers/foo.js) and a create function that can be used to instantiate the class with any registered injections, merging them with any additional properties that are passed.


Since this is a carry over from existing implementation I think an inline comment with a link to an issue is fine for now. Ember should also be updated to not pass the owner like this (though that will have to go through a deprecation cycle).

cc @pzuraq

Originally posted by @rwjblue in #23

[3.2.0]+ `state.teardown is not a function`

After updating from 3.1.0 to 3.2.1, I get a runtime exception:

Uncaught (in promise) TypeError: state.teardown is not a function
    at FunctionBasedModifierManager.destroyModifier (modifier-manager.js:75:1)

My modifier is pretty simple:

import { set } from '@ember/object';
import { modifier } from '@glint/environment-ember-loose/ember-modifier';
import type { ExtractFieldsOfType } from '...';

const refModifier = modifier(<T, U extends ExtractFieldsOfType<T, HTMLElement>>(element: Element & T[U], [context, property]: [T, U]) =>
  set(context, property, element),
);

export default refModifier;

The state variable in destroyModifier is:

element: a#ember322.ember-view
instance: (element, _ref) => {…}
teardown: a#ember322.ember-view
[[Prototype]]: Object

I.e. it has teardown but it's definitely not a function but rather some element. Any ideas?

modifier is called infinitely on Safari

I have a modifier like this:

import { modifier } from 'ember-modifier';

export function resizeObserver(
  element,
  [onResize],
  { hasResizeObserver = 'ResizeObserver' in window }
) {
  // Note: The resize observer will also run initially
  // If it is not supported, we only call it once, 
  // but it will not trigger on actual resizing
  // Note: This can be overwritten for tests
  if (!hasResizeObserver) {
    let width = Math.floor(element.offsetWidth);
    onResize({ width, element, runCount: 0 });
    return;
  }

  let runCount = 0;
  let previousWidth;
  let resizeObserver = new ResizeObserver(() => {
    let width = Math.floor(element.offsetWidth);

    if (width === previousWidth) {
      return;
    }

    onResize({ width, element, runCount });
    runCount++;
    previousWidth = width;
  });

  resizeObserver.observe(element);

  return () => resizeObserver.disconnect();
}

export default modifier(resizeObserver);

Used like this:

<FTable
  @data={{this.sortedData}}
  {{resize-observer this.onResizeTable}}
>

On Safari, this ended up calling the modifier function infinitely, completely crashing the page.
I checked a bit, and it called the function with the same element, all the time until Ember threw an "Infinite rendering invalidation...." error.

On Chrome & Firefox, it worked as expected (only calling the method once).
Not sure if I'm doing anything wrong here, but I can't really see any other way to do this.
I ended up adding a WeakMap to hold the element and comparing that, but I guess that's not how it's supposed to work?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.