chore: remove LionSingleton

This commit is contained in:
Ahmet Yesil 2020-08-04 13:42:20 +02:00 committed by Thomas Allmer
parent d5517f2dee
commit 9ecab4d5b2
16 changed files with 70 additions and 243 deletions

View file

@ -0,0 +1,9 @@
---
'@lion/ajax': minor
'@lion/core': minor
'@lion/icon': minor
'@lion/localize': minor
---
Removing LionSingleton as es modules are already guaranteed to be singletons.
This reduces complexity and means less code to ship to our users.

View file

@ -91,7 +91,7 @@ This prefix should be stripped before parsing the string as JSON.
Pass the prefix with the `jsonPrefix` option.
```js
const myAjax = AjaxClass.getNewInstance({ jsonPrefix: ")]}'," });
const myAjax = new AjaxClass({ jsonPrefix: ")]}'," });
myAjax
.get('./packages/ajax/docs/assets/data.json')
.then(response => {
@ -110,7 +110,7 @@ Add additional headers to the requests with the `headers` option.
export const additionalHeaders = () => html`
<button
@click=${() => {
const myAjax = AjaxClass.getNewInstance({ headers: { 'MY-HEADER': 'SOME-HEADER-VALUE' } });
const myAjax = new AjaxClass({ headers: { 'MY-HEADER': 'SOME-HEADER-VALUE' } });
myAjax
.get('./packages/ajax/docs/assets/data.json')
.then(response => {
@ -136,7 +136,7 @@ It is possible to make an Ajax request cancelable, and then call `cancel()` to m
export const cancelableRequests = () => html`
<button
@click=${() => {
const myAjax = AjaxClass.getNewInstance({ cancelable: true });
const myAjax = new AjaxClass({ cancelable: true });
requestAnimationFrame(() => {
myAjax.cancel('too slow');
});
@ -163,7 +163,7 @@ You can cancel concurrent requests with the `cancelPreviousOnNewRequest` option.
export const cancelConcurrentRequests = () => html`
<button
@click=${() => {
const myAjax = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
const myAjax = new AjaxClass({ cancelPreviousOnNewRequest: true });
myAjax
.get('./packages/ajax/docs/assets/data.json')
.then(response => {

View file

@ -1,5 +1,4 @@
import { axios } from '@bundled-es-modules/axios';
import { LionSingleton } from '@lion/core';
import {
cancelInterceptorFactory,
cancelPreviousOnNewRequestInterceptorFactory,
@ -11,7 +10,7 @@ import { jsonPrefixTransformerFactory } from './transformers.js';
* `AjaxClass` creates the singleton instance {@link:ajax}. It is a promise based system for
* fetching data, based on [axios](https://github.com/axios/axios).
*/
export class AjaxClass extends LionSingleton {
export class AjaxClass {
/**
* @property {Object} proxy the axios instance that is bound to the AjaxClass instance
*/
@ -28,8 +27,6 @@ export class AjaxClass extends LionSingleton {
* @param {string} config.cancelPreviousOnNewRequest prevents concurrent requests
*/
constructor(config) {
super();
this.__config = {
lang: document.documentElement.getAttribute('lang'),
languageHeader: true,

View file

@ -4,7 +4,7 @@ import { AjaxClass } from './AjaxClass.js';
/**
* @typedef {ajax} ajax the global instance for handling all ajax requests
*/
export let ajax = singletonManager.get('@lion/ajax::ajax::0.3.x') || AjaxClass.getInstance(); // eslint-disable-line import/no-mutable-exports
export let ajax = singletonManager.get('@lion/ajax::ajax::0.3.x') || new AjaxClass(); // eslint-disable-line import/no-mutable-exports
/**
* setAjax allows the Application Developer to override the globally used instance of {@link:ajax}.

View file

@ -6,6 +6,10 @@ import { AjaxClass } from '../src/AjaxClass.js';
describe('AjaxClass interceptors', () => {
let server;
function getInstance(cfg) {
return new AjaxClass(cfg);
}
beforeEach(() => {
server = sinon.fakeServer.create({ autoRespond: true });
});
@ -24,8 +28,8 @@ describe('AjaxClass interceptors', () => {
this[type] = [...this[type], myInterceptor];
}
}
const ajaxWithout = AjaxClass.getNewInstance();
const ajaxWith = MyApi.getNewInstance();
const ajaxWithout = getInstance();
const ajaxWith = new MyApi();
expect(ajaxWithout[type]).to.not.include(myInterceptor);
expect(ajaxWith[type]).to.include(myInterceptor);
});
@ -34,8 +38,8 @@ describe('AjaxClass interceptors', () => {
it('can be added per instance without changing the class', () => {
['requestInterceptors', 'responseInterceptors'].forEach(type => {
const myInterceptor = () => {};
const ajaxWithout = AjaxClass.getNewInstance();
const ajaxWith = AjaxClass.getNewInstance();
const ajaxWithout = getInstance();
const ajaxWith = getInstance();
ajaxWith[type].push(myInterceptor);
expect(ajaxWithout[type]).to.not.include(myInterceptor);
expect(ajaxWith[type]).to.include(myInterceptor);
@ -53,7 +57,7 @@ describe('AjaxClass interceptors', () => {
const myInterceptor = sinon.spy(foo => foo);
const ajax = AjaxClass.getNewInstance();
const ajax = getInstance();
ajax[type].push(myInterceptor);
await ajax.get('data.json');
@ -68,7 +72,7 @@ describe('AjaxClass interceptors', () => {
it('has access to provided instance config(options) on requestInterceptors', async () => {
server.respondWith('GET', 'data.json', [200, { 'Content-Type': 'application/json' }, '{}']);
const ajax = AjaxClass.getNewInstance();
const ajax = getInstance();
ajax.options.myCustomValue = 'foo';
let customValueAccess = false;
const myInterceptor = config => {
@ -94,7 +98,7 @@ describe('AjaxClass interceptors', () => {
'{ "method": "put" }',
]);
const enforcePutInterceptor = config => ({ ...config, method: 'PUT' });
const myAjax = AjaxClass.getNewInstance();
const myAjax = getInstance();
myAjax.requestInterceptors.push(enforcePutInterceptor);
const response = await myAjax.post('data.json');
expect(response.data).to.deep.equal({ method: 'put' });
@ -112,7 +116,7 @@ describe('AjaxClass interceptors', () => {
...response,
data: { ...response.data, foo: 'bar' },
});
const myAjax = AjaxClass.getNewInstance();
const myAjax = getInstance();
myAjax.responseInterceptors.push(addDataInterceptor);
const response = await myAjax.get('data.json');
expect(response.data).to.deep.equal({ method: 'get', foo: 'bar' });

View file

@ -7,6 +7,10 @@ import { ajax } from '../src/ajax.js';
describe('AjaxClass', () => {
let server;
function getInstance(cfg) {
return new AjaxClass(cfg);
}
beforeEach(() => {
server = sinon.fakeServer.create({ autoRespond: true });
});
@ -16,16 +20,16 @@ describe('AjaxClass', () => {
});
it('sets content type json if passed an object', async () => {
const myAjax = AjaxClass.getNewInstance();
const myAjax = getInstance();
server.respondWith('POST', /\/api\/foo/, [200, { 'Content-Type': 'application/json' }, '']);
await myAjax.post('/api/foo', { a: 1, b: 2 });
expect(server.requests[0].requestHeaders['Content-Type']).to.include('application/json');
});
describe('AjaxClass.getNewInstance({ jsonPrefix: "%prefix%" })', () => {
describe('AjaxClass({ jsonPrefix: "%prefix%" })', () => {
it('adds new transformer to responseDataTransformers', () => {
const myAjaxWithout = AjaxClass.getNewInstance({ jsonPrefix: '' });
const myAjaxWith = AjaxClass.getNewInstance({ jsonPrefix: 'prefix' });
const myAjaxWithout = getInstance({ jsonPrefix: '' });
const myAjaxWith = getInstance({ jsonPrefix: 'prefix' });
const lengthWithout = myAjaxWithout.responseDataTransformers.length;
const lengthWith = myAjaxWith.responseDataTransformers.length;
expect(lengthWith - lengthWithout).to.eql(1);
@ -38,7 +42,7 @@ describe('AjaxClass', () => {
'for(;;);{"success":true}',
]);
const myAjax = AjaxClass.getNewInstance({ jsonPrefix: 'for(;;);' });
const myAjax = getInstance({ jsonPrefix: 'for(;;);' });
const response = await myAjax.get('data.json');
expect(response.status).to.equal(200);
expect(response.data.success).to.equal(true);
@ -47,24 +51,24 @@ describe('AjaxClass', () => {
it('works with non-JSON responses', async () => {
server.respondWith('GET', 'data.txt', [200, { 'Content-Type': 'text/plain' }, 'some text']);
const myAjax = AjaxClass.getNewInstance({ jsonPrefix: 'for(;;);' });
const myAjax = getInstance({ jsonPrefix: 'for(;;);' });
const response = await myAjax.get('data.txt');
expect(response.status).to.equal(200);
expect(response.data).to.equal('some text');
});
});
describe('AjaxClass.getNewInstance({ cancelable: true })', () => {
describe('AjaxClass({ cancelable: true })', () => {
it('adds new interceptor to requestInterceptors', () => {
const myAjaxWithout = AjaxClass.getNewInstance();
const myAjaxWith = AjaxClass.getNewInstance({ cancelable: true });
const myAjaxWithout = getInstance();
const myAjaxWith = getInstance({ cancelable: true });
const lengthWithout = myAjaxWithout.requestInterceptors.length;
const lengthWith = myAjaxWith.requestInterceptors.length;
expect(lengthWith - lengthWithout).to.eql(1);
});
it('allows to cancel single running requests', async () => {
const myAjax = AjaxClass.getNewInstance({ cancelable: true });
const myAjax = getInstance({ cancelable: true });
setTimeout(() => {
myAjax.cancel('is cancelled');
@ -79,7 +83,7 @@ describe('AjaxClass', () => {
});
it('allows to cancel multiple running requests', async () => {
const myAjax = AjaxClass.getNewInstance({ cancelable: true });
const myAjax = getInstance({ cancelable: true });
let cancelCount = 0;
setTimeout(() => {
@ -102,7 +106,7 @@ describe('AjaxClass', () => {
});
it('does not cancel resolved requests', async () => {
const myAjax = AjaxClass.getNewInstance({ cancelable: true });
const myAjax = getInstance({ cancelable: true });
server.respondWith('GET', 'data.json', [
200,
{ 'Content-Type': 'application/json' },
@ -119,17 +123,17 @@ describe('AjaxClass', () => {
});
});
describe('AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true })', () => {
describe('AjaxClass({ cancelPreviousOnNewRequest: true })', () => {
it('adds new interceptor to requestInterceptors', () => {
const myAjaxWithout = AjaxClass.getNewInstance();
const myAjaxWith = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
const myAjaxWithout = getInstance();
const myAjaxWith = getInstance({ cancelPreviousOnNewRequest: true });
const lengthWithout = myAjaxWithout.requestInterceptors.length;
const lengthWith = myAjaxWith.requestInterceptors.length;
expect(lengthWith - lengthWithout).to.eql(1);
});
it('automatically cancels previous running request', async () => {
const myAjax = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
const myAjax = getInstance({ cancelPreviousOnNewRequest: true });
server.respondWith('GET', 'data.json', [
200,
{ 'Content-Type': 'application/json' },
@ -157,7 +161,7 @@ describe('AjaxClass', () => {
});
it('automatically cancels multiple previous requests to the same endpoint', async () => {
const myAjax = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
const myAjax = getInstance({ cancelPreviousOnNewRequest: true });
server.respondWith('GET', 'data.json', [
200,
{ 'Content-Type': 'application/json' },
@ -189,7 +193,7 @@ describe('AjaxClass', () => {
});
it('automatically cancels multiple previous requests to different endpoints', async () => {
const myAjax = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
const myAjax = getInstance({ cancelPreviousOnNewRequest: true });
server.respondWith('GET', 'data.json', [
200,
{ 'Content-Type': 'application/json' },
@ -221,7 +225,7 @@ describe('AjaxClass', () => {
});
it('does not automatically cancel requests made via generic ajax', async () => {
const myAjax = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
const myAjax = getInstance({ cancelPreviousOnNewRequest: true });
server.respondWith('GET', 'data.json', [
200,
{ 'Content-Type': 'application/json' },
@ -257,8 +261,8 @@ describe('AjaxClass', () => {
});
it('does not automatically cancel requests made via other instances', async () => {
const myAjax1 = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
const myAjax2 = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
const myAjax1 = getInstance({ cancelPreviousOnNewRequest: true });
const myAjax2 = getInstance({ cancelPreviousOnNewRequest: true });
server.respondWith('GET', 'data.json', [
200,
{ 'Content-Type': 'application/json' },

View file

@ -6,6 +6,10 @@ import { AjaxClass } from '../src/AjaxClass.js';
describe('AjaxClass transformers', () => {
let server;
function getInstance(cfg) {
return new AjaxClass(cfg);
}
beforeEach(() => {
server = sinon.fakeServer.create({ autoRespond: true });
});
@ -24,18 +28,18 @@ describe('AjaxClass transformers', () => {
this[type] = [...this[type], myInterceptor];
}
}
const ajaxWithout = AjaxClass.getNewInstance();
const ajaxWith = MyApi.getNewInstance();
const ajaxWithout = getInstance();
const ajaxWith = new MyApi();
expect(ajaxWithout[type]).to.not.include(myInterceptor);
expect(ajaxWith[type]).to.include(myInterceptor);
});
});
it('can be added per instance withour changing the class', () => {
it('can be added per instance without changing the class', () => {
['requestDataTransformers', 'responseDataTransformers'].forEach(type => {
const myInterceptor = () => {};
const ajaxWithout = AjaxClass.getNewInstance();
const ajaxWith = AjaxClass.getNewInstance();
const ajaxWithout = getInstance();
const ajaxWith = getInstance();
ajaxWith[type].push(myInterceptor);
expect(ajaxWithout[type]).to.not.include(myInterceptor);
expect(ajaxWith[type]).to.include(myInterceptor);
@ -53,7 +57,7 @@ describe('AjaxClass transformers', () => {
const myTransformer = sinon.spy(foo => foo);
const ajax = AjaxClass.getNewInstance();
const ajax = getInstance();
ajax[type].push(myTransformer);
await ajax.get('data.json');
@ -75,7 +79,7 @@ describe('AjaxClass transformers', () => {
'{ "method": "post" }',
]);
const addBarTransformer = data => ({ ...data, bar: 'bar' });
const myAjax = AjaxClass.getNewInstance();
const myAjax = getInstance();
myAjax.requestDataTransformers.push(addBarTransformer);
const response = await myAjax.post('data.json', { foo: 'foo' });
expect(JSON.parse(response.config.data)).to.deep.equal({
@ -93,7 +97,7 @@ describe('AjaxClass transformers', () => {
'{ "method": "get" }',
]);
const addBarTransformer = data => ({ ...data, bar: 'bar' });
const myAjax = AjaxClass.getNewInstance();
const myAjax = getInstance();
myAjax.responseDataTransformers.push(addBarTransformer);
const response = await myAjax.get('data.json');
expect(response.data).to.deep.equal({ method: 'get', bar: 'bar' });

View file

@ -53,7 +53,6 @@ export { dedupeMixin } from '@open-wc/dedupe-mixin';
export { DelegateMixin } from './src/DelegateMixin.js';
export { DisabledMixin } from './src/DisabledMixin.js';
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js';
export { LionSingleton } from './src/LionSingleton.js';
export { SlotMixin } from './src/SlotMixin.js';
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
export { browserDetection } from './src/browserDetection.js';

View file

@ -1,51 +0,0 @@
/**
* 'LionSingleton' provides an instance of the given class via .getInstance(foo, bar) and will
* return the same instance if already created. It can reset its instance so a new one will be
* created via .resetInstance() and can at any time add mixins via .addInstanceMixin().
* It can provide new instances (with applied Mixins) via .getNewInstance().
*/
export class LionSingleton {
/**
* @param {function} mixin
*/
static addInstanceMixin(mixin) {
if (!this.__instanceMixins) {
/** @type {function[]} */
this.__instanceMixins = [];
}
this.__instanceMixins.push(mixin);
}
/**
* @param {...*} args
* @returns {LionSingleton}
*/
static getNewInstance(...args) {
let Klass = this;
if (Array.isArray(this.__instanceMixins)) {
this.__instanceMixins.forEach(mixin => {
Klass = mixin(Klass);
});
}
// Ignoring, because it's up to the extension layer to accept arguments in its constructor
// @ts-ignore-next-line
return new Klass(...args);
}
/**
* @param {...*} args
* @returns {*}
*/
static getInstance(...args) {
if (this.__instance) {
return this.__instance;
}
this.__instance = this.getNewInstance(...args);
return this.__instance;
}
static resetInstance() {
this.__instance = undefined;
}
}

View file

@ -1,126 +0,0 @@
import { expect } from '@open-wc/testing';
import { LionSingleton } from '../src/LionSingleton.js';
describe('LionSingleton', () => {
it('provides an instance of the given class via .getInstance()', async () => {
class MySingleton extends LionSingleton {}
const mySingleton = MySingleton.getInstance();
expect(mySingleton).to.be.an.instanceOf(MySingleton);
});
it('supports parameters for .getInstance(foo, bar)', async () => {
class MySingleton extends LionSingleton {
/**
*
* @param {string} foo
* @param {string} bar
*/
constructor(foo, bar) {
super();
this.foo = foo;
this.bar = bar;
}
}
const mySingleton = MySingleton.getInstance('isFoo', 'isBar');
expect(mySingleton).to.deep.equal({
foo: 'isFoo',
bar: 'isBar',
});
});
it('will return the same instance if already created', async () => {
let counter = 0;
class MySingleton extends LionSingleton {
constructor() {
super();
counter += 1;
}
}
const mySingleton = MySingleton.getInstance();
mySingleton.foo = 'bar';
expect(mySingleton).to.deep.equal(MySingleton.getInstance());
expect(counter).to.equal(1);
expect(new MySingleton()).to.not.deep.equal(MySingleton.getInstance());
});
it('can reset its instance so a new one will be created via .resetInstance()', async () => {
let counter = 0;
class MySingleton extends LionSingleton {
constructor() {
super();
counter += 1;
}
}
const mySingleton = MySingleton.getInstance();
mySingleton.foo = 'bar';
expect(mySingleton.foo).to.equal('bar');
expect(counter).to.equal(1);
MySingleton.resetInstance();
const updatedSingleton = MySingleton.getInstance();
expect(updatedSingleton.foo).to.be.undefined;
expect(counter).to.equal(2);
});
it('can at any time add mixins via .addInstanceMixin()', () => {
// @ts-ignore because we're getting rid of LionSingleton altogether
const MyMixin = superclass =>
class extends superclass {
constructor() {
super();
this.myMixin = true;
}
};
class MySingleton extends LionSingleton {}
MySingleton.addInstanceMixin(MyMixin);
const mySingleton = MySingleton.getInstance();
expect(mySingleton.myMixin).to.be.true;
// @ts-ignore because we're getting rid of LionSingleton altogether
const OtherMixin = superclass =>
class extends superclass {
constructor() {
super();
this.otherMixin = true;
}
};
MySingleton.addInstanceMixin(OtherMixin);
expect(mySingleton.otherMixin).to.be.undefined;
MySingleton.resetInstance();
const updatedSingleton = MySingleton.getInstance();
expect(updatedSingleton.myMixin).to.be.true;
expect(updatedSingleton.otherMixin).to.be.true;
});
it('can provide new instances (with applied Mixins) via .getNewInstance()', async () => {
// @ts-ignore because we're getting rid of LionSingleton altogether
const MyMixin = superclass =>
class extends superclass {
constructor() {
super();
this.myMixin = true;
}
};
class MySingleton extends LionSingleton {}
MySingleton.addInstanceMixin(MyMixin);
const singletonOne = MySingleton.getNewInstance();
// @ts-ignore because we're getting rid of LionSingleton altogether
singletonOne.one = true;
// @ts-ignore because we're getting rid of LionSingleton altogether
expect(singletonOne.myMixin).to.be.true;
// @ts-ignore because we're getting rid of LionSingleton altogether
expect(singletonOne.one).to.be.true;
const singletonTwo = MySingleton.getNewInstance();
// @ts-ignore because we're getting rid of LionSingleton altogether
expect(singletonTwo.myMixin).to.be.true;
// @ts-ignore because we're getting rid of LionSingleton altogether
expect(singletonTwo.one).to.be.undefined;
// @ts-ignore because we're getting rid of LionSingleton altogether
expect(singletonOne.one).to.be.true; // to be sure
});
});

View file

@ -1,9 +1,5 @@
import { LionSingleton } from '@lion/core';
export class IconManager extends LionSingleton {
constructor(params = {}) {
super(params);
export class IconManager {
constructor() {
this.__iconResolvers = new Map();
}

View file

@ -2,7 +2,7 @@ import { singletonManager } from 'singleton-manager';
import { IconManager } from './IconManager.js';
// eslint-disable-next-line import/no-mutable-exports
export let icons = singletonManager.get('@lion/icon::icons::0.5.x') || IconManager.getInstance();
export let icons = singletonManager.get('@lion/icon::icons::0.5.x') || new IconManager();
export function setIcons(newIcons) {
icons = newIcons;

View file

@ -1,5 +1,4 @@
import MessageFormat from '@bundled-es-modules/message-format/MessageFormat.js';
import { LionSingleton } from '@lion/core';
import isLocalizeESModule from './isLocalizeESModule.js';
/**
@ -9,10 +8,9 @@ import isLocalizeESModule from './isLocalizeESModule.js';
/**
* `LocalizeManager` manages your translations (includes loading)
*/
export class LocalizeManager extends LionSingleton {
export class LocalizeManager {
// eslint-disable-line no-unused-vars
constructor({ autoLoadOnLocaleChange = false, fallbackLocale = '' } = {}) {
super();
this.__delegationTarget = document.createDocumentFragment();
this._autoLoadOnLocaleChange = !!autoLoadOnLocaleChange;
this._fallbackLocale = fallbackLocale;

View file

@ -5,7 +5,7 @@ import { LocalizeManager } from './LocalizeManager.js';
// eslint-disable-next-line import/no-mutable-exports
export let localize =
singletonManager.get('@lion/localize::localize::0.10.x') ||
LocalizeManager.getInstance({
new LocalizeManager({
autoLoadOnLocaleChange: true,
fallbackLocale: 'en-GB',
});

View file

@ -643,8 +643,7 @@ describe('When supporting external translation tools like Google Translate', ()
* @returns {LocalizeManager}
*/
function getInstance(cfg) {
LocalizeManager.resetInstance();
return LocalizeManager.getInstance(cfg || {});
return new LocalizeManager(cfg || {});
}
afterEach(() => {

View file

@ -1,7 +1,5 @@
import { expect } from '@open-wc/testing';
import sinon from 'sinon';
import { LionSingleton } from '@lion/core';
import { LocalizeManager } from '../src/LocalizeManager.js';
import { localize, setLocalize } from '../src/localize.js';
@ -13,10 +11,6 @@ describe('localize', () => {
// we test newly created instances of this class separately
// this allows to avoid any side effects caused by changing singleton state between tests
it('is a singleton', () => {
expect(localize).to.be.an.instanceOf(LionSingleton);
});
it('is an instance of LocalizeManager', () => {
expect(localize).to.be.an.instanceOf(LocalizeManager);
});