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