Skip to content

Instantly share code, notes, and snippets.

@mixonic
Last active April 21, 2021 23:49
Show Gist options
  • Save mixonic/acbc9b8d57819e2f2c77b206ab7f422e to your computer and use it in GitHub Desktop.
Save mixonic/acbc9b8d57819e2f2c77b206ab7f422e to your computer and use it in GitHub Desktop.
Services for Glimmer.js

Setup

In config/environment.js:

// config/environment.js
'use strict';

/*
 * Mostly this is the stock module config.
 */
const moduleConfiguration = {
  types: {
    application: { definitiveCollection: 'main' },
    component: { definitiveCollection: 'components' },
    'component-test': { unresolvable: true },
    helper: { definitiveCollection: 'components' },
    'helper-test': { unresolvable: true },
    renderer: { definitiveCollection: 'main' },
    template: { definitiveCollection: 'components' },
    /*
     * Add service as a type.
     */
    service: { definitiveCollection: 'services' }
  },
  collections: {
    main: {
      types: ['application', 'renderer']
    },
    components: {
      group: 'ui',
      types: ['component', 'component-test', 'template', 'helper', 'helper-test'],
      defaultType: 'component',
      privateCollections: ['utils']
    },
    styles: {
      group: 'ui',
      unresolvable: true
    },
    utils: {
      unresolvable: true
    },
    /*
     * Add services as a collection.
     */
    services: {
      types: ['service'],
      defaultType: 'service',
      privateCollections: ['utils']
    }
  }
};

module.exports = function(environment) {
  let ENV = {
    modulePrefix: 'bodega-glimmer',
    environment: environment,
    /*
     * Pass the module config here.
     */
    moduleConfiguration
  };

  return ENV;
};

Usage

// src/ui/components/apple-pay-button/component.ts
import Online from '../../../services/online';
import Component, { tracked } from '@glimmer/component';
import trackService from '../../../utils/tracked';

/*
 * Use `trackService` to both inject the dependency and
 * allow it to dirty the property.
 */
@trackService('online')
export default class ApplePayButton extends Component {
  /*
   * Define the property and type.
   */
  online: Online;

  /*
   * Track the service like any other property.
   */
  @tracked('online', 'args')
  get isDisabled() {
    return !this.online.isOnline || this.args.disabled;
  }
};
// src/ui/services/apple-pay.ts
import Service from './-utils/service';
import { tracked } from '@glimmer/component';

export default class ApplePay extends Service {

  /*
   * If you use this property in a template, ala `{{applePay.tracked}}`,
   * you will want to `@tracked` it. However you don't need this for
   * dirtying the service where it is injected.
   */
  @tracked isAvailable = false;

  constructor(options) {
    super(options);
    if (self.Stripe && self.Stripe.applePay) {
      self.Stripe.applePay.checkAvailability((result) => {
        /*
         * Set the property like normal.
         */
        this.isAvailable = result;
        
        /*
         * This dirties all properties where this service is injected.
         */
        this.notify();
      });
    }
  }
}
// src/services/-utils/service.ts
import { REGISTER_TRACKING } from '../../utils/tracked';
export default class Service {
_tracking = [];
static create(options) {
return new this(options);
}
constructor(options) {
Object.assign(this, options);
}
[REGISTER_TRACKING](instance) {
this._tracking.push(instance);
}
notify() {
this._tracking.forEach(t => t());
}
}
// src/utils/tracked.ts
import { tracked } from '@glimmer/component';
import { getOwner } from '@glimmer/di';
export const REGISTER_TRACKING = Symbol('register-component');
/*
* Lifted from https://github.com/shahata/dasherize/blob/master/index.js
*/
function dashCase(str) {
return str.replace(/[A-Z](?:(?=[^A-Z])|[A-Z]*(?=[A-Z][^A-Z]|$))/g, function (s, i) {
return (i > 0 ? '-' : '') + s.toLowerCase();
});
}
export default function trackService(propertyWithService): any {
return (baseConstructor) => {
let propertyCache = Symbol('property-cache');
class SubClassWithInjection extends baseConstructor {
@tracked
get [propertyWithService]() {
if (!this[propertyCache]) {
let service = getOwner(this).lookup(`service:${dashCase(propertyWithService)}`);
if (service[REGISTER_TRACKING]) {
service[REGISTER_TRACKING](() => this[propertyWithService] = service);
}
this[propertyCache] = service;
}
return this[propertyCache];
}
set [propertyWithService](value) {
this[propertyCache] = value;
}
}
return SubClassWithInjection;
}
}
@mike-north
Copy link

Note for consumers of this code, when creating derived properties, you should (and can only) state the service its self as a dependency. Declaring dependencies on properties within the service (i.e., @tracked('applePay.isAvailable')) will not work the way Ember.computed does.

Also, for those interested in injecting multiple services without making their inheritance chain longer with each injection, here's a class decorator that'll allow use like this

import Component, { tracked } from '@glimmer/component';

import injectServices from '../../../utils/inject-services';

import Auth from '../../../services/auth';
import Store from '../../../services/store';
import Router from '../../../services/router';

@injectServices('auth', 'store', 'router')
class MyComponent extends Component {
  store: Store;
  router: Router;
  auth: Auth;
}
import { tracked } from '@glimmer/component';
import { getOwner } from '@glimmer/di';

export const REGISTER_TRACKING = Symbol('register-component');

const dasherize = (str: string) => str.replace(
  /[A-Z](?:(?=[^A-Z])|[A-Z]*(?=[A-Z][^A-Z]|$))/g,
  (s, i) => (i > 0 ? '-' : '') + s.toLowerCase()
);

const PROPERTY_CACHE = Symbol('injection-cache');

export default function injectServices(...properties: string[]): any {
  return (baseConstructor: any) => {
    class SubClassWithInjections extends baseConstructor { }

    let pdm: PropertyDescriptorMap = properties.reduce((acc, item) => {
      let pd: PropertyDescriptor = {
        get() {
          let self: any = this;
          if (!self[PROPERTY_CACHE]) {
            self[PROPERTY_CACHE] = {};
          }
          let resolved = self[PROPERTY_CACHE][item];
          if (!resolved) {
            resolved = getOwner(this).lookup(`service:${dasherize(item)}`);
            if (resolved[REGISTER_TRACKING]) {
              resolved[REGISTER_TRACKING](() => {
                self[item] = resolved;
              });
            }
            self[PROPERTY_CACHE][item] = resolved;
          }
          return resolved;
        },
        set(value) {
          let self: any = this;
          if (!self[PROPERTY_CACHE]) {
            self[PROPERTY_CACHE] = {};
          }
          self[PROPERTY_CACHE][item] = value;
        }
      };
      return Object.assign({
        [`${item}`]: tracked(SubClassWithInjections.prototype, item, pd)
      }, acc);
    }, {});

    Object.defineProperties(SubClassWithInjections.prototype, pdm);

    return SubClassWithInjections;
  };
}

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