npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ember-class-based-modifier

v0.10.0

Published

A class-based API for authoring element modifiers in Ember.

Downloads

8,698

Readme

ember-class-based-modifier

Build Status

This is the next iteration of ember-oo-modifiers with some breaking changes to the API. If you are looking for the previous documentation, select the version you are using in the tags dropdown at the top of the page. For a list of API differences, see here

This addon provides a class-based API for authoring element modifiers in Ember, similar to the class-based helper API.

Compatibility

This is currently compatible with:

  • Ember.js v2.18 or above
  • Ember CLI v2.13 or above

Installation

ember install ember-class-based-modifier

Usage

This addon does not provide any modifiers out of the box; instead, this library allows you to write your own.

Much of this addon was based on ember-oo-modifiers, and, in turn, ember-functional-modifiers.

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.

// app/modifiers/scroll-position.js

import Modifier from 'ember-class-based-modifier';

export default class ScrollPositionModifier extends Modifier {
  get scrollPosition() {
    // get the first positional argument passed to the modifier
    //
    // {{scoll-position @someNumber relative=@someBoolean}}
    //                  ~~~~~~~~~~~
    //
    return this.args.positional[0];
  }

  get isRelative() {
    // get the named argument "relative" passed to the modifier
    //
    // {{scoll-position @someNumber relative=@someBoolean}}
    //                                       ~~~~~~~~~~~~
    //
    return this.args.named.relative
  }

  didReceiveArguments() {
    if(this.isRelative) {
      this.element.scrollTop += this.scrollPosition;
    } else {
      this.element.scrollTop = this.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 the willRemove hook.

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 { action } from '@ember/object';
import Modifier from 'ember-class-based-modifier';

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

export default class MoveRandomlyModifier extends Modifier {
  setIntervalId = null;

  get delay() {
    // get the named argument "delay" passed to the modifier
    //
    // {{move-randomly delay=@someNumber}}
    //                       ~~~~~~~~~~~
    //
    return this.args.named.delay || DEFAULT_DELAY;
  }

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

  didReceiveArguments() {
    if (this.setIntervalId !== null) {
      clearInterval(this.setIntervalId);
    }

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

  willRemove() {
    clearInterval(this.setIntervalId);
    this.setIntervalId = null;
  }
}

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 { action } from '@ember/object';
import { inject as service } from '@ember/service';
import Modifier from 'ember-class-based-modifier';

export default class TrackClickModifier extends Modifier {
  @service metrics;

  get eventName() {
    // get the first positional argument passed to the modifier
    //
    // {{track-click "like-button-click" page="some page" title="some title"}}
    //               ~~~~~~~~~~~~~~~~~~~
    //
    return this.args.positional[0];
  }

  get options() {
    // get the named arguments passed to the modifier
    //
    // {{track-click "like-button-click" page="some page" title="some title"}}
    //                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    //
    return this.args.named;
  }

  @action onClick() {
    this.metrics.trackEvent(this.eventName, this.options);
  }

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

  willRemove() {
    this.element.removeEventListener('click', this.onClick, true);
  }
}

Usage:

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

Classic API

If you would like to use Ember.Object based APIs, such as this.get, this.set, this.setProperties, etc, you can import the "classic" base class instead, located at the import path ember-class-based-modifier/classic.

The examples above can be rewritten using the classic API:

// app/modifiers/scroll-position.js

import { computed } from '@ember/object';
import Modifier from 'ember-class-based-modifier/classic';

export default Modifier.extend({
  scrollPosition: computed('args.positional.[]', function() {
    // get the first positional argument passed to the modifier
    //
    // {{scoll-position @someNumber relative=@someBoolean}}
    //                  ~~~~~~~~~~~
    //
    return this.args.positional[0];
  }),

  isRelative: computed('args.named.relative', function() {
    // get the named argument "relative" passed to the modifier
    //
    // {{scoll-position @someNumber relative=@someBoolean}}
    //                                       ~~~~~~~~~~~~
    //
    return this.args.named.relative;
  }),

  didReceiveArguments() {
    if(this.isRelative) {
      this.element.scrollTop += this.scrollPosition;
    } else {
      this.element.scrollTop = this.scrollPosition;
    }
  }
});
// app/modifiers/move-randomly.js

import { action, computed } from '@ember/object';
import Modifier from 'ember-class-based-modifier/classic';

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

export default Modifier.extend({
  init() {
    this._super(...arguments);
    this.set('setIntervalId', null);
  },

  delay: computed('args.named.delay', function() {
    // get the named argument "delay" passed to the modifier
    //
    // {{move-randomly delay=@someNumber}}
    //                       ~~~~~~~~~~~
    //
    return this.args.named.delay || DEFAULT_DELAY;
  }),

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

  didReceiveArguments() {
    let setIntervalId = this.get('setIntervalId');

    if (setIntervalId !== null) {
      clearInterval(setIntervalId);
    }

    setIntervalId = setInterval(this.moveElement, this.get('delay'));

    this.set('setIntervalId', setIntervalId);
  },

  willRemove() {
    clearInterval(this.setIntervalId);
    this.setIntervalId = null;
  }
});
// app/modifiers/track-click.js

import { action, computed } from '@ember/object';
import { inject as service } from '@ember/service';
import Modifier from 'ember-class-based-modifier/classic';

export default Modifier.extend({
  metrics: service(),

  eventName: computed('args.positional.[]', function() {
    // get the first positional argument passed to the modifier
    //
    // {{track-click "like-button-click" page="some page" title="some title"}}
    //               ~~~~~~~~~~~~~~~~~~~
    //
    return this.args.positional[0];
  }),

  options: computed('args.named', function() {
    // get the named arguments passed to the modifier
    //
    // {{track-click "like-button-click" page="some page" title="some title"}}
    //                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    //
    return this.args.named;
  }),

  onClick: action(function() {
    this.metrics.trackEvent(this.get('eventName'), this.get('options'));
  }),

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

  willRemove() {
    this.element.removeEventListener('click', this.onClick, true);
  }
});

Whenever possible, it is recommended that you use the default "modern" API instead of the classic API.

TypeScript

Using the "modern" native class API, you can use .ts instead of .js and it'll just work, as long as you do runtime checks to narrow the types of your args when you access them.

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

export default class ScrollPositionModifier extends Modifier {
  // ...
}

But to avoid writing runtime checks, you can extend Modifier with your own args, similar to the way you would define your args for a Glimmer Component.

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

interface ScrollPositionModifierArgs {
  positional: [number],
  named: {
    relative: boolean
  }
}

export default class ScrollPositionModifier extends Modifier<ScrollPositionModifierArgs> {
  get scrollPosition(): number {
    // get the first positional argument passed to the modifier
    //
    // {{scoll-position @someNumber relative=@someBoolean}}
    //                  ~~~~~~~~~~~
    //
    return this.args.positional[0];
  }

  get isRelative(): boolean {
    // get the named argument "relative" passed to the modifier
    //
    // {{scoll-position @someNumber relative=@someBoolean}}
    //                                       ~~~~~~~~~~~~
    //
    return this.args.named.relative
  }

  didReceiveArguments() {
    if(this.isRelative) {
      this.element.scrollTop += this.scrollPosition;
    } else {
      this.element.scrollTop = this.scrollPosition;
    }
  }
}

See this pull request comment for a full discussion about using TypeScript with your Modifiers.

API

Lifecycle Summary

  • (#) Indicates the order of invocation for the lifecycle event.
  • ❌ Indicates that the method is not invoked for a given lifecycle / property is not available.
  • ✔️ Indicates that the property is available during the invocation of the given method.

API differences from ember-oo-modifiers

  • Renamed package to ember-class-based-modifier.
  • No Modifier.modifier() function.
  • Classic API is located at ember-class-based-modifier/classic.
  • Arguments, both positional and named, are available on this.args.
  • Named arguments do not become properties on the modifier instance.
  • Arguments are not passed to life-cycle hooks.
  • Renamed didInsertElement to didInstall and willDestroyElement to willRemove. This is to emphasize that when the modifier is installed or removed, the underlying element may not be freshly inserted or about to go away. Therefore, it is important to perform clean-up work in the willRemove to reverse any modifications you made to the element.
  • Changed life-cycle hook order: didReceiveArguments fires before didInstall, and didUpdateArguments fires before didReceiveArguments, mirroring the classic component life-cycle hooks ordering.
  • Added willDestroy, isDestroying and isDestroyed with the same semantics as Ember objects and Glimmer components.

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.