mirror of https://github.com/ghostfolio/ghostfolio
13 changed files with 320 additions and 53 deletions
@ -0,0 +1,63 @@ |
|||
import { Component, signal } from '@angular/core'; |
|||
import { TestBed } from '@angular/core/testing'; |
|||
|
|||
import { GfFlashOnChangeDirective } from './flash-on-change.directive'; |
|||
|
|||
@Component({ |
|||
imports: [GfFlashOnChangeDirective], |
|||
template: `<tr [gfFlashOnChange]="value()"></tr>` |
|||
}) |
|||
class TestHostComponent { |
|||
public readonly value = signal<number | undefined>(100); |
|||
} |
|||
|
|||
describe('GfFlashOnChangeDirective', () => { |
|||
const setup = () => { |
|||
const fixture = TestBed.createComponent(TestHostComponent); |
|||
fixture.detectChanges(); |
|||
|
|||
return { |
|||
fixture, |
|||
classes: () => fixture.nativeElement.querySelector('tr').classList |
|||
}; |
|||
}; |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ imports: [TestHostComponent] }); |
|||
}); |
|||
|
|||
it('does not flash initially', () => { |
|||
const { classes } = setup(); |
|||
|
|||
expect(classes().contains('gf-flash-up')).toBe(false); |
|||
expect(classes().contains('gf-flash-down')).toBe(false); |
|||
}); |
|||
|
|||
it('flashes up on an increase', () => { |
|||
const { classes, fixture } = setup(); |
|||
|
|||
fixture.componentInstance.value.set(101); |
|||
fixture.detectChanges(); |
|||
|
|||
expect(classes().contains('gf-flash-up')).toBe(true); |
|||
}); |
|||
|
|||
it('flashes down on a decrease', () => { |
|||
const { classes, fixture } = setup(); |
|||
|
|||
fixture.componentInstance.value.set(99); |
|||
fixture.detectChanges(); |
|||
|
|||
expect(classes().contains('gf-flash-down')).toBe(true); |
|||
}); |
|||
|
|||
it('does not flash if the value becomes undefined', () => { |
|||
const { classes, fixture } = setup(); |
|||
|
|||
fixture.componentInstance.value.set(undefined); |
|||
fixture.detectChanges(); |
|||
|
|||
expect(classes().contains('gf-flash-up')).toBe(false); |
|||
expect(classes().contains('gf-flash-down')).toBe(false); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,71 @@ |
|||
import { |
|||
Directive, |
|||
effect, |
|||
ElementRef, |
|||
inject, |
|||
input, |
|||
untracked |
|||
} from '@angular/core'; |
|||
|
|||
import { getValueChangeDirection } from '../value/value-change.helper'; |
|||
|
|||
/** |
|||
* Briefly highlights the host element (e.g. a table row) in green or red if |
|||
* the bound number changes, e.g. after a background refresh. The highlight |
|||
* is styled globally by the `gf-flash-up` and `gf-flash-down` classes. |
|||
*/ |
|||
@Directive({ |
|||
selector: '[gfFlashOnChange]' |
|||
}) |
|||
export class GfFlashOnChangeDirective { |
|||
public readonly gfFlashDigits = input(2); |
|||
public readonly gfFlashOnChange = input<number | null | undefined>(); |
|||
|
|||
private hasValue = false; |
|||
private previousValue: number | null | undefined; |
|||
|
|||
private readonly element = inject<ElementRef<HTMLElement>>(ElementRef); |
|||
|
|||
public constructor() { |
|||
effect(() => { |
|||
const digits = this.gfFlashDigits(); |
|||
const value = this.gfFlashOnChange(); |
|||
|
|||
untracked(() => { |
|||
if (this.hasValue) { |
|||
const direction = getValueChangeDirection({ |
|||
digits, |
|||
currentValue: value, |
|||
previousValue: this.previousValue |
|||
}); |
|||
|
|||
if (direction) { |
|||
this.flash(direction); |
|||
} |
|||
} |
|||
|
|||
this.hasValue = true; |
|||
this.previousValue = value; |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
private flash(aDirection: 'down' | 'up') { |
|||
const element = this.element.nativeElement; |
|||
|
|||
element.classList.remove('gf-flash-down', 'gf-flash-up'); |
|||
|
|||
// Force a reflow to restart the animation
|
|||
void element.offsetWidth; |
|||
|
|||
element.classList.add(`gf-flash-${aDirection}`); |
|||
|
|||
element.addEventListener( |
|||
'animationend', |
|||
() => { |
|||
element.classList.remove('gf-flash-down', 'gf-flash-up'); |
|||
}, |
|||
{ once: true } |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './flash-on-change.directive'; |
|||
Loading…
Reference in new issue