Skip to content

Commit

Permalink
Merge pull request #154 from DDtMM/19.x
Browse files Browse the repository at this point in the history
Fix bad manualCleanups: false, and improve springSignal
  • Loading branch information
DDtMM authored Nov 28, 2024
2 parents 3ae711c + 9fb9ace commit bab1d23
Show file tree
Hide file tree
Showing 13 changed files with 134 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { RouterLink } from '@angular/router';
import { DemoHostComponent } from '../../controls/demo-host.component';
import { MemberPageHeaderComponent } from '../../controls/member-page-header.component';
import { SimpleSpringDemoComponent } from '../../demos/spring-signal/simple-spring-demo/simple-spring-demo.component';
import { MultipleSpringNumbersDemoComponent } from '../../demos/spring-signal/multiple-spring-numbers-demo/multiple-spring-numbers-demo.component';


@Component({
selector: 'app-spring-signal-page',
imports: [
DemoHostComponent,
MemberPageHeaderComponent,
MultipleSpringNumbersDemoComponent,
RouterLink,
SimpleSpringDemoComponent
],
Expand Down Expand Up @@ -41,7 +43,11 @@ import { SimpleSpringDemoComponent } from '../../demos/spring-signal/simple-spri
hiddenPattern="spring-options" >
<app-simple-spring-demo />
</app-demo-host>
<app-demo-host name="Animating Multiple Values"
pattern="spring-signal/(multiple-spring-numbers-demo|shared)"
hiddenPattern="spring-options" >
<app-multiple-spring-numbers-demo />
</app-demo-host>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { SimpleTweenDemoComponent } from '../../demos/tween-signal/simple-tween-
hiddenPattern="easing-selector">
<app-simple-tween-demo />
</app-demo-host>
<app-demo-host name="Multiple Value Changes"
<app-demo-host name="Animation Multiple Values"
pattern="tween-signal/(multiple-numbers-demo|shared)"
hiddenPattern="easing-selector">
<app-multiple-numbers-demo />
Expand Down
4 changes: 2 additions & 2 deletions projects/demo/src/app/demo-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export const DEMO_CONFIGURATIONS = [
name: 'springSignal',
page: () => import('./content/signal-factories/spring-signal-page.component').then(x => x.SpringSignalPageComponent),
route: 'spring-signal',
sourceUrl: 'signals/dom-observers/spring-signal.ts',
sourceUrl: 'signals/animation/spring-signal.ts',
usages: ['generator', 'writableSignal']
},
{
Expand Down Expand Up @@ -242,7 +242,7 @@ export const DEMO_CONFIGURATIONS = [
name: 'tweenSignal',
page: () => import('./content/signal-factories/tween-signal-page.component').then(x => x.TweenSignalPageComponent),
route: 'tween-signal',
sourceUrl: 'signals/tween-signal.ts',
sourceUrl: 'signals/animation/tween-signal.ts',
usages: ['generator', 'writableSignal']
}
] satisfies DemoConfigurationItem<string>[];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->
<div class="flex flex-row gap-3 items-center pb-3">
<app-spring-options [(springOptions)]="$springOptions" (springOptionsChange)="$coords.setOptions($springOptions())" />
</div>
<div #target class="relative cursor-pointer w-36 h-36 bg-green-600 rounded-lg border-slate-900 border-solid border overflow-hidden"
(click)="setCords($event, target)"
(mousemove)="setCords($event, target)" >
<div class="w-4 h-4 bg-slate-900 rounded-full"
[ngStyle]="{ 'translate': $coords()[0] + 'px ' + $coords()[1] + 'px' }">
</div>
<div class="text-sm absolute bottom-2 right-2 pointer-events-none">
({{$coords()[0] | number: '1.1-2'}}, {{$coords()[1] | number: '1.1-2'}})
</div>
</div>

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { SpringOptions, springSignal } from '@ddtmm/angular-signal-generators';
import { SpringOptionsComponent } from "../shared/spring-options.component";

@Component({
selector: 'app-multiple-spring-numbers-demo',
templateUrl: './multiple-spring-numbers-demo.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, SpringOptionsComponent]
})
export class MultipleSpringNumbersDemoComponent {
readonly $springOptions = signal<Partial<SpringOptions>>({ damping: 7, stiffness: 150 });
readonly $coords = springSignal([0, 0], { ...this.$springOptions() });

setCords(event: MouseEvent, desiredTarget: HTMLElement) {
if (event.target === desiredTarget) {
this.$coords.set([event.offsetX - 8, event.offsetY - 8]);
}
}
}
2 changes: 2 additions & 0 deletions projects/demo/src/app/services/demos-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export default {
"spring-signal/simple-spring-demo/simple-spring-demo.component.ts": "import { ChangeDetectionStrategy, Component, signal } from '@angular/core';\r\nimport { SpringOptions, springSignal } from '@ddtmm/angular-signal-generators';\r\nimport { SpringOptionsComponent } from \"../shared/spring-options.component\";\r\n\r\n@Component({\r\n selector: 'app-simple-spring-demo',\r\n templateUrl: './simple-spring-demo.component.html',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [SpringOptionsComponent]\r\n})\r\nexport class SimpleSpringDemoComponent {\r\n readonly $springOptions = signal<Partial<SpringOptions>>({ damping: 3, stiffness: 100 });\r\n readonly $sliderValue = springSignal(0, { clamp: true, ...this.$springOptions() });\r\n}\r\n",
"spring-signal/simple-spring-demo/simple-spring-demo.component.html": "<div class=\"flex flex-row gap-3 items-center pb-3\">\r\n <app-spring-options [(springOptions)]=\"$springOptions\" (springOptionsChange)=\"$sliderValue.setOptions($springOptions())\" />\r\n</div>\r\n<div class=\"flex flex-col w-full sm:flex-row items-center gap-3\">\r\n <div class=\"flex-none\">\r\n <div class=\"join\">\r\n <button type=\"button\" class=\"btn btn-primary join-item\" (click)=\"$sliderValue.set(0)\">0%</button>\r\n <button type=\"button\" class=\"btn btn-primary join-item\" (click)=\"$sliderValue.set(50)\">50%</button>\r\n <button type=\"button\" class=\"btn btn-primary join-item\" (click)=\"$sliderValue.set(100)\">100%</button>\r\n </div>\r\n </div>\r\n <div>\r\n <input class=\"range range-primary\" type=\"range\" [value]=\"$sliderValue()\" min=\"0\" max=\"100\" step=\".0001\" />\r\n </div>\r\n</div>\r\n",
"spring-signal/shared/spring-options.component.ts": "import { CommonModule } from '@angular/common';\r\nimport { ChangeDetectionStrategy, Component, model } from '@angular/core';\r\nimport { FormsModule } from '@angular/forms';\r\nimport { SpringOptions } from '@ddtmm/angular-signal-generators';\r\n\r\n\r\n@Component({\r\n selector: 'app-spring-options',\r\n imports: [CommonModule, FormsModule],\r\n template: `\r\n<div class=\"grid grid-cols-[auto,auto,auto] md:grid-cols-[auto,auto,auto,auto,auto,auto] gap-3\">\r\n <div class=\"grid grid-cols-subgrid gap-3 col-span-3 bg-base-200 p-3 rounded\">\r\n <label for=\"dampingRange\">Damping</label>\r\n <input id=\"dampingRange\" type=\"range\" min=\"0.1\" max=\"20\" class=\"range range-primary\" step=\"0.01\" \r\n [ngModel]=\"$springOptions().damping\" (ngModelChange)=\"patchOptions({ damping: $event })\" />\r\n <span aria-label=\"Damping %\"> {{ ($springOptions().damping || 0) | number: '1.1-1' }} </span>\r\n </div>\r\n <div class=\"grid grid-cols-subgrid gap-3 col-span-3 bg-base-200 p-3 rounded\">\r\n <label for=\"stiffnessRange\">Stiffness</label>\r\n <input id=\"stiffnessRange\" type=\"range\" min=\"0.1\" max=\"200\" class=\"range range-primary\" step=\"0.01\"\r\n [ngModel]=\"$springOptions().stiffness\" (ngModelChange)=\"patchOptions({ stiffness: $event })\" /> \r\n <span aria-label=\"Stiffness %\"> {{ ($springOptions().stiffness || 0) | number: '1.1-1' }} </span>\r\n </div>\r\n</div>\r\n `,\r\n changeDetection: ChangeDetectionStrategy.OnPush\r\n})\r\nexport class SpringOptionsComponent {\r\n readonly $springOptions = model.required<Partial<SpringOptions>>(\r\n { alias: 'springOptions' }\r\n );\r\n\r\n patchOptions(partialOptions: Partial<SpringOptions>): void {\r\n this.$springOptions.update((x) => ({ ...x, ...partialOptions }));\r\n }\r\n}\r\n",
"spring-signal/multiple-spring-numbers-demo/multiple-spring-numbers-demo.component.ts": "import { CommonModule } from '@angular/common';\r\nimport { ChangeDetectionStrategy, Component, signal } from '@angular/core';\r\nimport { SpringOptions, springSignal } from '@ddtmm/angular-signal-generators';\r\nimport { SpringOptionsComponent } from \"../shared/spring-options.component\";\r\n\r\n@Component({\r\n selector: 'app-multiple-spring-numbers-demo',\r\n templateUrl: './multiple-spring-numbers-demo.component.html',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [CommonModule, SpringOptionsComponent]\r\n})\r\nexport class MultipleSpringNumbersDemoComponent {\r\n readonly $springOptions = signal<Partial<SpringOptions>>({ damping: 7, stiffness: 150 });\r\n readonly $coords = springSignal([0, 0], { ...this.$springOptions() });\r\n\r\n setCords(event: MouseEvent, desiredTarget: HTMLElement) {\r\n if (event.target === desiredTarget) {\r\n this.$coords.set([event.offsetX - 8, event.offsetY - 8]);\r\n }\r\n }\r\n}\r\n",
"spring-signal/multiple-spring-numbers-demo/multiple-spring-numbers-demo.component.html": "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\r\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\r\n<div class=\"flex flex-row gap-3 items-center pb-3\">\r\n <app-spring-options [(springOptions)]=\"$springOptions\" (springOptionsChange)=\"$coords.setOptions($springOptions())\" />\r\n</div>\r\n<div #target class=\"relative cursor-pointer w-36 h-36 bg-green-600 rounded-lg border-slate-900 border-solid border overflow-hidden\"\r\n (click)=\"setCords($event, target)\"\r\n (mousemove)=\"setCords($event, target)\" >\r\n <div class=\"w-4 h-4 bg-slate-900 rounded-full\"\r\n [ngStyle]=\"{ 'translate': $coords()[0] + 'px ' + $coords()[1] + 'px' }\">\r\n </div>\r\n <div class=\"text-sm absolute bottom-2 right-2 pointer-events-none\">\r\n ({{$coords()[0] | number: '1.1-2'}}, {{$coords()[1] | number: '1.1-2'}})\r\n </div>\r\n</div>\r\n\r\n",
"sequence-signal/toggle-demo/toggle-demo.component.ts": "import { ChangeDetectionStrategy, Component } from '@angular/core';\r\nimport { sequenceSignal } from '@ddtmm/angular-signal-generators';\r\n\r\n@Component({\n selector: 'app-toggle-demo',\n imports: [],\n templateUrl: './toggle-demo.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\r\nexport class ToggleDemoComponent {\r\n readonly $trueFalseToggle = sequenceSignal([true, false]);\r\n}\r\n",
"sequence-signal/toggle-demo/toggle-demo.component.html": "<div>\r\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"$trueFalseToggle.next()\">TOGGLE ME</button>\r\n <div class=\"label\">Current Value</div>\r\n <div>{{$trueFalseToggle()}}</div>\r\n</div>\r\n",
"sequence-signal/fibonacci-demo/fibonacci-demo.component.ts": "import { ChangeDetectionStrategy, Component } from '@angular/core';\r\nimport { FormsModule } from '@angular/forms';\r\nimport { sequenceSignal } from '@ddtmm/angular-signal-generators';\r\n\r\n@Component({\n selector: 'app-fibonacci-demo',\n imports: [FormsModule],\n templateUrl: './fibonacci-demo.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\r\nexport class FibonacciDemoComponent {\r\n readonly $fibonacci = sequenceSignal((() => {\r\n let values = [1, 2];\r\n return {\r\n next: (relativeChange: number) => {\r\n for (let i = 0; i < relativeChange; i++) {\r\n values = [values[1], values[0] + values[1]];\r\n }\r\n for (let i = relativeChange; i < 0; i++) {\r\n values = [Math.max(1, values[1] - values[0]), Math.max(values[0], 2)];\r\n }\r\n return { hasValue: true, value: values[0] };\r\n },\r\n reset: () => values = [1, 2]\r\n };\r\n })());\r\n fibonacciStepSize = 1;\r\n}\r\n",
Expand Down
2 changes: 1 addition & 1 deletion projects/signal-generators/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ddtmm/angular-signal-generators",
"version": "3.0.2",
"version": "3.0.3",
"license": "MIT",
"description": "Specialized Angular signals to help with frequently encountered situations.",
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ import {
runTypeGuardTests
} from '../../../testing/common-signal-tests';
import { createFixture, tickAndAssertValues } from '../../../testing/testing-utilities';
import { AnimatedSignal, animatedSignalFactory, AnimatedSignalOptions, AnimationOptions, AnimationStepFn, WritableAnimatedSignal } from './animated-signal-base';
import {
AnimatedSignal,
animatedSignalFactory,
AnimatedSignalOptions,
AnimationOptions,
AnimationState,
AnimationStepFn,
WritableAnimatedSignal
} from './animated-signal-base';
import { ValueSource } from '../../value-source';
import { ReactiveSource } from '../../reactive-source';


describe('animatedSignalFactory', () => {
describe('when passed a value', () => {
runDebugNameOptionTest((debugName) => createAnimationSignalForTest(1, { debugName }));
Expand Down Expand Up @@ -90,7 +97,7 @@ describe('animatedSignalFactory', () => {
]
);
}));

it('returns an interpolated value when interpolator is passed', fakeAsync(() => {
const sut = TestBed.runInInjectionContext(() =>
createAnimationSignalForTest(1, {
Expand Down Expand Up @@ -158,12 +165,36 @@ describe('animatedSignalFactory', () => {
]
);
}));
it('updates after next effect if duration is less than 0', fakeAsync(() => {
it('updates to final value after first effect if step function determines it should be done.', fakeAsync(() => {
const sut = TestBed.runInInjectionContext(() => createAnimationSignalForTest(1, { duration: -100 }));
sut.set(5);
tick();
expect(sut()).toBe(5);
}));
it('maintains the previous state if a new animation starts before the previous one is finished', fakeAsync(() => {
/*
Because state is not immutable spy.toHaveBeenCalledWith will just contain the state of the last call.
So, we keep track of the tickCount in the step function and use that to determine that the state was maintained.
*/
const initialState = { tickCount: 0 };
let tickCountOuter = 0;

const stepFn = jasmine.createSpy().and.callFake((state: AnimationState<typeof initialState>, options: TestAnimationOptions) => {
state.progress = options.duration > 0 ? Math.min(1, state.timeElapsed / options.duration) : 1;
state.isDone = state.progress === 1;
tickCountOuter = ++state.tickCount;
});
const sut = TestBed.runInInjectionContext(() => createAnimationSignalForTest(1, { duration: 100 }, stepFn, initialState));
sut.set(5);
tick(50);
const tickCountAtChange = tickCountOuter;
expect(tickCountAtChange).toBeGreaterThan(0);
expect(stepFn).toHaveBeenCalledWith(jasmine.objectContaining({ tickCount: tickCountAtChange }), jasmine.anything());
sut.set(9);
tick(0);
expect(tickCountOuter).toBe(tickCountAtChange + 1);

}));
// fit('updates predictably if for some reason multiple frames occur within the same time interval', fakeAsync(() => {
// const sut = TestBed.runInInjectionContext(() => createAnimationSignalForTest(1, { duration: 500 }));
// sut.set(5);
Expand Down Expand Up @@ -278,7 +309,9 @@ describe('animatedSignalFactory', () => {
}));

it('returns the end value if there are not a matching property to transition from in the original object', fakeAsync(() => {
const sut = TestBed.runInInjectionContext(() => createAnimationSignalForTest<Record<string, number>>({ x: 0 }, { duration: 500 }));
const sut = TestBed.runInInjectionContext(() =>
createAnimationSignalForTest<Record<string, number>, {}>({ x: 0 }, { duration: 500 })
);
sut.set({ x: 10, y: -10 });
tickAndAssertValues(
() => ({ x: Math.round(sut()['x']), y: Math.round(sut()['y']) }),
Expand Down Expand Up @@ -339,27 +372,27 @@ describe('animatedSignalFactory', () => {
});
});

function createStepFunctionSpy(): jasmine.Spy<AnimationStepFn<any, AnimationOptions>> {
return jasmine.createSpy<AnimationStepFn<any, AnimationOptions>>('stepFunction').and.callFake((progress: number) => progress);
}

interface TestAnimationOptions extends AnimationOptions {
duration: number;
}

/**
* Creates an animated signal for testing.
* @param source The value source for the signal
* @param signalOptions Animation options to the signal that came from the user.
* @param stepFn A step function to use, if none is passed a linear step function will be used.
* @param initialState A state bag, if none is provided then an empty object will be used.
* @returns
* @returns
*/
function createAnimationSignalForTest<T>(
source: ValueSource<T>,
signalOptions?: Partial<AnimatedSignalOptions<T, TestAnimationOptions>>,
stepFn?: AnimationStepFn<{ duration: number }, TestAnimationOptions>,
function createAnimationSignalForTest<TVal, TState extends object>(
source: ValueSource<TVal>,
signalOptions?: Partial<AnimatedSignalOptions<TVal, TestAnimationOptions>>,
stepFn?: AnimationStepFn<TState, TestAnimationOptions>,
initialState?: any
): typeof source extends ReactiveSource<any> ? AnimatedSignal<T, TestAnimationOptions> : WritableAnimatedSignal<T, TestAnimationOptions> {
): typeof source extends ReactiveSource<TVal>
? AnimatedSignal<TVal, TestAnimationOptions>
: WritableAnimatedSignal<TVal, TestAnimationOptions> {

return animatedSignalFactory(
source,
signalOptions,
Expand All @@ -370,4 +403,4 @@ function createAnimationSignalForTest<T>(
state.isDone = state.progress === 1;
})
) as any; // can't seem to get the types right here.
}
}
Loading

0 comments on commit bab1d23

Please sign in to comment.