;
12 | }
13 |
14 | @Component({ selector: 'no-focusable', template: `` })
15 | class NoFocusableComponent implements OnInit {
16 | public constructor(private $element: ElementRef) {}
17 |
18 | public ngOnInit() {
19 | (this.$element.nativeElement as HTMLElement).focus = undefined as any;
20 | }
21 | }
22 |
23 | @Directive({ selector: '[focus-binding]', exportAs: 'focusBinding' })
24 | class FocusBindingDirective {
25 |
26 | public constructor(
27 | @Inject(DOCUMENT)
28 | private readonly $document: Document,
29 | private readonly $el: ElementRef,
30 | ) {}
31 |
32 | public get isFocused(): boolean {
33 | return this.$el.nativeElement === this.$document.activeElement;
34 | }
35 | }
36 |
37 | @Directive({ selector: '[self-focusing]' })
38 | class SelfFocusingDirective implements OnInit {
39 |
40 | public readonly element: HTMLElement;
41 |
42 | public constructor($er: ElementRef) {
43 | this.element = $er.nativeElement;
44 | }
45 |
46 | public ngOnInit(): void {
47 | this.element.focus();
48 | spyOn(this.element, 'focus');
49 | }
50 |
51 | }
52 |
53 | @Component({
54 | selector: 'wrapper',
55 | template: `
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | {{ focusBindingDir?.isFocused }}
74 |
75 |
76 |
77 | {{ focusBindingDir?.isFocused }}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | `,
86 | })
87 | class TestWrapperComponent {
88 | @ViewChild(FocusBindingDirective, { static: false })
89 | public focusBindingDir!: FocusBindingDirective;
90 |
91 | @ViewChild(SelfFocusingDirective, { static: false })
92 | public selfFocusingDir!: SelfFocusingDirective;
93 |
94 | @ViewChild(AutofocusFixDirective, { static: false })
95 | public dir!: TestAutofocusFixDirective;
96 |
97 | public showNoFocusable = false;
98 | public show: boolean[] = Array(4).fill(false);
99 | public autofocusValue: any = true;
100 | public smartEmptyCheck = false;
101 |
102 | public showFocusBinding = false;
103 | public showFocusBindingWithTriggerChangeDetection = false;
104 |
105 | public showAsync = false;
106 |
107 | public showSelfFocusing = false;
108 | }
109 |
110 | const configMock = (): Mutable => ({
111 | async: false,
112 | triggerDetectChanges: false,
113 | smartEmptyCheck: false,
114 | });
115 |
116 | describe(AutofocusFixDirective.prototype.constructor.name, () => {
117 | let comp: TestWrapperComponent;
118 | let fixture: ComponentFixture;
119 |
120 | function getInput(num: number): HTMLElement | undefined | null {
121 | const debugElement = fixture.debugElement.query(By.css('.input-' + num));
122 | return debugElement && debugElement.nativeElement;
123 | }
124 |
125 | function getFocused(): HTMLElement | undefined | null {
126 | const debugElement = fixture.debugElement.query(By.css(':focus'));
127 | return debugElement && debugElement.nativeElement;
128 | }
129 |
130 | beforeEach(async () => {
131 | await TestBed
132 | .configureTestingModule({
133 | imports: [CommonModule],
134 | declarations: [
135 | // For testing
136 | TestWrapperComponent,
137 | NoFocusableComponent,
138 | FocusBindingDirective,
139 | SelfFocusingDirective,
140 |
141 | // The directive
142 | AutofocusFixDirective,
143 | ],
144 | providers: [
145 | {
146 | provide: AutofocusFixConfig,
147 | useFactory: configMock,
148 | },
149 | ],
150 | })
151 | .compileComponents();
152 |
153 | fixture = TestBed.createComponent(TestWrapperComponent);
154 | comp = fixture.componentInstance;
155 | fixture.detectChanges();
156 | });
157 |
158 | describe('SCENARIO: Testing TestWrapperComponent', () => {
159 | describe('GIVEN: Initialization', () => {
160 | it('should create', () => {
161 | expect(comp).toBeTruthy();
162 | });
163 | it('should have correct default values', () => {
164 | comp.show.forEach(v => expect(v).toBe(false));
165 | expect(comp.autofocusValue).toBe(true);
166 | expect(comp.smartEmptyCheck).toBe(false);
167 | });
168 | });
169 |
170 | for (let i = 0; i < 4; i++) {
171 | describe(`GIVEN: The should be inserted and deleted from HTML depend of .show${ i }`, () => {
172 |
173 | describe(`WHEN: .show${ i } === false`, () => {
174 | it('THEN: must be absent', () => {
175 | expect(getInput(i)).toBeFalsy();
176 | });
177 | });
178 |
179 | describe(`WHEN: .show${ i } become false`, () => {
180 | it('THEN: must be inserted', () => {
181 | // act
182 | comp.show[i] = true;
183 | fixture.detectChanges();
184 |
185 | // assert
186 | expect(getInput(i)).toBeTruthy();
187 | });
188 | });
189 | });
190 | }
191 | }); // end :: SCENARIO: Testing TestWrapperComponent
192 |
193 | describe('SCENARIO: Edge cases', () => {
194 | describe('GIVEN: No .focus() method on the HTMLElement', () => {
195 | describe('WHEN: Initialize autofocus', () => {
196 | it('THEN: Print console warning', () => {
197 | // arrange
198 | spyOn(console, 'warn');
199 |
200 | // act
201 | comp.showNoFocusable = true;
202 | fixture.detectChanges();
203 |
204 | // assert
205 | const noFocusable = fixture.debugElement.query(By.directive(NoFocusableComponent));
206 | expect(noFocusable).toBeTruthy();
207 | expect(console.warn).toHaveBeenCalled();
208 | });
209 | });
210 | });
211 | });
212 |
213 | describe('SCENARIO: Input autofocus on creation', () => {
214 |
215 | describe('GIVEN: Autofocus in case one input', () => {
216 | describe('WHEN: Input created', () => {
217 | it('THEN: Should be autofocused', () => {
218 | // act
219 | comp.show[0] = true;
220 | fixture.detectChanges();
221 |
222 | // assert
223 | const input = getInput(0);
224 | expect(input).toBeTruthy();
225 | expect(input).toBe(getFocused());
226 | });
227 | });
228 |
229 | describe('WHEN: Input created with no value for @Input(\'autofocus\')', () => {
230 | it('THEN: Should be autofocused', () => {
231 | // act
232 | comp.show[2] = true;
233 | fixture.detectChanges();
234 |
235 | // assert
236 | const input = getInput(2);
237 | expect(input).toBeTruthy();
238 | expect(input).toBe(getFocused());
239 | });
240 | });
241 | });
242 |
243 | describe('GIVEN: Opposite autofocus behavior for an empty string in case Smart Empty Check', () => {
244 | describe('WHEN: Input created', () => {
245 | it('THEN: Should be autofocused', () => {
246 | // arrange
247 | comp.smartEmptyCheck = true;
248 | comp.autofocusValue = '';
249 |
250 | // act
251 | comp.show[0] = true;
252 | fixture.detectChanges();
253 |
254 | // assert
255 | const input = getInput(0);
256 | expect(input).toBeTruthy();
257 | expect(getFocused()).toBeFalsy();
258 | });
259 | });
260 |
261 | describe('WHEN: Input created with no value for @Input(\'autofocus\')', () => {
262 | it('THEN: Should be autofocused', () => {
263 | // arrange
264 | comp.smartEmptyCheck = true;
265 |
266 | // act
267 | comp.show[3] = true;
268 | fixture.detectChanges();
269 |
270 | // assert
271 | const input = getInput(3);
272 | expect(input).toBeTruthy();
273 | expect(getFocused()).toBeFalsy();
274 | });
275 | });
276 | });
277 |
278 | describe('GIVEN: Disable autofocus on creation in case @Input(\'autofocus\') falsy', () => {
279 | describe('WHEN: Input created with @Input(\'autofocus\') === false', () => {
280 | it('THEN: Should not be autofocused', () => {
281 | // arrange
282 | comp.autofocusValue = false;
283 |
284 | // act
285 | comp.show[0] = true;
286 | fixture.detectChanges();
287 |
288 | // assert
289 | const input = getInput(0);
290 | expect(input).toBeTruthy();
291 | expect(getFocused()).toBeFalsy();
292 | });
293 | });
294 |
295 | describe('WHEN: Input created with @Input(\'autofocus\') === undefined', () => {
296 | it('THEN: Should not be autofocused', () => {
297 | // arrange
298 | comp.autofocusValue = undefined;
299 |
300 | // act
301 | comp.show[0] = true;
302 | fixture.detectChanges();
303 |
304 | // assert
305 | const input = getInput(0);
306 | expect(input).toBeTruthy();
307 | expect(getFocused()).toBeFalsy();
308 | });
309 | });
310 | });
311 |
312 | describe('GIVEN: Autofocus in case multiple inputs', () => {
313 | describe('WHEN: Second input created', () => {
314 | it('THEN: Second input should be autofocused', () => {
315 | // arrange
316 | comp.show[0] = true;
317 | fixture.detectChanges();
318 |
319 | // act
320 | comp.show[1] = true;
321 | fixture.detectChanges();
322 |
323 | // assert
324 | const input1 = getInput(0);
325 | const input2 = getInput(1);
326 | expect(input1).toBeTruthy();
327 | expect(input2).toBeTruthy();
328 | expect(input2).toBe(getFocused());
329 | });
330 | });
331 | });
332 |
333 | describe('GIVEN: Input params changes after directive initialized', () => {
334 | describe('WHEN: Change autofocusFixSmartEmptyCheck after directive initialized', () => {
335 | it('THEN: .localConfig should have the previous value', () => {
336 | // arrange
337 | comp.show[0] = true;
338 | comp.smartEmptyCheck = true;
339 | fixture.detectChanges();
340 |
341 | // pre assert
342 | expect(comp.dir.autofocusFixSmartEmptyCheck).toBe(true);
343 | expect(comp.dir.localConfig.smartEmptyCheck).toBe(true);
344 |
345 | // act
346 | comp.smartEmptyCheck = false;
347 | fixture.detectChanges();
348 |
349 | // assert
350 | expect(comp.dir.autofocusFixSmartEmptyCheck).toBe(false);
351 | expect(comp.dir.localConfig.smartEmptyCheck).toBe(true);
352 | });
353 | });
354 | });
355 |
356 | describe('GIVEN: Triggering .focus() by others before the autofocus directive', () => {
357 | describe('WHEN: Component triggers by the component on the which [autofocus] added', () => {
358 | it('THEN: .focus() should not be triggered by [autofocus] directive', () => {
359 | // act
360 | comp.showSelfFocusing = true;
361 | fixture.detectChanges();
362 |
363 | // pre assert
364 | expect(comp.selfFocusingDir).toBeTruthy();
365 | const focusedEl = getFocused();
366 | expect(focusedEl).toBeTruthy();
367 |
368 | // assert
369 | expect(comp.selfFocusingDir.element.focus).not.toHaveBeenCalled();
370 | });
371 | });
372 | });
373 | }); // end :: SCENARIO: Autofocus on creation
374 |
375 | describe('SCENARIO: Triggering Change Detection after focusing', () => {
376 | describe('GIVEN: Multiple directives on the same HTMLElement', () => {
377 |
378 | describe('WHEN: triggerChangeDetection === false (default)', () => {
379 | it('THEN: Should throw ExpressionChangedAfterItHasBeenCheckedError', () => {
380 | // act
381 | comp.showFocusBinding = true;
382 | const cb = () => fixture.detectChanges();
383 |
384 | // assert
385 | expect(cb).toThrowError(/ExpressionChangedAfterItHasBeenCheckedError/);
386 | });
387 | });
388 |
389 | describe('WHEN: With autofocusFixTriggerDetectChanges attribute', () => {
390 | it('THEN: Should NOT throw ExpressionChangedAfterItHasBeenCheckedError', () => {
391 | comp.showFocusBindingWithTriggerChangeDetection = true;
392 | fixture.detectChanges();
393 | });
394 | });
395 |
396 | describe('WHEN: .triggerDetectChanges enabled via global config', () => {
397 | it(
398 | 'THEN: Should NOT throw ExpressionChangedAfterItHasBeenCheckedError',
399 | inject([AutofocusFixConfig], (config: Mutable) => {
400 | config.smartEmptyCheck = true;
401 | fixture.detectChanges();
402 | }),
403 | );
404 | });
405 |
406 | });
407 | });
408 |
409 | describe('SCENARIO: Asynchronous focusing', () => {
410 | describe('GIVEN: Input should not have focus in the main execution flow', () => {
411 |
412 | describe('WHEN: Async enabled via attribute autofocusFixAsync', () => {
413 | it('THEN: Input should not be focused immediately', async () => {
414 | // act
415 | comp.showAsync = true;
416 | fixture.detectChanges();
417 |
418 | // assert
419 | expect(getFocused()).toBeFalsy();
420 | await fixture.whenStable();
421 | expect(getFocused()).toBeTruthy();
422 | });
423 | });
424 |
425 | describe('WHEN: .async enabled via global config', () => {
426 | it(
427 | 'THEN: Input should not be focused immediately',
428 | inject([AutofocusFixConfig], async (config: Mutable) => {
429 | // arrange
430 | config.async = true;
431 |
432 | // act
433 | comp.show[0] = true;
434 | fixture.detectChanges();
435 |
436 | // assert
437 | expect(getFocused()).toBeFalsy();
438 | await fixture.whenStable();
439 | expect(getFocused()).toBeTruthy();
440 | }),
441 | );
442 | });
443 |
444 | });
445 | });
446 | });
447 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/src/lib/autofocus-fix.directive.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AfterContentInit,
3 | ChangeDetectorRef,
4 | Directive,
5 | ElementRef,
6 | Inject,
7 | Input,
8 | OnChanges, OnInit,
9 | SimpleChange,
10 | } from '@angular/core';
11 | import { DOCUMENT } from '@angular/common';
12 |
13 | import { normalizeInputAsBoolean, MutablePartial, Mutable } from './utils';
14 | import { AutofocusFixConfig } from './autofocus-fix-config';
15 |
16 | // @todo: check configuration
17 |
18 | /**
19 | * ## Ways to turn off autofocus: any js-falsely value, except empty string
20 | *
21 | *
22 | *
23 | *
24 | *
25 | *
26 | *
27 | *
28 | *
29 | *
30 | *
31 | *
32 | *
33 | *
34 | *
35 | *
36 | *
37 | *
38 | *
39 | * ## Ways to enable autofocus: any js-true value and empty string
40 | *
41 | *
42 | *
43 | *
44 | *
45 | *
46 | *
47 | *
48 | *
49 | *
50 | *
51 | * @dynamic
52 | * Notice: @dynamic used for correctly Document inject
53 | * https://github.com/angular/angular/issues/20351
54 | */
55 | @Directive({
56 | selector: '[autofocus]',
57 | })
58 | export class AutofocusFixDirective implements OnChanges, OnInit, AfterContentInit {
59 |
60 | /** Raw value. Always have default value: '' */
61 | @Input()
62 | public autofocus: any;
63 |
64 | /** @see {@link AutofocusFixConfig.smartEmptyCheck} */
65 | @Input()
66 | public autofocusFixSmartEmptyCheck?: boolean | any;
67 |
68 | /** @see {@link AutofocusFixConfig.triggerDetectChanges} */
69 | @Input()
70 | public autofocusFixTriggerDetectChanges?: boolean | any;
71 |
72 | /** @see {@link AutofocusFixConfig.async} */
73 | @Input()
74 | public autofocusFixAsync?: boolean | any;
75 |
76 | private wasInitialized = false;
77 | /** Notice: protected for unit testing */
78 | protected localConfig: MutablePartial = {};
79 | private config!: Mutable;
80 | private autofocusEnabled = false;
81 | private readonly element: HTMLElement;
82 |
83 | public constructor(
84 | $er: ElementRef,
85 | private readonly $cdr: ChangeDetectorRef,
86 | @Inject(DOCUMENT)
87 | private readonly $document: Document,
88 | private readonly $config: AutofocusFixConfig,
89 | ) {
90 | this.element = $er.nativeElement;
91 | }
92 |
93 | public ngOnChanges(changes: { [key in keyof AutofocusFixDirective]?: SimpleChange }): void {
94 | // Autofocus works only once. No need to do the initialization on each change detection cycle.
95 | if (this.wasInitialized) { return; }
96 |
97 | this.normalizeLocalConfigItem('async', changes.autofocusFixAsync);
98 | this.normalizeLocalConfigItem('smartEmptyCheck', changes.autofocusFixSmartEmptyCheck);
99 | this.normalizeLocalConfigItem('triggerDetectChanges', changes.autofocusFixTriggerDetectChanges);
100 | }
101 |
102 | public ngOnInit(): void {
103 | if (!this.element.focus) {
104 | return console.warn(
105 | 'AutofocusFixDirective: There is no .focus() method on the element: %O',
106 | this.element,
107 | );
108 | }
109 |
110 | this.config = {} as AutofocusFixConfig;
111 | AutofocusFixConfig.keys.forEach(key => {
112 | const local = this.localConfig[key];
113 | this.config[key] = local !== undefined ? local : this.$config[key];
114 | });
115 |
116 | this.autofocusEnabled = normalizeInputAsBoolean(this.autofocus, this.config.smartEmptyCheck);
117 | }
118 |
119 | public ngAfterContentInit(): void {
120 | this.wasInitialized = true;
121 | if (!this.element.focus) { return; }
122 |
123 | this.checkFocus();
124 | }
125 |
126 | private checkFocus(): void {
127 | this.config.async ? setTimeout(this.checkFocusInternal.bind(this)) : this.checkFocusInternal();
128 | }
129 |
130 | private checkFocusInternal(): void {
131 | if (!this.autofocusEnabled || this.amIFocused) { return; }
132 |
133 | this.element.focus();
134 | if (this.config.triggerDetectChanges) {
135 | this.$cdr.detectChanges();
136 | }
137 | }
138 |
139 | private get amIFocused(): boolean {
140 | return this.$document.activeElement === this.element;
141 | }
142 |
143 | private normalizeLocalConfigItem(configKey: keyof AutofocusFixConfig, change?: SimpleChange): void {
144 | if (change) {
145 | this.localConfig[configKey] = normalizeInputAsBoolean(change.currentValue);
146 | }
147 | }
148 |
149 | }
150 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/src/lib/autofocus-fix.module.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { AutofocusFixConfig } from './autofocus-fix-config';
3 | import { AutofocusFixModule } from './autofocus-fix.module';
4 |
5 | const moduleName = AutofocusFixModule.prototype.constructor.name;
6 |
7 | describe(moduleName, () => {
8 | describe('SCENARIO: Module importing', () => {
9 |
10 | describe(`WHEN: Importing just '${moduleName}'`, () => {
11 | it('THEN: Should throw an error that can\'t inject config', () => {
12 | TestBed.configureTestingModule({
13 | imports: [AutofocusFixModule],
14 | });
15 | const cb = () => TestBed.get(AutofocusFixConfig);
16 |
17 | expect(cb).toThrow();
18 | });
19 | });
20 |
21 | describe(`WHEN: Importing '${moduleName}'.forRoot()`, () => {
22 | it('THEN: Should get instance', () => {
23 | TestBed.configureTestingModule({
24 | imports: [AutofocusFixModule.forRoot()],
25 | });
26 | const ins = TestBed.get(AutofocusFixConfig);
27 |
28 | expect(ins instanceof AutofocusFixConfig).toBeTruthy();
29 | });
30 | });
31 |
32 | describe(`WHEN: Importing just '${moduleName}' and providing config manually`, () => {
33 | it('THEN: Should get instance', () => {
34 | TestBed.configureTestingModule({
35 | imports: [AutofocusFixModule],
36 | providers: [
37 | {
38 | provide: AutofocusFixConfig,
39 | useValue: new AutofocusFixConfig({}),
40 | }]
41 | });
42 | const ins = TestBed.get(AutofocusFixConfig);
43 |
44 | expect(ins instanceof AutofocusFixConfig).toBeTruthy();
45 | });
46 | });
47 |
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/src/lib/autofocus-fix.module.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken, ModuleWithProviders, NgModule, Optional } from '@angular/core';
2 |
3 | import { AutofocusFixConfig, AutofocusFixOptions } from './autofocus-fix-config';
4 | import { AutofocusFixDirective } from './autofocus-fix.directive';
5 | import { noAutofocusFixConfigError } from './no-autofocus-fix-config.error';
6 |
7 | // Exists for AoT support
8 | export function configFactory(options: AutofocusFixOptions) {
9 | return new AutofocusFixConfig(options);
10 | }
11 | // Exists for AoT support
12 | const AutofocusFixOptionsInternalToken = new InjectionToken('AutofocusFixOptions');
13 |
14 | @NgModule({
15 | declarations: [AutofocusFixDirective],
16 | exports: [AutofocusFixDirective]
17 | })
18 | export class AutofocusFixModule {
19 |
20 | public constructor(@Optional() $config: AutofocusFixConfig) {
21 | if (!$config) {
22 | noAutofocusFixConfigError();
23 | }
24 | }
25 |
26 | public static forRoot(options: AutofocusFixOptions = {}): ModuleWithProviders {
27 |
28 | return {
29 | ngModule: AutofocusFixModule,
30 | providers: [
31 | {
32 | provide: AutofocusFixOptionsInternalToken,
33 | useValue: options,
34 | },
35 | {
36 | provide: AutofocusFixConfig,
37 | useFactory: configFactory,
38 | deps: [AutofocusFixOptionsInternalToken],
39 | },
40 | ],
41 | };
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/src/lib/no-autofocus-fix-config.error.spec.ts:
--------------------------------------------------------------------------------
1 | import { noAutofocusFixConfigError } from './no-autofocus-fix-config.error';
2 |
3 | describe('noAutofocusFixConfigError()', () => {
4 | it('should throw the error', () => {
5 | const cb = () => noAutofocusFixConfigError();
6 |
7 | expect(cb).toThrow();
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/src/lib/no-autofocus-fix-config.error.ts:
--------------------------------------------------------------------------------
1 | export function noAutofocusFixConfigError() {
2 | const moduleName = 'AutofocusFixModule';
3 | const configName = 'AutofocusFixConfig';
4 |
5 | throw new Error(`${ moduleName }: Can't inject ${ configName }.
6 |
7 | Option 1: Use .forRoot() when you importing the module:
8 | Do it in case you import ${ moduleName } to the root module of your application.
9 |
10 | @NgModule({
11 | ...
12 | imports: [
13 | ...
14 | ${ moduleName }.forRoot(), // <--- new code
15 | ],
16 | ...
17 | })
18 | export class AppModule {}
19 |
20 |
21 | Option 2: Provide ${ configName } manually providing ${ configName }:
22 | Do it in case you want to provide specific config to the one of your lazy loadable modules.
23 |
24 | @NgModule({
25 | ...
26 | providers: [
27 | ...
28 | { // <--- new code
29 | provide: ${ configName } // <---
30 | useValue: new ${configName}({ ... }), // <---
31 | }, // <---
32 | ],
33 | ...
34 | })
35 | export class AppModule {}
36 | `);
37 | }
38 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/src/lib/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { normalizeInputAsBoolean } from './utils';
2 |
3 | describe('Utils -> normalizeInputAsBoolean()', () => {
4 |
5 | describe('SCENARIO: .smartEmptyCheck === false', () => {
6 | describe('GIVEN: Should be false', () => {
7 | it('false', () => {
8 | expect(normalizeInputAsBoolean(false)).toBe(false);
9 | });
10 | it('null', () => {
11 | expect(normalizeInputAsBoolean(null)).toBe(false);
12 | });
13 | it('undefined', () => {
14 | expect(normalizeInputAsBoolean(undefined)).toBe(false);
15 | });
16 | it('0', () => {
17 | expect(normalizeInputAsBoolean(0)).toBe(false);
18 | });
19 |
20 | it(`'false'`, () => {
21 | expect(normalizeInputAsBoolean('false')).toBe(false);
22 | });
23 | it(`'null'`, () => {
24 | expect(normalizeInputAsBoolean('null')).toBe(false);
25 | });
26 | it(`'undefined'`, () => {
27 | expect(normalizeInputAsBoolean('undefined')).toBe(false);
28 | });
29 | it(`'0'`, () => {
30 | expect(normalizeInputAsBoolean('0')).toBe(false);
31 | });
32 |
33 | it('NaN', () => {
34 | expect(normalizeInputAsBoolean(NaN)).toBe(false);
35 | });
36 | it(`'NaN'`, () => {
37 | expect(normalizeInputAsBoolean('NaN')).toBe(false);
38 | });
39 | });
40 |
41 | describe('GIVEN: Should be true', () => {
42 | it(`'a string'`, () => {
43 | expect(normalizeInputAsBoolean('a string')).toBe(true);
44 | });
45 | it(`''`, () => {
46 | expect(normalizeInputAsBoolean('')).toBe(true);
47 | });
48 | it('[]', () => {
49 | expect(normalizeInputAsBoolean([])).toBe(true);
50 | });
51 | it('{}', () => {
52 | expect(normalizeInputAsBoolean({})).toBe(true);
53 | });
54 | });
55 | }); // end :: SCENARIO: .smartEmptyCheck === false
56 |
57 | describe('SCENARIO: .smartEmptyCheck === true', () => {
58 | describe('GIVEN: Should be false', () => {
59 | it('false', () => {
60 | expect(normalizeInputAsBoolean(false, true)).toBe(false);
61 | });
62 | it('null', () => {
63 | expect(normalizeInputAsBoolean(null, true)).toBe(false);
64 | });
65 | it('undefined', () => {
66 | expect(normalizeInputAsBoolean(undefined, true)).toBe(false);
67 | });
68 | it('0', () => {
69 | expect(normalizeInputAsBoolean(0, true)).toBe(false);
70 | });
71 |
72 | it(`'false'`, () => {
73 | expect(normalizeInputAsBoolean('false', true)).toBe(false);
74 | });
75 | it(`'null'`, () => {
76 | expect(normalizeInputAsBoolean('null', true)).toBe(false);
77 | });
78 | it(`'undefined'`, () => {
79 | expect(normalizeInputAsBoolean('undefined', true)).toBe(false);
80 | });
81 | it(`'0'`, () => {
82 | expect(normalizeInputAsBoolean('0', true)).toBe(false);
83 | });
84 |
85 | it('NaN', () => {
86 | expect(normalizeInputAsBoolean(NaN, true)).toBe(false);
87 | });
88 | it(`'NaN'`, () => {
89 | expect(normalizeInputAsBoolean('NaN', true)).toBe(false);
90 | });
91 |
92 | it(`''`, () => {
93 | expect(normalizeInputAsBoolean('', true)).toBe(false);
94 | });
95 | it('[]', () => {
96 | expect(normalizeInputAsBoolean([], true)).toBe(false);
97 | });
98 | it('{}', () => {
99 | expect(normalizeInputAsBoolean({}, true)).toBe(false);
100 | });
101 | });
102 |
103 | describe('GIVEN: Should be true', () => {
104 | it(`'a string'`, () => {
105 | expect(normalizeInputAsBoolean('a string', true)).toBe(true);
106 | });
107 | it('[1]', () => {
108 | expect(normalizeInputAsBoolean([1], true)).toBe(true);
109 | });
110 | it('{ a: 1 }', () => {
111 | expect(normalizeInputAsBoolean({ a: 1 }, true)).toBe(true);
112 | });
113 | });
114 | }); // end :: SCENARIO: .smartEmptyCheck === false
115 |
116 | });
117 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | export type MutablePartial = { -readonly [K in keyof T]+?: T[K] };
2 | export type Mutable = { -readonly [K in keyof T]: T[K] };
3 |
4 | export function normalizeInputAsBoolean(value: any, smartEmptyCheck: boolean = false): boolean {
5 | const isFalse = value === false
6 | || value === null
7 | || value === undefined
8 | || value === 0
9 | || value === 'false'
10 | || value === 'null'
11 | || value === 'undefined'
12 | || value === '0'
13 | || (typeof value === 'number' && isNaN(value))
14 | || value === 'NaN'
15 | || smartEmptyCheck && (
16 | value === '' // Notice: opposite default behavior!
17 | || value instanceof Array && !value.length
18 | || value !== null && typeof value === 'object' && !Object.keys(value).length
19 | );
20 |
21 | return !isFalse;
22 | }
23 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/src/public-api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of ngx-autofocus-fix
3 | */
4 |
5 | export { AutofocusFixModule } from './lib/autofocus-fix.module';
6 | export { AutofocusFixConfig, AutofocusFixOptions } from './lib/autofocus-fix-config';
7 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone';
4 | import 'zone.js/dist/zone-testing';
5 | import { getTestBed } from '@angular/core/testing';
6 | import {
7 | BrowserDynamicTestingModule,
8 | platformBrowserDynamicTesting
9 | } from '@angular/platform-browser-dynamic/testing';
10 |
11 | declare const require: any;
12 |
13 | // First, initialize the Angular testing environment.
14 | getTestBed().initTestEnvironment(
15 | BrowserDynamicTestingModule,
16 | platformBrowserDynamicTesting()
17 | );
18 | // Then we find all the tests.
19 | const context = require.context('./', true, /\.spec\.ts$/);
20 | // And load the modules.
21 | context.keys().map(context);
22 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/lib",
5 | "target": "es2015",
6 | "declaration": true,
7 | "inlineSources": true,
8 | "types": [],
9 | "lib": [
10 | "dom",
11 | "es2018"
12 | ]
13 | },
14 | "angularCompilerOptions": {
15 | "annotateForClosureCompiler": true,
16 | "skipTemplateCodegen": true,
17 | "strictMetadataEmit": true,
18 | "fullTemplateTypeCheck": true,
19 | "strictInjectionParameters": true,
20 | "enableResourceInlining": true
21 | },
22 | "exclude": [
23 | "src/test.ts",
24 | "**/*.spec.ts"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts"
12 | ],
13 | "include": [
14 | "**/*.spec.ts",
15 | "**/*.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/projects/ngx-autofocus-fix/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json",
3 | "rules": {
4 | "directive-selector": false,
5 | "component-selector": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tools/common.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import { execSync } from 'child_process';
3 |
4 | export const libName = 'ngx-autofocus-fix';
5 | export const root = path.join.bind(path, __dirname, '..');
6 | export const libRoot = root.bind(path, 'projects', libName);
7 |
8 | export function run(command: string): Buffer {
9 | console.log(`$> ${command}`);
10 | return execSync(command);
11 | }
12 |
--------------------------------------------------------------------------------
/tools/prepare-version.ts:
--------------------------------------------------------------------------------
1 | import { libRoot, run } from './common';
2 |
3 | /** @example v1.0.12 */
4 | type StrVersion = string;
5 |
6 | interface Version {
7 | major: number;
8 | minor: number;
9 | patch: number;
10 | }
11 |
12 | process.chdir(libRoot());
13 |
14 | const localVer = getLocalVersion();
15 | const publicVers = getPublicVersions();
16 |
17 | const isLocalVerPublished = publicVers.some(v => _compareVersion(v, localVer));
18 | if (isLocalVerPublished) {
19 | const v = incrementVersion(localVer);
20 | gitPush();
21 | console.log('Version updated: ', _formatVersion(v));
22 | } else {
23 | console.log('Current version is actual');
24 | }
25 |
26 | function gitPush() {
27 | run(`git push --no-verify --follow-tags`);
28 | // run(`git push --no-verify --tags`);
29 | }
30 |
31 | function incrementVersion(prevVer: Version, type: keyof Version = 'patch'): Version {
32 | const newStrVer = run(`npm version ${type}`).toString().trim();
33 | const prevStrVer = _formatVersion(prevVer);
34 | run(`git add package.json package-lock.json`);
35 | run(`git commit -m 'Update package version: ${prevStrVer} -> ${newStrVer}'`);
36 | run(`git tag -a ${newStrVer} -m 'new version: ${newStrVer}'`);
37 |
38 | return _parseVersion(newStrVer);
39 | }
40 |
41 | function getLocalVersion(): Version | undefined {
42 | return _parseVersion(require(libRoot('package.json')).version);
43 | }
44 |
45 | function getPublicVersions(): Version[] {
46 | const output = run('npm view . versions --json');
47 | const allVersions = JSON.parse(output.toString());
48 |
49 | return allVersions
50 | .map((v: string) => _parseVersion(v))
51 | .filter(Boolean);
52 | }
53 |
54 | // function getGitVersions(): Version[] {
55 | // const output = run(`git tag -l 'v*'`);
56 | //
57 | // return String(output)
58 | // .split('\n')
59 | // .slice(0, -1)
60 | // .map((v: string) => _parseVersion(v))
61 | // .filter(Boolean) as Version[];
62 | // }
63 |
64 | function _parseVersion(strVersion: StrVersion): Version | undefined {
65 | const res = strVersion.match(/^v?(?\d+)\.(?\d+)\.(?\d+)$/);
66 | if (!res || !res.groups) { return; }
67 |
68 | return {
69 | major: +res.groups.major,
70 | minor: +res.groups.minor,
71 | patch: +res.groups.patch,
72 | };
73 | }
74 |
75 | function _formatVersion(v: Version): StrVersion {
76 | return `v${v.major}.${v.minor}.${v.patch}`;
77 | }
78 |
79 | function _compareVersion(a?: Version, b?: Version): boolean {
80 | return !!(a && b)
81 | && a.major === b.major
82 | && a.minor === b.minor
83 | && a.patch === b.patch
84 | ;
85 | }
86 |
--------------------------------------------------------------------------------
/tools/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "moduleResolution": "node",
5 | "target": "es2018"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "experimentalDecorators": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "strict": true,
14 | "target": "es2015",
15 | "typeRoots": [
16 | "node_modules/@types"
17 | ],
18 | "lib": [
19 | "es2018",
20 | "dom"
21 | ],
22 | "paths": {
23 | "angular-autofocus-fix": [
24 | "dist/angular-autofocus-fix"
25 | ],
26 | "angular-autofocus-fix/*": [
27 | "dist/angular-autofocus-fix/*"
28 | ]
29 | }
30 | },
31 | "angularCompilerOptions": {
32 | "fullTemplateTypeCheck": true,
33 | "strictInjectionParameters": true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rulesDirectory": [
4 | "codelyzer"
5 | ],
6 | "rules": {
7 | "array-type": false,
8 | "arrow-parens": false,
9 | "deprecation": {
10 | "severity": "warning"
11 | },
12 | "import-blacklist": [
13 | true,
14 | "rxjs/Rx"
15 | ],
16 | "interface-name": false,
17 | "max-classes-per-file": false,
18 | "max-line-length": [
19 | true,
20 | 140
21 | ],
22 | "member-access": false,
23 | "member-ordering": [
24 | true,
25 | {
26 | "order": [
27 | "static-field",
28 | "instance-field",
29 | "static-method",
30 | "instance-method"
31 | ]
32 | }
33 | ],
34 | "no-consecutive-blank-lines": false,
35 | "no-console": [
36 | true,
37 | "debug",
38 | "info",
39 | "time",
40 | "timeEnd",
41 | "trace"
42 | ],
43 | "no-empty": false,
44 | "no-inferrable-types": [
45 | true,
46 | "ignore-params"
47 | ],
48 | "no-non-null-assertion": true,
49 | "no-redundant-jsdoc": true,
50 | "no-switch-case-fall-through": true,
51 | "no-use-before-declare": true,
52 | "no-var-requires": false,
53 | "object-literal-key-quotes": [
54 | true,
55 | "as-needed"
56 | ],
57 | "object-literal-sort-keys": false,
58 | "ordered-imports": false,
59 | "quotemark": [
60 | true,
61 | "single"
62 | ],
63 | "trailing-comma": false,
64 | "component-class-suffix": true,
65 | "contextual-lifecycle": true,
66 | "directive-class-suffix": true,
67 | "no-conflicting-lifecycle": true,
68 | "no-host-metadata-property": true,
69 | "no-input-rename": true,
70 | "no-inputs-metadata-property": true,
71 | "no-output-native": true,
72 | "no-output-on-prefix": true,
73 | "no-output-rename": true,
74 | "no-outputs-metadata-property": true,
75 | "template-banana-in-box": true,
76 | "template-no-negated-async": true,
77 | "use-lifecycle-interface": true,
78 | "use-pipe-transform-interface": true
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/wallaby.js:
--------------------------------------------------------------------------------
1 | module.exports = function(wallaby) {
2 | const wallabyWebpack = require('wallaby-webpack');
3 | const path = require('path');
4 | const fs = require('fs');
5 |
6 | const specPattern = '/**/*spec.ts';
7 | const angularConfig = require('./angular.json');
8 |
9 | const projects = Object.keys(angularConfig.projects).map(key => {
10 | return { name: key, ...angularConfig.projects[key] };
11 | }).filter(project => project.sourceRoot)
12 | .filter(project => project.projectType === 'library');
13 | // Notice: uncomment if you want to test applications
14 | // .filter(project => project.projectType !== 'application' ||
15 | // (project.architect &&
16 | // project.architect.test &&
17 | // project.architect.test.builder === '@angular-devkit/build-angular:karma'));
18 |
19 | const applications = projects.filter(project => project.projectType === 'application');
20 | const libraries = projects.filter(project => project.projectType === 'library');
21 |
22 | const tsConfigFile = projects
23 | .map(project => path.join(__dirname, project.root, 'tsconfig.spec.json'))
24 | .find(tsConfig => fs.existsSync(tsConfig));
25 |
26 | const tsConfigSpec = tsConfigFile ? JSON.parse(fs.readFileSync(tsConfigFile)) : {};
27 |
28 | const compilerOptions = Object.assign(require('./tsconfig.json').compilerOptions, tsConfigSpec.compilerOptions);
29 | compilerOptions.emitDecoratorMetadata = true;
30 |
31 | return {
32 | files: [
33 | { pattern: path.basename(__filename), load: false, instrument: false },
34 | ...projects.map(project => ({
35 | pattern: project.sourceRoot + '/**/*.+(ts|js|css|less|scss|sass|styl|html|json|svg)',
36 | load: false
37 | })),
38 | ...projects.map(project => ({
39 | pattern: project.sourceRoot + specPattern,
40 | ignore: true
41 | })),
42 | ...projects.map(project => ({
43 | pattern: project.sourceRoot + '/**/*.d.ts',
44 | ignore: true
45 | })),
46 | ...projects.map(project => ({
47 | pattern: project.sourceRoot + '/**/test.ts',
48 | ignore: true
49 | })),
50 | ],
51 |
52 | tests: [
53 | ...projects.map(project => ({
54 | pattern: project.sourceRoot + specPattern,
55 | load: false
56 | }))
57 | ],
58 |
59 | testFramework: 'jasmine',
60 |
61 | compilers: {
62 | '**/*.ts': wallaby.compilers.typeScript({
63 | typescript: require('typescript'),
64 | ...compilerOptions,
65 | getCustomTransformers: program => {
66 | return {
67 | before: [
68 | require('@ngtools/webpack/src/transformers/replace_resources').replaceResources(
69 | path => true,
70 | () => program.getTypeChecker(),
71 | false
72 | )
73 | ]
74 | };
75 | }
76 | })
77 | },
78 |
79 | preprocessors: {
80 | /* Initialize Test Environment for Wallaby */
81 | [path.basename(__filename)]: file => `
82 | import 'zone.js/dist/zone';
83 | import 'zone.js/dist/zone-testing';
84 | import '@angular-devkit/build-angular/src/angular-cli-files/models/jit-polyfills';
85 | import { getTestBed } from '@angular/core/testing';
86 | import {
87 | BrowserDynamicTestingModule,
88 | platformBrowserDynamicTesting
89 | } from '@angular/platform-browser-dynamic/testing';
90 |
91 |
92 | getTestBed().initTestEnvironment(
93 | BrowserDynamicTestingModule,
94 | platformBrowserDynamicTesting()
95 | );
96 | `
97 | },
98 |
99 | middleware: function(app, express) {
100 | const path = require('path');
101 |
102 | applications.forEach(application => {
103 | if (
104 | !application.architect ||
105 | !application.architect.test ||
106 | !application.architect.test.options ||
107 | !application.architect.test.options.assets
108 | ) {
109 | return;
110 | }
111 |
112 | application.architect.test.options.assets.forEach(asset => {
113 | if (asset && !asset.glob) {
114 | // Only works for file assets (not globs)
115 | // (https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/asset-configuration.md#project-assets)
116 | app.use(asset.slice(application.sourceRoot.length), express.static(path.join(__dirname, asset)));
117 | }
118 | });
119 | });
120 | },
121 |
122 | env: {
123 | kind: 'chrome'
124 | },
125 |
126 | postprocessor: wallabyWebpack({
127 | entryPatterns: [
128 | ...applications
129 | .map(project => project.sourceRoot + '/polyfills.js')
130 | .filter(polyfills => fs.existsSync(path.join(__dirname, polyfills.replace(/js$/, 'ts')))),
131 | path.basename(__filename),
132 | ...projects.map(project => project.sourceRoot + specPattern.replace(/ts$/, 'js'))
133 | ],
134 |
135 | module: {
136 | rules: [
137 | { test: /\.css$/, loader: ['raw-loader'] },
138 | { test: /\.html$/, loader: 'raw-loader' },
139 | {
140 | test: /\.ts$/,
141 | loader: '@ngtools/webpack',
142 | include: /node_modules/,
143 | query: { tsConfigPath: 'tsconfig.json' }
144 | },
145 | { test: /\.styl$/, loaders: ['raw-loader', 'stylus-loader'] },
146 | { test: /\.less$/, loaders: ['raw-loader', { loader: 'less-loader' }] },
147 | {
148 | test: /\.scss$|\.sass$/,
149 | loaders: [{ loader: 'raw-loader' }, { loader: 'sass-loader', options: { implementation: require('sass') } }]
150 | },
151 | { test: /\.(jpg|png|svg)$/, loader: 'raw-loader' }
152 | ]
153 | },
154 |
155 | resolve: {
156 | extensions: ['.js', '.ts'],
157 | modules: [
158 | wallaby.projectCacheDir,
159 | ...(projects.length ? projects.filter(project => project.root)
160 | .map(project => path.join(wallaby.projectCacheDir, project.root)) : []),
161 | ...(projects.length ? projects.filter(project => project.sourceRoot)
162 | .map(project => path.join(wallaby.projectCacheDir,project.sourceRoot)) : []),
163 | 'node_modules'
164 | ],
165 | alias: libraries.reduce((result, project) => {
166 | result[project.name] = path.join(wallaby.projectCacheDir, project.sourceRoot, 'public-api');
167 | return result;
168 | }, {})
169 | }
170 | }),
171 |
172 | setup: function() {
173 | window.__moduleBundler.loadTests();
174 | }
175 | };
176 | };
177 |
--------------------------------------------------------------------------------