Skip to content

Commit

Permalink
feat(progressbar): first draft of progressbar
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinderoubaix committed Aug 24, 2023
1 parent c40a8b7 commit ddb0241
Show file tree
Hide file tree
Showing 50 changed files with 1,437 additions and 68 deletions.
83 changes: 83 additions & 0 deletions angular/demo/src/app/samples/progressbar/custom.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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, map, startWith, 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 au-progressbar #progressbar [value]="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>
`,
})
export default class DefaultProgressBarComponent implements OnDestroy {
value = 0;
subscription: Subscription | undefined;

start() {
if (!this.subscription) {
this.subscription = interval(500)
.pipe(
startWith(-1),
map((val) => 10 * (val + 2)),
takeWhile((val) => val <= 100)
)
.subscribe((val) => {
this.value = val;
});
}
}
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();
}
}
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, showPercentage: true};
return config;
}),
],
template: `
<div class="d-flex flex-column gap-2">
<div au-progressbar [value]="20"></div>
<div au-progressbar [value]="40" className="text-bg-success"></div>
<div au-progressbar [value]="60" className="text-bg-info"></div>
<div au-progressbar [value]="80" className="text-bg-warning"></div>
<div au-progressbar [value]="100" className="text-bg-danger"></div>
</div>
`,
})
export default class DefaultProgressBarComponent {}
23 changes: 23 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,23 @@
import type {RatingComponent} from '@agnos-ui/angular';
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 au-progressbar #widget></div>`,
})
export default class PlaygroundComponent {
@ViewChild('widget') widget: RatingComponent;

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

@Component({
standalone: true,
imports: [ProgressbarComponent],
template: `
<div class="pb-2">
A progressbar using custom values for minimum and maximum:
<div au-progressbar [minimum]="1" [maximum]="5" [value]="4" ariaLabel="Step 4 out of 5">Step 4 out of 5</div>
</div>
<div class="pb-2">
A striped animated progress bar:
<div au-progressbar className="text-bg-info" [value]="63" [striped]="true" [animated]="true"></div>
</div>
<div class="pb-2">
Changing the height:
<div au-progressbar [height]="'1.5rem'" [value]="47"></div>
</div>
`,
})
export default class StripedProgressBarComponent {}
1 change: 1 addition & 0 deletions angular/demo/src/assets/coffee-img-1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions angular/demo/src/assets/coffee-img-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions angular/demo/src/styles.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/* You can add global styles to this file, and also import other style files */

@import 'bootstrap/dist/css/bootstrap.css';

.cup-fill {
background: url('assets/coffee-img-1.svg'), url('assets/coffee-img-2.svg');
}
141 changes: 141 additions & 0 deletions angular/lib/src/lib/progressbar/progressbar.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type {AdaptSlotContentProps, AdaptWidgetSlots, SlotContent} from '../slot.directive';
import {ComponentTemplate, SlotDirective, callWidgetFactory} from '../slot.directive';
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 type {ProgressbarContext as ProgressbarCoreContext, WidgetProps, WidgetState} from '@agnos-ui/core';
import {createProgressbar, toSlotContextWidget} from '@agnos-ui/core';
import {writable} from '@amadeus-it-group/tansu';
import {toSignal} from '@angular/core/rxjs-interop';
import {patchSimpleChanges} from '../utils';
import {SlotDefaultDirective} from '../slotDefault.directive';

export type ProgressbarWidget = AdaptWidgetSlots<ReturnType<typeof createProgressbar>>;
export type ProgressbarState = WidgetState<ProgressbarWidget>;
export type ProgressbarProps = WidgetProps<ProgressbarWidget>;

export type ProgressbarContext = AdaptSlotContentProps<ProgressbarCoreContext>;

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

Check warning on line 23 in angular/lib/src/lib/progressbar/progressbar.component.ts

View check run for this annotation

Codecov / codecov/patch

angular/lib/src/lib/progressbar/progressbar.component.ts#L23

Added line #L23 was not covered by tests
}
}

@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>
<ng-container *ngIf="state.showPercentage">{{ state.percentage }}%</ng-container>
</div>
</div>
</ng-template>
`,
})
export class ProgressbarDefaultSlotsComponent {
@ViewChild('content', {static: true}) content: TemplateRef<ProgressbarCoreContext>;
}

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

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

@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'div[au-progressbar]',
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().minimum',
'[attr.aria-valuemax]': 'state().maximum',
},
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() ariaLabel: string | undefined;

/**
* the minimum value
*/
@Input() minimum: number | undefined;

/**
* the maximum value
*/
@Input() maximum: number | undefined;

/**
* the current value
*/
@Input() value: number | undefined;

/**
* class to add to the content
*/
@Input() className: string | undefined;

@Input() slotDefault: SlotContent<AdaptSlotContentProps<ProgressbarCoreContext>>;
@Input() slotContent: SlotContent<AdaptSlotContentProps<ProgressbarCoreContext>>;
@ContentChild(ProgressbarContentDirective, {static: false}) slotContentFromContent: ProgressbarContentDirective | undefined;

/**
* if `true`, display the current percentage in the `xx%` format
*/
@Input() showPercentage: boolean | undefined;

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

/**
* if `true`, animates a striped progressbar
*/
@Input() animated: boolean | undefined;

/**
* if `true`, shows a striped progressbar
*/
@Input() striped: boolean | 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);
}
}
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/modal/modal.component';
export * from './lib/alert/alert.component';
export * from './lib/accordion/accordion.component';
export * from './lib/progressbar/progressbar.component';
Loading

0 comments on commit ddb0241

Please sign in to comment.