Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor module to include a configuration when calling #forRoot #77

Merged
merged 13 commits into from
Jun 10, 2021
Merged
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ After that, you can use the `ngx-skeleton-loader` components in your templates,

Also, you can import the module in your app by calling `NgxSkeletonLoaderModule.forRoot()` when adding it. So it will be available across your Angular application.

Importing the module this way also allows you to globally configure the default values for the `ngx-skeleton-loader` components in your application, in case you need some different default values for your app.

```typescript
...
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
Expand All @@ -98,7 +100,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
],
imports: [
...
NgxSkeletonLoaderModule.forRoot(),
NgxSkeletonLoaderModule.forRoot({ animation: 'pulse', loadingText: 'This item is actually loading...' }),
...
],
providers: [],
Expand All @@ -112,6 +114,7 @@ export class YourAppComponent {}
```html
<div class="item">
<ngx-skeleton-loader count="5" appearance="circle"></ngx-skeleton-loader>
<!-- above line will produce the rendering of 5 circles with the pulse animation and the aria-valuetext attribute set with "This item is actually loading..." -->
</div>
```

Expand All @@ -125,7 +128,8 @@ You can also define which appearance want to use in your skeleton loader by pass

### Options

- `''` - _default_: it will use it `''` as appearance. At the end, it will render like a line, but line is not a expected appearance to be passed;
- `''` - _default_: it will use it `''` as appearance. At the end, it will render like a line;
- `line`: it will render like a line. This is the same behavior as passing an empty string;
- `circle`: it will use `circle` as appearance. Great for avatar skeletons, for example :);

## Animations
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { InjectionToken } from '@angular/core';

export type NgxSkeletonLoaderConfigTheme = {
// This is required since ngStyle is using `any` as well
// More details in https://angular.io/api/common/NgStyle
// tslint:disable-next-line: no-any
[k: string]: any;
} | null;

export interface NgxSkeletonLoaderConfig {
HunteRoi marked this conversation as resolved.
Show resolved Hide resolved
appearance: 'circle' | 'line' | '';
animation: 'progress' | 'progress-dark' | 'pulse' | 'false' | false;
theme: NgxSkeletonLoaderConfigTheme;
loadingText: string;
count: number;
}

export const NGX_SKELETON_LOADER_CONFIG = new InjectionToken<NgxSkeletonLoaderConfig>('ngx-skeleton-loader.config');
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, PLATFORM_ID } from '@angular/core';
import { async as waitForAsync, TestBed } from '@angular/core/testing';
import { NGX_SKELETON_LOADER_CONFIG } from './ngx-skeleton-loader-config.types';

import { NgxSkeletonLoaderComponent } from './ngx-skeleton-loader.component';

Expand Down Expand Up @@ -59,6 +60,10 @@ import { NgxSkeletonLoaderComponent } from './ngx-skeleton-loader.component';
<ngx-skeleton-loader appearance="circle" [theme]="{ width: '70px', height: '70px', 'border-radius': '10px' }">
</ngx-skeleton-loader>
</div>

<div class="skeletons-with-provided-config">
<ngx-skeleton-loader></ngx-skeleton-loader>
</div>
</div>
`,
})
Expand All @@ -70,123 +75,147 @@ class ContainerComponent {
describe('NgxSkeletonLoaderComponent', () => {
// tslint:disable-next-line: no-any
let fixture: any;

beforeEach(
waitForAsync(() => {
spyOn(console, 'error');
fixture = TestBed.configureTestingModule({
declarations: [ContainerComponent, NgxSkeletonLoaderComponent],
providers: [{ provide: PLATFORM_ID, useValue: 'browser' }],
}).createComponent(ContainerComponent);
fixture.detectChanges();
}),
);

it('should console 3 errors if `animation`, `appearance` and `count` receives invalid options and is running in development mode', () => {
expect(console.error).toHaveBeenCalledTimes(3);
beforeEach(() => {
spyOn(console, 'error');
});

it('should console errors if `animation` is an invalid option and is running in development mode', () => {
expect(console.error).toHaveBeenCalledWith(
// tslint:disable-next-line: max-line-length
`\`NgxSkeletonLoaderComponent\` need to receive 'animation' as: progress, progress-dark, pulse, false. Forcing default to "progress".`,
describe('When the component uses default configuration', () => {
beforeEach(
waitForAsync(() => {
fixture = TestBed.configureTestingModule({
declarations: [ContainerComponent, NgxSkeletonLoaderComponent],
providers: [{ provide: PLATFORM_ID, useValue: 'browser' }],
}).createComponent(ContainerComponent);
fixture.detectChanges();
}),
);
});

it('should console errors if `count` is an invalid option and is running in development mode', () => {
expect(console.error).toHaveBeenCalledWith(
// tslint:disable-next-line: max-line-length
`\`NgxSkeletonLoaderComponent\` need to receive 'count' a numeric value. Forcing default to "1".`,
);
});
it('should console 3 errors if `animation`, `appearance` and `count` receives invalid options and is running in development mode', () => {
expect(console.error).toHaveBeenCalledTimes(3);
});

it('should console errors if `appearance` is an invalid option and is running in development mode', () => {
expect(console.error).toHaveBeenCalledWith(
// tslint:disable-next-line: max-line-length
`\`NgxSkeletonLoaderComponent\` need to receive 'appearance' as: circle or empty string. Forcing default to "''".`,
);
});
it('should console errors if `animation` is an invalid option and is running in development mode', () => {
expect(console.error).toHaveBeenCalledWith(
// tslint:disable-next-line: max-line-length
`\`NgxSkeletonLoaderComponent\` need to receive 'animation' as: progress, progress-dark, pulse, false. Forcing default to "progress".`,
);
});

it('should add all relevant WAI-ARIA `aria-` attributes in all ngx-skeleton-loader', () => {
expect(fixture.nativeElement.querySelectorAll('[aria-busy="true"]').length).toBe(14);
expect(fixture.nativeElement.querySelectorAll('[aria-valuemin="0"]').length).toBe(14);
expect(fixture.nativeElement.querySelectorAll('[aria-valuemax="100"]').length).toBe(14);
expect(fixture.nativeElement.querySelectorAll('[aria-valuetext]').length).toBe(14);
expect(fixture.nativeElement.querySelectorAll('[role="progressbar"]').length).toBe(14);
expect(fixture.nativeElement.querySelectorAll('[tabindex="0"]').length).toBe(14);
});
it('should console errors if `count` is an invalid option and is running in development mode', () => {
expect(console.error).toHaveBeenCalledWith(
// tslint:disable-next-line: max-line-length
`\`NgxSkeletonLoaderComponent\` need to receive 'count' a numeric value. Forcing default to "1".`,
);
});

it('should use progress as default animation if `animation` is not passed as component attribute', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-defaults .loader.progress').length).toBe(1);
});
it('should console errors if `appearance` is an invalid option and is running in development mode', () => {
expect(console.error).toHaveBeenCalledWith(
// tslint:disable-next-line: max-line-length
`\`NgxSkeletonLoaderComponent\` need to receive 'appearance' as: circle or line or empty string. Forcing default to "''".`,
);
});

describe('When skeleton is created using default settings', () => {
it('should render a single skeleton', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-defaults .loader').length).toBe(1);
it('should add all relevant WAI-ARIA `aria-` attributes in all ngx-skeleton-loader', () => {
expect(fixture.nativeElement.querySelectorAll('[aria-busy="true"]').length).toBe(15);
expect(fixture.nativeElement.querySelectorAll('[aria-valuemin="0"]').length).toBe(15);
expect(fixture.nativeElement.querySelectorAll('[aria-valuemax="100"]').length).toBe(15);
expect(fixture.nativeElement.querySelectorAll('[aria-valuetext]').length).toBe(15);
expect(fixture.nativeElement.querySelectorAll('[role="progressbar"]').length).toBe(15);
expect(fixture.nativeElement.querySelectorAll('[tabindex="0"]').length).toBe(15);
});
});

describe('When skeleton is created passing loading text to be used as WAI-ARIA `aria-valuetext`', () => {
it('should render a single skeleton', () => {
expect(fixture.nativeElement.querySelectorAll('[aria-valuetext="Loading. Please wait ..."]').length).toBe(1);
it('should use progress as default animation if `animation` is not passed as component attribute', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-defaults .loader.progress').length).toBe(1);
});
});

describe('When skeleton is created with count', () => {
it('should render skeleton based on given count attribute', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-with-count .loader').length).toBe(2);
describe('When skeleton is created using default settings', () => {
it('should render a single skeleton', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-defaults .loader').length).toBe(1);
});
});
});

describe('When skeleton is created with circle appearance', () => {
it('should add styles based on circle class on the skeleton components', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-appearance-circle .loader.circle').length).toBe(1);
describe('When skeleton is created passing loading text to be used as WAI-ARIA `aria-valuetext`', () => {
it('should render a single skeleton', () => {
expect(fixture.nativeElement.querySelectorAll('[aria-valuetext="Loading. Please wait ..."]').length).toBe(1);
});
});
});

describe('When skeleton is created without animation', () => {
it('should NOT add progress animation styles based on animation class on the skeleton components', () => {
expect(
fixture.nativeElement.querySelectorAll('.skeletons-animation-no-animation .loader:not(.animation)').length,
).toBe(1);
describe('When skeleton is created with count', () => {
it('should render skeleton based on given count attribute', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-with-count .loader').length).toBe(2);
});
});

it('should NOT add progress animation styles based on animation class if animation value is passed via binding', () => {
expect(
fixture.nativeElement.querySelectorAll('.skeletons-animation-no-animation-via-binding .loader:not(.animation)')
.length,
).toBe(1);
describe('When skeleton is created with circle appearance', () => {
it('should add styles based on circle class on the skeleton components', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-appearance-circle .loader.circle').length).toBe(1);
});
});
});

describe('When skeleton is created using `pulse` as animation', () => {
it('should add pulse animation styles based on animation class on the skeleton components', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-animation-pulse .loader.pulse').length).toBe(1);
describe('When skeleton is created without animation', () => {
it('should NOT add progress animation styles based on animation class on the skeleton components', () => {
expect(
fixture.nativeElement.querySelectorAll('.skeletons-animation-no-animation .loader:not(.animation)').length,
).toBe(1);
});

it('should NOT add progress animation styles based on animation class if animation value is passed via binding', () => {
expect(
fixture.nativeElement.querySelectorAll(
'.skeletons-animation-no-animation-via-binding .loader:not(.animation)',
).length,
).toBe(1);
});
});
});

describe('When skeleton is created using `progress-dark` as animation', () => {
it('should add progress-dark animation styles based on animation class on the skeleton components', () => {
expect(
fixture.nativeElement.querySelectorAll('.skeletons-animation-progress-dark .loader.progress-dark').length,
).toBe(1);
describe('When skeleton is created using `pulse` as animation', () => {
it('should add pulse animation styles based on animation class on the skeleton components', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-animation-pulse .loader.pulse').length).toBe(1);
});
});
});

describe('When skeleton is created using `progress` as animation', () => {
it('should add progress animation styles based on animation class on the skeleton components', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-animation-progress .loader.progress').length).toBe(1);
describe('When skeleton is created using `progress-dark` as animation', () => {
it('should add progress-dark animation styles based on animation class on the skeleton components', () => {
expect(
fixture.nativeElement.querySelectorAll('.skeletons-animation-progress-dark .loader.progress-dark').length,
).toBe(1);
});
});

describe('When skeleton is created using `progress` as animation', () => {
it('should add progress animation styles based on animation class on the skeleton components', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-animation-progress .loader.progress').length).toBe(1);
});
});

describe('When skeleton is created with theming', () => {
it('should render skeleton with styles based on theme attribute', () => {
const skeletonWithTheming = fixture.nativeElement.querySelector('.skeletons-with-theming .loader.circle')
.attributes as NamedNodeMap;

expect((skeletonWithTheming.getNamedItem('style') as Attr).value).toBe(
'width: 70px; height: 70px; border-radius: 10px;',
);
});
});
});

describe('When skeleton is created with theming', () => {
it('should render skeleton with styles based on theme attribute', () => {
const skeletonWithTheming = fixture.nativeElement.querySelector('.skeletons-with-theming .loader.circle')
.attributes as NamedNodeMap;
describe('When the component receives a different default via module configuration', () => {
beforeEach(
waitForAsync(() => {
fixture = TestBed.configureTestingModule({
declarations: [ContainerComponent, NgxSkeletonLoaderComponent],
providers: [
{ provide: PLATFORM_ID, useValue: 'browser' },
{ provide: NGX_SKELETON_LOADER_CONFIG, useValue: { appearance: 'circle', count: 3 } },
],
}).createComponent(ContainerComponent);
fixture.detectChanges();
}),
);

expect((skeletonWithTheming.getNamedItem('style') as Attr).value).toBe(
'width: 70px; height: 70px; border-radius: 10px;',
);
it('should render skeleton with the provided config', () => {
expect(fixture.nativeElement.querySelectorAll('.skeletons-with-provided-config .loader.circle').length).toBe(3);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ import {
ChangeDetectionStrategy,
OnChanges,
SimpleChanges,
Optional,
Inject,
} from '@angular/core';
import { start, end } from 'perf-marks/marks';
import {
NgxSkeletonLoaderConfig,
NgxSkeletonLoaderConfigTheme,
NGX_SKELETON_LOADER_CONFIG,
} from './ngx-skeleton-loader-config.types';

@Component({
selector: 'ngx-skeleton-loader',
Expand All @@ -24,24 +31,32 @@ export class NgxSkeletonLoaderComponent implements OnInit, AfterViewInit, OnDest
static ngAcceptInputType_animation: boolean | string;

@Input()
count = 1;
count: NgxSkeletonLoaderConfig['count'];
HunteRoi marked this conversation as resolved.
Show resolved Hide resolved

@Input()
loadingText = 'Loading...';
loadingText: NgxSkeletonLoaderConfig['loadingText'];

@Input()
appearance: 'circle' | '' = '';
appearance: NgxSkeletonLoaderConfig['appearance'];

@Input()
animation: 'progress' | 'progress-dark' | 'pulse' | 'false' | false = 'progress';
animation: NgxSkeletonLoaderConfig['animation'];

// This is required since ngStyle is using `any` as well
// More details in https://angular.io/api/common/NgStyle
// tslint:disable-next-line: no-any
@Input() theme: { [k: string]: any } = {};
@Input()
theme: NgxSkeletonLoaderConfigTheme;

// tslint:disable-next-line: no-any
items: Array<any> = [];
items: Array<any>;
HunteRoi marked this conversation as resolved.
Show resolved Hide resolved

constructor(@Inject(NGX_SKELETON_LOADER_CONFIG) @Optional() config?: NgxSkeletonLoaderConfig) {
const { appearance = 'line', animation = 'progress', theme = null, loadingText = 'Loading...', count = 1 } = config || {};
this.appearance = appearance;
this.animation = animation;
this.theme = theme;
this.loadingText = loadingText;
this.count = count;
this.items = [];
}

ngOnInit() {
start('NgxSkeletonLoader:Rendered');
Expand All @@ -61,7 +76,6 @@ export class NgxSkeletonLoaderComponent implements OnInit, AfterViewInit, OnDest
}
this.count = 1;
}

this.items.length = this.count;

const allowedAnimations = ['progress', 'progress-dark', 'pulse', 'false'];
Expand All @@ -77,11 +91,11 @@ export class NgxSkeletonLoaderComponent implements OnInit, AfterViewInit, OnDest
this.animation = 'progress';
}

if (['circle', ''].indexOf(String(this.appearance)) === -1) {
if (['circle', 'line', ''].indexOf(String(this.appearance)) === -1) {
// Shows error message only in Development
if (isDevMode()) {
console.error(
`\`NgxSkeletonLoaderComponent\` need to receive 'appearance' as: circle or empty string. Forcing default to "''".`,
`\`NgxSkeletonLoaderComponent\` need to receive 'appearance' as: circle or line or empty string. Forcing default to "''".`,
);
}
this.appearance = '';
Expand Down