Skip to content

Commit

Permalink
feat: addition of progressbar widget (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinderoubaix authored Sep 19, 2023
1 parent 909f58a commit f6600cb
Show file tree
Hide file tree
Showing 44 changed files with 1,521 additions and 3 deletions.
23 changes: 23 additions & 0 deletions angular/demo/src/app/samples/progressbar/default.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {ProgressbarComponent, provideWidgetsConfig} from '@agnos-ui/angular';
import {Component} from '@angular/core';

@Component({
standalone: true,
imports: [ProgressbarComponent],
providers: [
provideWidgetsConfig((config) => {
config.progressbar = {...config.progressbar, slotDefault: (widget) => `${widget.state.percentage}%`};
return config;
}),
],
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>
`,
})
export default class DefaultProgressBarComponent {}
80 changes: 80 additions & 0 deletions angular/demo/src/app/samples/progressbar/fullCustom.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {ProgressbarComponent, ProgressbarContentDirective} from '@agnos-ui/angular';
import {NgIf} from '@angular/common';
import type {OnDestroy} from '@angular/core';
import {Component} from '@angular/core';
import type {Subscription} from 'rxjs';
import {interval, takeWhile} from 'rxjs';

@Component({
standalone: true,
imports: [ProgressbarComponent, ProgressbarContentDirective, NgIf],
template: `
<div class="d-flex align-items-center flex-wrap">
<div style="width: 350px">
<div auProgressbar #progressbar [auValue]="value">
<ng-template auProgressbarContent let-state="state">
<div class="position-relative" style="height: 300px">
<div class="cup">
<div class="cup-fill-parent">
<div class="cup-fill" [style.height.px]="value * 1.7">
<div class="bubble bubble-1" *ngIf="value >= 50"></div>
<div class="bubble bubble-2" *ngIf="value >= 50"></div>
<div class="bubble bubble-3" *ngIf="value >= 50"></div>
</div>
</div>
</div>
</div>
</ng-template>
</div>
</div>
<div class="d-flex flex-column justify-content-evenly h-100 ms-5">
<div class="btn-group" role="group">
<button class="btn btn-outline-primary" (click)="start()" [disabled]="progressbar.state().started">Start</button>
<button
class="btn btn-outline-primary"
[disabled]="!progressbar.state().started || progressbar.state().finished"
(click)="toggleProgress()"
>
{{ subscription ? 'Pause' : 'Resume' }}
</button>
<button class="btn btn-outline-primary" [disabled]="!progressbar.state().started" (click)="stop(true)">Reset</button>
</div>
<p class="mt-3">
<span>{{ value === 0 ? 'Need to wake up.' : value < 100 ? 'Retrieving coffee... ' + value + '%' : 'Ready to work !' }}</span>
</p>
</div>
</div>
`,
styles: ["@import '@agnos-ui/common/samples/progressbar/custom.scss';"],
})
export default class FullCustomProgressBarComponent implements OnDestroy {
value = 0;
subscription: Subscription | undefined;

start() {
if (!this.subscription) {
this.subscription = interval(500)
.pipe(takeWhile(() => this.value < 100))
.subscribe(() => {
this.value += 10;
});
}
}
stop(reset = false) {
this.subscription?.unsubscribe();
this.subscription = undefined;
if (reset) {
this.value = 0;
}
}
toggleProgress() {
if (this.subscription) {
this.stop();
} else {
this.start();
}
}
ngOnDestroy() {
this.stop();
}
}
22 changes: 22 additions & 0 deletions angular/demo/src/app/samples/progressbar/playground.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {ProgressbarComponent} from '@agnos-ui/angular';
import {getProgressbarDefaultConfig} from '@agnos-ui/core';
import {Component, ViewChild} from '@angular/core';
import {getUndefinedValues, hashChangeHook, provideHashConfig} from '../../utils';

const undefinedConfig = getUndefinedValues(getProgressbarDefaultConfig());

@Component({
standalone: true,
imports: [ProgressbarComponent],
providers: provideHashConfig('progressbar'),
template: `<div auProgressbar #widget></div>`,
})
export default class PlaygroundComponent {
@ViewChild('widget') widget: ProgressbarComponent;

constructor() {
hashChangeHook((props) => {
this.widget?._widget.patch({...undefinedConfig, ...props});
});
}
}
26 changes: 26 additions & 0 deletions angular/demo/src/app/samples/progressbar/simpleCustom.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {ProgressbarComponent} from '@agnos-ui/angular';
import {Component} from '@angular/core';

@Component({
standalone: true,
imports: [ProgressbarComponent],
template: `
<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>
<div>
A striped animated progress bar:
<div auProgressbar auClassName="text-bg-info" [auValue]="63" [auStriped]="true" [auAnimated]="true"></div>
</div>
<div>
Changing the height:
<div auProgressbar [auHeight]="'1.5rem'" [auValue]="47"></div>
</div>
</div>
`,
})
export default class SimpleCustomProgressBarComponent {
readonly valueText = (val: number, _min: number, max: number) => `Step ${val} out of ${max}`;
}
5 changes: 5 additions & 0 deletions angular/headless/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ export type RatingState = WidgetState<RatingWidget>;
export type SelectWidget<Item> = AdaptWidgetSlots<import('@agnos-ui/core').SelectWidget<Item>>;
export type SelectProps<Item> = WidgetProps<SelectWidget<Item>>;
export type SelectState<Item> = WidgetState<SelectWidget<Item>>;

export type ProgressbarWidget = AdaptWidgetSlots<import('@agnos-ui/core').ProgressbarWidget>;
export type ProgressbarProps = WidgetProps<ProgressbarWidget>;
export type ProgressbarState = WidgetState<ProgressbarWidget>;
export type ProgressbarContext = AdaptSlotContentProps<import('@agnos-ui/core').ProgressbarContext>;
140 changes: 140 additions & 0 deletions angular/lib/src/lib/progressbar/progressbar.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type {ProgressbarContext, ProgressbarProps, ProgressbarState, SlotContent} from '@agnos-ui/angular-headless';
import {
ComponentTemplate,
SlotDirective,
callWidgetFactory,
createProgressbar,
patchSimpleChanges,
toSlotContextWidget,
SlotDefaultDirective,
} from '@agnos-ui/angular-headless';
import type {AfterContentChecked, OnChanges, Signal, SimpleChanges} from '@angular/core';
import {NgClass, NgIf} from '@angular/common';
import {ChangeDetectionStrategy, Component, ContentChild, Directive, Input, TemplateRef, ViewChild, inject} from '@angular/core';
import {writable} from '@amadeus-it-group/tansu';
import {toSignal} from '@angular/core/rxjs-interop';

@Directive({selector: 'ng-template[auProgressbarContent]', standalone: true})
export class ProgressbarContentDirective {
public templateRef = inject(TemplateRef<ProgressbarContext>);
static ngTemplateContextGuard(_dir: ProgressbarContentDirective, context: unknown): context is ProgressbarContext {
return true;
}
}

@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgClass, NgIf, SlotDirective, ProgressbarContentDirective],
template: `
<ng-template auProgressbarContent #content let-state="state" let-widget="widget">
<div class="progress" [style.height]="state.height">
<div
class="progress-bar"
[class.progress-bar-striped]="state.striped"
[class.progress-bar-animated]="state.animated"
[ngClass]="state.className"
[style.width.%]="state.percentage"
>
<ng-template [auSlot]="state.slotDefault" [auSlotProps]="{state, widget}"></ng-template>
</div>
</div>
</ng-template>
`,
})
export class ProgressbarDefaultSlotsComponent {
@ViewChild('content', {static: true}) content: TemplateRef<ProgressbarContext>;
}

export const progressbarDefaultSlotContent = new ComponentTemplate(ProgressbarDefaultSlotsComponent, 'content');

const defaultConfig: Partial<ProgressbarProps> = {
slotContent: progressbarDefaultSlotContent,
};

@Component({
selector: '[auProgressbar]',
standalone: true,
imports: [SlotDirective, SlotDefaultDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
role: 'progressbar',
'[attr.aria-label]': 'state().ariaLabel || undefined',
'[attr.aria-valuenow]': 'state().value',
'[attr.aria-valuemin]': 'state().min',
'[attr.aria-valuemax]': 'state().max',
'[attr.aria-valuetext]': 'state().ariaValueText',
},
template: `
<ng-template [auSlotDefault]="defaultSlots"><ng-content></ng-content></ng-template>
<ng-template [auSlot]="state().slotContent" [auSlotProps]="{state: state(), widget}"></ng-template>
`,
})
export class ProgressbarComponent implements AfterContentChecked, OnChanges {
readonly defaultSlots = writable(defaultConfig);

/**
* The aria label.
*/
@Input('auAriaLabel') ariaLabel: string | undefined;

/**
* The minimum value.
*/
@Input('auMin') min: number | undefined;

/**
* The maximum value.
*/
@Input('auMax') max: number | undefined;

/**
* The current value.
*/
@Input('auValue') value: number | undefined;

/**
* CSS classes to be applied on the widget main container
*/
@Input('auClassName') className: string | undefined;

@Input('auSlotDefault') slotDefault: SlotContent<ProgressbarContext>;
@Input('auSlotContent') slotContent: SlotContent<ProgressbarContext>;
@ContentChild(ProgressbarContentDirective, {static: false}) slotContentFromContent: ProgressbarContentDirective | undefined;

/**
* Height of the progressbar, can be any valid css height value.
*/
@Input('auHeight') height: string | undefined;

/**
* If `true`, animates a striped progressbar.
* Takes effect only for browsers supporting CSS3 animations, and if `striped` is `true`.
*/
@Input('auAnimated') animated: boolean | undefined;

/**
* If `true`, shows a striped progressbar.
*/
@Input('auStriped') striped: boolean | undefined;

/**
* Return the value for the 'aria-valuetext' attribute.
*/
@Input('auAriaValueTextFn') ariaValueTextFn: ((value: number, minimum: number, maximum: number) => string | undefined) | undefined;

readonly _widget = callWidgetFactory(createProgressbar, 'progressbar', this.defaultSlots);
readonly widget = toSlotContextWidget(this._widget);
readonly api = this._widget.api;
readonly state: Signal<ProgressbarState> = toSignal(this._widget.state$, {requireSync: true});

ngAfterContentChecked(): void {
this._widget.patch({
slotContent: this.slotContentFromContent?.templateRef,
});
}

ngOnChanges(changes: SimpleChanges): void {
patchSimpleChanges(this._widget.patch, changes);
}
}
2 changes: 1 addition & 1 deletion angular/lib/src/lib/rating/rating.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class RatingComponent implements ControlValueAccessor, OnChanges, AfterCo
}

/**
* Return the value for the 'aria-value' attribute.
* Return the value for the 'aria-valuetext' attribute.
*/
@Input('auAriaValueTextFn') ariaValueTextFn: ((rating: number, maxRating: number) => string) | undefined;

Expand Down
1 change: 1 addition & 0 deletions angular/lib/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './lib/modal/modal.service';
export * from './lib/pagination/pagination.component';
export * from './lib/rating/rating.component';
export * from './lib/select/select.component';
export * from './lib/progressbar/progressbar.component';
Loading

0 comments on commit f6600cb

Please sign in to comment.