Skip to content

Commit

Permalink
feat: support and enforce number and boolean transform functions in a…
Browse files Browse the repository at this point in the history
…ngular
  • Loading branch information
quentinderoubaix committed Nov 15, 2023
1 parent 5a09508 commit b7f4e21
Show file tree
Hide file tree
Showing 23 changed files with 341 additions and 87 deletions.
4 changes: 2 additions & 2 deletions angular/demo/src/app/samples/accordion/default.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import BODY from '!raw-loader!@agnos-ui/common/samples/accordion/body.txt';
imports: [AgnosUIAngularModule],
template: `
<div auAccordion>
<div auAccordionItem [auItemVisible]="true">
<div auAccordionItem auItemVisible>
<ng-template auAccordionItemHeader>Simple</ng-template>
<ng-template auAccordionItemBody>{{ BODY }} </ng-template>
</div>
Expand All @@ -17,7 +17,7 @@ import BODY from '!raw-loader!@agnos-ui/common/samples/accordion/body.txt';
>
<ng-template auAccordionItemBody>{{ BODY }} </ng-template>
</div>
<div auAccordionItem [auItemDisabled]="true">
<div auAccordionItem auItemDisabled>
<ng-template auAccordionItemHeader>Disabled</ng-template>
<ng-template auAccordionItemBody>{{ BODY }} </ng-template>
</div>
Expand Down
4 changes: 2 additions & 2 deletions angular/demo/src/app/samples/pagination/custom.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ const FILTER_PAG_REGEX = /[^0-9]/g;
imports: [AgnosUIAngularModule],
template: `
<p>A pagination with customized links:</p>
<nav auPagination [auCollectionSize]="60" [(auPage)]="customPage" auAriaLabel="Page navigation with customized links">
<nav auPagination auCollectionSize="60" [(auPage)]="customPage" auAriaLabel="Page navigation with customized links">
<ng-template auPaginationPrevious>Prev</ng-template>
<ng-template auPaginationNext>Next</ng-template>
<ng-template auPaginationNumber let-displayedPage="displayedPage">{{ getPageSymbol(displayedPage) }}</ng-template>
</nav>
<hr />
<p>A pagination with customized pages:</p>
<nav auPagination [auCollectionSize]="60" [(auPage)]="customPage" auAriaLabel="Page navigation with customized pages">
<nav auPagination auCollectionSize="60" [(auPage)]="customPage" auAriaLabel="Page navigation with customized pages">
<ng-template auPaginationPages let-widget="widget" let-state="state">
@if (state.pages.length > 0) {
<li class="au-custom-pages-item">
Expand Down
8 changes: 4 additions & 4 deletions angular/demo/src/app/samples/pagination/default.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import {Component} from '@angular/core';
imports: [AgnosUIAngularModule],
template: `
<h5>Basic pagination:</h5>
<nav auPagination [(auPage)]="page" [auCollectionSize]="60"></nav>
<nav auPagination auCollectionSize="60" [(auPage)]="page"></nav>
<h5>No direction links:</h5>
<nav auPagination [auCollectionSize]="60" [(auPage)]="page" [auDirectionLinks]="false"></nav>
<nav auPagination auCollectionSize="60" [(auPage)]="page" [auDirectionLinks]="false"></nav>
<h5>With boundary links:</h5>
<nav auPagination [auCollectionSize]="60" [(auPage)]="page" [auBoundaryLinks]="true"></nav>
<nav auPagination auCollectionSize="60" [(auPage)]="page" auBoundaryLinks></nav>
<div class="mb-3">
Current page: <span id="defaultPage">{{ page }}</span>
</div>
<h5>Disabled pagination:</h5>
<nav auPagination [auCollectionSize]="60" [(auPage)]="pageAlone" auAriaLabel="Disabled pagination" [auDisabled]="true"></nav>
<nav auPagination auCollectionSize="60" [(auPage)]="pageAlone" auAriaLabel="Disabled pagination" auDisabled></nav>
`,
})
export default class DefaultPaginationComponent {
Expand Down
10 changes: 5 additions & 5 deletions angular/demo/src/app/samples/progressbar/default.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import {Component} from '@angular/core';
],
template: `
<div class="d-flex flex-column gap-2">
<div auProgressbar [auValue]="20"></div>
<div auProgressbar [auValue]="40" auClassName="text-bg-success"></div>
<div auProgressbar [auValue]="60" auClassName="text-bg-info"></div>
<div auProgressbar [auValue]="80" auClassName="text-bg-warning"></div>
<div auProgressbar [auValue]="100" auClassName="text-bg-danger"></div>
<div auProgressbar auValue="20"></div>
<div auProgressbar auValue="40" auClassName="text-bg-success"></div>
<div auProgressbar auValue="60" auClassName="text-bg-info"></div>
<div auProgressbar auValue="80" auClassName="text-bg-warning"></div>
<div auProgressbar auValue="100" auClassName="text-bg-danger"></div>
</div>
`,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import {Component} from '@angular/core';
<div class="d-flex flex-column gap-2">
<div>
A progressbar using custom values for minimum and maximum:
<div auProgressbar [auMin]="1" [auMax]="5" [auValue]="4" [auAriaValueTextFn]="valueText">Step 4 out of 5</div>
<div auProgressbar auMin="1" auMax="5" auValue="4" [auAriaValueTextFn]="valueText">Step 4 out of 5</div>
</div>
<div>
A striped animated progress bar:
<div auProgressbar auClassName="text-bg-info" [auValue]="63" [auStriped]="true" [auAnimated]="true"></div>
<div auProgressbar auClassName="text-bg-info" auValue="63" auStriped auAnimated></div>
</div>
<div>
Changing the height:
<div auProgressbar [auHeight]="'1.5rem'" [auValue]="47"></div>
<div auProgressbar [auHeight]="'1.5rem'" auValue="47"></div>
</div>
</div>
`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {Component} from '@angular/core';
standalone: true,
imports: [AgnosUIAngularModule],
template: `
<div class="rating-custom" [auRating]="7" auAriaLabel="custom rating">
<div class="rating-custom" auRating="7" auAriaLabel="custom rating">
<ng-template auRatingStar let-fill="fill" let-index="index">
<span class="star" [class.filled]="fill === 100" [class.bad]="index < 3">&#9733;</span>
</ng-template>
Expand Down
2 changes: 1 addition & 1 deletion angular/demo/src/app/samples/rating/readonly.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {DomSanitizer} from '@angular/platform-browser';
<span [innerHTML]="sanitizer.bypassSecurityTrustHtml(heartFill)"></span>
</span>
</ng-template>
<div class="rating-readonly" [auRating]="3.64" [auSlotStar]="custom" [auReadonly]="true" [auMaxRating]="5" auAriaLabel="readonly rating"></div>
<div class="rating-readonly" auRating="3.64" [auSlotStar]="custom" auReadonly auMaxRating="5" auAriaLabel="readonly rating"></div>
`,
encapsulation: ViewEncapsulation.None,
styles: "@import '@agnos-ui/common/samples/rating/readonly.scss';",
Expand Down
6 changes: 3 additions & 3 deletions angular/demo/src/app/samples/slider/default.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
imports: [AgnosUIAngularModule, ReactiveFormsModule, FormsModule],
template: `
<h2>Slider with form control</h2>
<div auSlider [auMin]="0" [auMax]="100" [auStepSize]="1" [formControl]="sliderControl"></div>
<div auSlider auMin="0" auMax="100" auStepSize="1" [formControl]="sliderControl"></div>
Form control value:
{{ sliderControl.value }}
<hr />
<h2>Slider with value</h2>
<div auSlider [auMin]="0" [auMax]="100" [auStepSize]="1" [(auValues)]="sliderValues"></div>
<div auSlider auMin="0" auMax="100" auStepSize="1" [(auValues)]="sliderValues"></div>
Value:
{{ sliderValues }}
<hr />
<h2>Disabled slider</h2>
<div auSlider [auMin]="0" [auMax]="100" [auStepSize]="1" [formControl]="disabledControl" [auReadonly]="readonlyToggle"></div>
<div auSlider auMin="0" auMax="100" auStepSize="1" [formControl]="disabledControl" [auReadonly]="readonlyToggle"></div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="disabled" [(ngModel)]="disabledToggle" (change)="handleDisabled()" />
Expand Down
4 changes: 2 additions & 2 deletions angular/demo/src/app/samples/slider/range.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
imports: [AgnosUIAngularModule, ReactiveFormsModule, FormsModule],
template: `
<h2>Slider with form control</h2>
<div auSlider [auMin]="0" [auMax]="100" [auStepSize]="1" [formControl]="sliderControl"></div>
<div auSlider auMin="0" auMax="100" auStepSize="1" [formControl]="sliderControl"></div>
Form control values: {{ sliderControl.value?.join(', ') }}
<hr />
<h2>Slider with values</h2>
<div auSlider [auMin]="0" [auMax]="100" [auStepSize]="1" [(auValues)]="sliderValues"></div>
<div auSlider auMin="0" auMax="100" auStepSize="1" [(auValues)]="sliderValues"></div>
Values: {{ sliderValues.join(', ') }}
`,
})
Expand Down
4 changes: 2 additions & 2 deletions angular/demo/src/app/samples/slider/vertical.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
template: `
<div class="d-flex" style="height: 350px">
<div class="col-6" style="height: 300px">
<div auSlider [auMin]="0" [auMax]="100" [auStepSize]="1" [auVertical]="true" [formControl]="sliderControl" auClassName="my-0"></div>
<div auSlider auMin="0" auMax="100" auStepSize="1" auVertical [formControl]="sliderControl" auClassName="my-0"></div>
<div class="mt-3">Form control values: {{ sliderControl.value?.join(', ') }}</div>
</div>
<div class="col-6" style="height: 300px">
<div auSlider [auMin]="0" [auMax]="100" [auStepSize]="1" [auVertical]="true" [formControl]="sliderControlRange" auClassName="my-0"></div>
<div auSlider auMin="0" auMax="100" auStepSize="1" auVertical [formControl]="sliderControlRange" auClassName="my-0"></div>
<div class="mt-3">From control value: {{ sliderControlRange.value?.join(', ') }}</div>
</div>
</div>
Expand Down
112 changes: 112 additions & 0 deletions angular/headless/src/lib/coercion.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {auBooleanAttribute, auNumberAttribute} from './coercion';
import {describe, expect, it} from 'vitest';

describe('coercion functions', () => {
describe('auBooleanAttribute', () => {
it('should coerce undefined to undefined', () => {
expect(auBooleanAttribute(undefined)).toBe(undefined);
});

it('should coerce null to false', () => {
expect(auBooleanAttribute(null)).toBe(false);
});

it('should coerce the empty string to true', () => {
expect(auBooleanAttribute('')).toBe(true);
});

it('should coerce zero to true', () => {
expect(auBooleanAttribute(0)).toBe(true);
});

it('should coerce the string "false" to false', () => {
expect(auBooleanAttribute('false')).toBe(false);
});

it('should coerce the boolean false to false', () => {
expect(auBooleanAttribute(false)).toBe(false);
});

it('should coerce the boolean true to true', () => {
expect(auBooleanAttribute(true)).toBe(true);
});

it('should coerce the string "true" to true', () => {
expect(auBooleanAttribute('true')).toBe(true);
});

it('should coerce an arbitrary string to true', () => {
expect(auBooleanAttribute('pink')).toBe(true);
});

it('should coerce an object to true', () => {
expect(auBooleanAttribute({})).toBe(true);
});

it('should coerce an array to true', () => {
expect(auBooleanAttribute([])).toBe(true);
});
});

describe('auNumberAttribute', () => {
it('should coerce undefined to undefined', () => {
expect(auNumberAttribute(undefined)).toBe(undefined);
});

it('should coerce null to NaN', () => {
expect(auNumberAttribute(null)).toBeNaN();
});

it('should coerce true to NaN', () => {
expect(auNumberAttribute(true)).toBeNaN();
});

it('should coerce false to NaN', () => {
expect(auNumberAttribute(false)).toBeNaN();
});

it('should coerce the empty string to NaN', () => {
expect(auNumberAttribute('')).toBeNaN();
});

it('should coerce the string "1" to 1', () => {
expect(auNumberAttribute('1')).toBe(1);
});

it('should coerce the string "123.456" to 123.456', () => {
expect(auNumberAttribute('123.456')).toBe(123.456);
});

it('should coerce the string "-123.456" to -123.456', () => {
expect(auNumberAttribute('-123.456')).toBe(-123.456);
});

it('should coerce an arbitrary string to NaN', () => {
expect(auNumberAttribute('pink')).toBeNaN();
});

it('should coerce an arbitrary string prefixed with a number to NaN', () => {
expect(auNumberAttribute('123pink')).toBeNaN();
});

it('should coerce the number 1 to 1', () => {
expect(auNumberAttribute(1)).toBe(1);
});

it('should coerce the number 123.456 to 123.456', () => {
expect(auNumberAttribute(123.456)).toBe(123.456);
});

it('should coerce the number -123.456 to -123.456', () => {
expect(auNumberAttribute(-123.456)).toBe(-123.456);
});

it('should coerce an object to NaN', () => {
expect(auNumberAttribute({})).toBeNaN();
});

it('should coerce an array to NaN', () => {
expect(auNumberAttribute([])).toBeNaN();
});
});
});
35 changes: 35 additions & 0 deletions angular/headless/src/lib/coercion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {booleanAttribute, numberAttribute} from '@angular/core';

/**
* Transforms a value (typically a string) to a boolean.
* Intended to be used as a transform function of an input.
*
* @usageNotes
* ```typescript
* @Input({ transform: auBooleanAttribute }) status: boolean | undefined;
* ```
* @param value Value to be transformed.
*/
export function auBooleanAttribute(value: unknown): boolean | undefined {
if (value === undefined) {
return undefined;
}
return booleanAttribute(value);
}

/**
* Transforms a value (typically a string) to a number.
* Intended to be used as a transform function of an input.
* @param value Value to be transformed.
*
* @usageNotes
* ```typescript
* @Input({ transform: auNumberAttribute }) id: number | undefined;
* ```
*/
export function auNumberAttribute(value: unknown): number | undefined {
if (value === undefined) {
return undefined;
}
return numberAttribute(value);
}
1 change: 1 addition & 0 deletions angular/headless/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './lib/slotTypes';
export type {SlotContent} from './lib/slotTypes';
export * from './lib/use.directive';
export * from './lib/utils';
export * from './lib/coercion';

import type {PropsConfig, WidgetFactory, WidgetProps, WidgetState} from '@agnos-ui/core';
import {
Expand Down
19 changes: 10 additions & 9 deletions angular/lib/src/lib/accordion/accordion.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ComponentTemplate,
SlotDirective,
UseDirective,
auBooleanAttribute,
callWidgetFactory,
createAccordion,
patchSimpleChanges,
Expand Down Expand Up @@ -199,21 +200,21 @@ export class AccordionItemComponent implements OnChanges, AfterContentChecked, A
/**
* If `true`, the content of the accordion-item collapse will be removed from the DOM. It will be just hidden otherwise.
*/
@Input('auItemDestroyOnHide') itemDestroyOnHide: boolean | undefined;
@Input({alias: 'auItemDestroyOnHide', transform: auBooleanAttribute}) itemDestroyOnHide: boolean | undefined;
/**
* If `true`, the accordion-item will be disabled.
* It will not react to user's clicks, but still will be possible to toggle programmatically.
*/
@Input('auItemDisabled') itemDisabled: boolean | undefined;
@Input({alias: 'auItemDisabled', transform: auBooleanAttribute}) itemDisabled: boolean | undefined;

/**
* If `true`, the accordion-item will be visible (expanded). Otherwise, it will be hidden (collapsed).
*/
@Input('auItemVisible') itemVisible: boolean | undefined;
@Input({alias: 'auItemVisible', transform: auBooleanAttribute}) itemVisible: boolean | undefined;
/**
* If `true`, accordion-item will be animated.
*/
@Input('auItemAnimation') itemAnimation: boolean | undefined;
@Input({alias: 'auItemAnimation', transform: auBooleanAttribute}) itemAnimation: boolean | undefined;
/**
* Classes to add on the accordion-item header DOM element.
*/
Expand Down Expand Up @@ -295,7 +296,7 @@ export class AccordionDirective implements OnChanges {
/**
* If `true`, only one item at the time can stay open.
*/
@Input('auCloseOthers') closeOthers: boolean | undefined;
@Input({alias: 'auCloseOthers', transform: auBooleanAttribute}) closeOthers: boolean | undefined;

/**
* CSS classes to be applied on the widget main container
Expand Down Expand Up @@ -326,27 +327,27 @@ export class AccordionDirective implements OnChanges {
*
* It is a prop of the accordion-item.
*/
@Input('auItemDestroyOnHide') itemDestroyOnHide: boolean | undefined;
@Input({alias: 'auItemDestroyOnHide', transform: auBooleanAttribute}) itemDestroyOnHide: boolean | undefined;
/**
* If `true`, the accordion-item will be disabled.
* It will not react to user's clicks, but still will be possible to toggle programmatically.
*
* It is a prop of the accordion-item.
*/
@Input('auItemDisabled') itemDisabled: boolean | undefined;
@Input({alias: 'auItemDisabled', transform: auBooleanAttribute}) itemDisabled: boolean | undefined;

/**
* If `true`, the accordion-item will be visible (expanded). Otherwise, it will be hidden (collapsed).
*
* It is a prop of the accordion-item.
*/
@Input('auItemVisible') itemVisible: boolean | undefined;
@Input({alias: 'auItemVisible', transform: auBooleanAttribute}) itemVisible: boolean | undefined;
/**
* If `true`, accordion-item will be animated.
*
* It is a prop of the accordion-item.
*/
@Input('auItemAnimation') itemAnimation: boolean | undefined;
@Input({alias: 'auItemAnimation', transform: auBooleanAttribute}) itemAnimation: boolean | undefined;
/**
* The transition to use for the accordion-item collapse when is toggled.
*
Expand Down
Loading

0 comments on commit b7f4e21

Please sign in to comment.