Skip to content

Commit

Permalink
Merge pull request #54 from DDtMM/17.x
Browse files Browse the repository at this point in the history
1.4 ready to go.
  • Loading branch information
DDtMM authored Dec 26, 2023
2 parents 19dd273 + 3ed775a commit 4c3f27b
Show file tree
Hide file tree
Showing 41 changed files with 814 additions and 624 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ Adds new methods to a signal - even hiding the existing methods if desired. It

### filterSignal

Filters values from another signal or from values set on directly on the signal. Can be used with guard functions.
Filters values set to a signal to prevent the value from changing:
If the filter assigned at creation does not pass then the signal does not change.
Can be used with guard functions.

### liftSignal

Expand All @@ -68,6 +70,9 @@ Filters values from another signal or from values set on directly on the signal.
Creates a signal whose input value is immediately mapped to a different value based on a selector.
Either a value or multiple signals can be passed and used in the selector function.

### reduceSignal
Creates a signal similar to `Array.reduce` or Rxjs's `scan` operator, using a reducer function to create a new value from the current and prior values.

### sequenceSignal

The Sequence Signal is useful for situations where you want to easily cycle between options. For example, if you want to toggle between true/false or a list of sizes. These are still writable signals so you can manually override the current value.
Expand All @@ -85,7 +90,8 @@ This was directly inspired by Svelte's *tweened* function. When the signal valu
## Conventions

### SignalInput and ValueSource
Many arguments are of type **SignalInput<T>** or **ValueSource<T>**.
As much as possible signals the functions provided try to create signals from either values or other signals.
To accommodate this, many arguments are of type **SignalInput<T>** or **ValueSource<T>**.

*SignalInput* can be either something that can be either converted to a signal with *toSignal*, a function that can be passed to *computed* or a regular old *signal*. The purpose of this is to make things just a bit more convenient.

Expand Down
15 changes: 4 additions & 11 deletions projects/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { RouterLink, RouterOutlet } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import { DEMO_CONFIGURATIONS } from './demo-configuration';

@Component({
selector: 'app-root',
Expand Down Expand Up @@ -41,9 +42,9 @@ import { faGithub } from '@fortawesome/free-brands-svg-icons';
<li>
<h2 class="menu-title">Signals</h2>
<ul>
@for (l of generatorLinks; track l) {
@for (l of demos; track l) {
<li>
<a [routerLink]="l.path">{{l.label}}</a>
<a [routerLink]="l.route">{{l.name}}</a>
</li>
}
</ul>
Expand All @@ -60,13 +61,5 @@ import { faGithub } from '@fortawesome/free-brands-svg-icons';
export class AppComponent {
readonly faGithub = faGithub;

readonly generatorLinks = [
{ label: 'Debounce', path: '/debounce-signal' },
{ label: 'Extend', path: '/extend-signal' },
{ label: 'Lift', path: '/lift-signal' },
{ label: 'Map', path: '/map-signal' },
{ label: 'Sequence', path: '/sequence-signal' },
{ label: 'Timer', path: '/timer-signal' },
{ label: 'Tween', path: '/tween-signal' },
]
readonly demos = DEMO_CONFIGURATIONS;
}
18 changes: 10 additions & 8 deletions projects/demo/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { TimerSignalComponent } from './signal-generators/timer-signal.component';
import { DebounceSignalComponent } from './signal-generators/debounce-signal.component';
import { SequenceSignalComponent } from './signal-generators/sequence-signal.component';
import { MapSignalComponent } from './signal-generators/map-signal.component';
import { LiftSignalComponent } from './signal-generators/lift-signal.component';
import { ExtendSignalComponent } from './signal-generators/extend-signal.component';
import { TweenSignalComponent } from './signal-generators/tween-signal.component';
import { FilterSignalComponent } from './signal-generators/filter-signal.component';
import { DebounceSignalComponent } from './demos/debounce-signal.component';
import { ExtendSignalComponent } from './demos/extend-signal.component';
import { FilterSignalComponent } from './demos/filter-signal.component';
import { LiftSignalComponent } from './demos/lift-signal.component';
import { MapSignalComponent } from './demos/map-signal.component';
import { ReduceSignalComponent } from './demos/reduce-signal.component';
import { SequenceSignalComponent } from './demos/sequence-signal.component';
import { TimerSignalComponent } from './demos/timer-signal.component';
import { TweenSignalComponent } from './demos/tween-signal.component';

export const routes: Routes = [
{ path: '', component: HomeComponent },
Expand All @@ -16,6 +17,7 @@ export const routes: Routes = [
{ path: 'filter-signal', component: FilterSignalComponent },
{ path: 'lift-signal', component: LiftSignalComponent },
{ path: 'map-signal', component: MapSignalComponent },
{ path: 'reduce-signal', component: ReduceSignalComponent },
{ path: 'sequence-signal', component: SequenceSignalComponent },
{ path: 'timer-signal', component: TimerSignalComponent },
{ path: 'tween-signal', component: TweenSignalComponent },
Expand Down
10 changes: 10 additions & 0 deletions projects/demo/src/app/controls/contents-class.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Directive, HostBinding } from '@angular/core';

/** Adds contents to host. */
@Directive({
selector: '[appContentsClass]',
standalone: true
})
export class ContentsClassDirective {
@HostBinding('class.contents') contentsClass: boolean = true;
}
42 changes: 23 additions & 19 deletions projects/demo/src/app/controls/home-box.component.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { SignalGeneratorType, SignalTypeBadgeComponent } from './signal-type-badge.component';
import { DEMO_CONFIG_MAP, DemoConfigurationItem, SignalFunctionName } from '../demo-configuration';
import { ContentsClassDirective } from './contents-class.directive';
import { SignalTypeBadgeComponent } from './signal-type-badge.component';


@Component({
selector: 'app-home-box',
standalone: true,
imports: [RouterLink, SignalTypeBadgeComponent],
hostDirectives: [ContentsClassDirective],
template: `
<li class="card card-compact bg-base-100 hover:bg-base-200 shadow-lg">
<div class="card-body">
<h3 class="card-title cursor-pointer" [routerLink]="link">
<a class="link" [routerLink]="link">{{name}}</a>
@for (badgeType of badgeTypes; track badgeType) {
<app-signal-type-badge [type]="badgeType"/>
}
</h3>
<ng-content></ng-content>
</div>
</li>
@if ($demoConfig(); as demoConfig) {
<li class="card card-compact bg-base-100 hover:bg-base-200 shadow-lg">
<div class="card-body">
<h3 class="card-title cursor-pointer" [routerLink]="demoConfig.route">
<a class="link" [routerLink]="demoConfig.route">{{demoConfig.name}}</a>
@for (signalType of demoConfig.signalTypes; track signalType) {
<app-signal-type-badge [type]="signalType"/>
}
</h3>
<ng-content></ng-content>
</div>
</li>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeBoxComponent {
@Input({ required: true }) badgeTypes: SignalGeneratorType[] = [];
@Input({ required: true }) link: string = '';
@Input({ required: true }) name: string = '';

@HostBinding('class.contents') contentsClass: boolean = true;
readonly $demoConfig = signal<DemoConfigurationItem<string> | undefined>(undefined);

@Input({ required: true })
set fnName(value: SignalFunctionName) {
this.$demoConfig.set(DEMO_CONFIG_MAP[value]);
}
}
32 changes: 19 additions & 13 deletions projects/demo/src/app/controls/signal-header.component.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { SignalGeneratorType, SignalTypeBadgeComponent } from './signal-type-badge.component';
import { faFile } from '@fortawesome/free-solid-svg-icons';
import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faFile } from '@fortawesome/free-solid-svg-icons';
import { DEMO_CONFIG_MAP, DemoConfigurationItem, SignalFunctionName } from '../demo-configuration';
import { SignalTypeBadgeComponent } from './signal-type-badge.component';

@Component({
selector: 'app-signal-header',
standalone: true,
imports: [CommonModule, FontAwesomeModule, SignalTypeBadgeComponent],
template: `
<div class="flex flex-row items-baseline gap-3">
<h1>{{name}}</h1>
<div class="flex flex-row gap-1">
@for (type of types; track type) {
<app-signal-type-badge [type]="type"/>
@if($demoConfig(); as demoConfig) {
<h1>{{demoConfig.name}}</h1>
<div class="flex flex-row gap-1">
@for (type of demoConfig.signalTypes; track type) {
<app-signal-type-badge [type]="type"/>
}
</div>
@if (demoConfig.docUrl) {
<a class="btn btn-sm btn-warning self-end" [href]="demoConfig.docUrl"><fa-icon [icon]="faFile" /> API Docs</a>
}
</div>
@if (apiPath) {
<a class="btn btn-sm btn-warning self-end" [href]="apiPath"><fa-icon [icon]="faFile" /> API Docs</a>
}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SignalHeaderComponent {
readonly $demoConfig = signal<DemoConfigurationItem<string> | undefined>(undefined);

readonly faFile = faFile;

@Input({ required: true }) apiPath?: string;
@Input({ required: true }) name?: string;
@Input({ required: true }) types: SignalGeneratorType[] = [];
@Input({ required: true })
set fnName(value: SignalFunctionName) {
this.$demoConfig.set(DEMO_CONFIG_MAP[value]);
}
}
6 changes: 3 additions & 3 deletions projects/demo/src/app/controls/signal-type-badge.component.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SignalType } from '../demo-configuration';

export type SignalGeneratorType = 'signal' | 'generator';
@Component({
selector: 'app-signal-type-badge',
standalone: true,
imports: [CommonModule],
template: `
@switch (type) {
@case ('signal') {
@case ('writableSignal') {
<div class="badge leading-4 tooltip bg-green-300"
data-tip="This can be passed a value to create an writable signal."
aria-description="Badge indicating this can be passed a value to create an writable signal.">
Expand All @@ -28,7 +28,7 @@ export type SignalGeneratorType = 'signal' | 'generator';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SignalTypeBadgeComponent {
@Input({ required: true }) type: SignalGeneratorType = 'signal';
@Input({ required: true }) type: SignalType = 'writableSignal';


}
106 changes: 106 additions & 0 deletions projects/demo/src/app/demo-configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Type } from '@angular/core';
import { DebounceSignalComponent } from './home-demos/debounce-signal.component';
import { TweenSignalComponent } from './home-demos/tween-signal.component';
import { TimerSignalComponent } from './home-demos/timer-signal.component';
import { SequenceSignalComponent } from './home-demos/sequence-signal.component';
import { ReduceSignalComponent } from './home-demos/reduce-signal.component';
import { MapSignalComponent } from './home-demos/map-signal.component';
import { LiftSignalComponent } from './home-demos/lift-signal.component';
import { FilterSignalComponent } from './home-demos/filter-signal.component';
import { ExtendSignalComponent } from './home-demos/extend-signal.component';

/** What type of signals are returned from signal factory functions. */
export type SignalType = 'writableSignal' | 'generator';

export interface DemoConfigurationItem<FnName extends string> {
readonly component: Type<unknown>,
/** The url to the docs from the root. */
readonly docUrl: string;
/** Function name to generate signal. Acts as distinct key. */
readonly fnName: FnName;
/** Display name */
readonly name: string;
/** The route from the root of the app. */
readonly route: string;
readonly signalTypes: SignalType[];
}

export const DEMO_CONFIGURATIONS = [
{
component: DebounceSignalComponent,
docUrl: './api/functions/debounceSignal.html',
fnName: 'debounceSignal' as const,
name: 'Debounce',
route: 'debounce-signal',
signalTypes: ['generator', 'writableSignal']
},
{
component: ExtendSignalComponent,
docUrl: './api/functions/extendSignal.html',
fnName: 'extendSignal' as const,
name: 'Extend',
route: 'extend-signal',
signalTypes: ['generator', 'writableSignal']
},
{
component: FilterSignalComponent,
docUrl: './api/functions/filterSignal-1.html',
fnName: 'filterSignal' as const,
name: 'Filter',
route: 'filter-signal',
signalTypes: ['writableSignal']
},
{
component: LiftSignalComponent,
docUrl: './api/functions/liftSignal.html',
fnName: 'liftSignal' as const,
name: 'Lift',
route: 'lift-signal',
signalTypes: ['generator', 'writableSignal']
},
{
component: MapSignalComponent,
docUrl: './api/functions/mapSignal-1.html',
fnName: 'mapSignal' as const,
name: 'Map',
route: 'map-signal',
signalTypes: ['generator', 'writableSignal']
},
{
component: ReduceSignalComponent,
docUrl: './api/functions/reduceSignal-1.html',
fnName: 'reduceSignal' as const,
name: 'Reduce',
route: 'reduce-signal',
signalTypes: ['writableSignal']
},
{
component: SequenceSignalComponent,
docUrl: './api/functions/sequenceSignal-1.html',
fnName: 'sequenceSignal' as const,
name: 'Sequence',
route: 'sequence-signal',
signalTypes: ['generator', 'writableSignal']
},
{
component: TimerSignalComponent,
docUrl: './api/functions/timerSignal-1.html',
fnName: 'timerSignal' as const,
name: 'Timer',
route: 'timer-signal',
signalTypes: ['generator', 'writableSignal']
},
{
component: TweenSignalComponent,
docUrl: './api/functions/tweenSignal-1.html',
fnName: 'tweenSignal' as const,
name: 'Tween',
route: 'tween-signal',
signalTypes: ['generator', 'writableSignal']
}
] satisfies DemoConfigurationItem<string>[];

export type SignalFunctionName = typeof DEMO_CONFIGURATIONS[number]['fnName'];

export const DEMO_CONFIG_MAP = DEMO_CONFIGURATIONS.reduce((prior, cur) => ({ ...prior, [cur.fnName]: cur}),
{} as Record<SignalFunctionName, DemoConfigurationItem<SignalFunctionName>>)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ExampleCodeComponent } from '../controls/example-code.component';
standalone: true,
imports: [CommonModule, ExampleCodeComponent, FormsModule, SignalHeaderComponent],
template: `
<app-signal-header name="Debounce Signal" apiPath="./api/functions/debounceSignal.html" [types]="['generator', 'signal']" />
<app-signal-header fnName="debounceSignal" />
<p>
This is very similar to rxjs's <i>debounce</i> operator.
This has two overloads - one where it accepts a signal and the value is debounced in a readonly signal,
Expand Down
Loading

0 comments on commit 4c3f27b

Please sign in to comment.