Skip to content

Commit

Permalink
feat(collapse): add the collapse (#911)
Browse files Browse the repository at this point in the history
  • Loading branch information
ExFlo authored Oct 16, 2024
1 parent cad0845 commit 39cfe8b
Show file tree
Hide file tree
Showing 42 changed files with 704 additions and 15 deletions.
2 changes: 2 additions & 0 deletions angular/bootstrap/src/agnos-ui-angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import {SliderComponent, SliderHandleDirective, SliderLabelDirective, SliderStructureDirective} from './components/slider/slider.component';
import {ProgressbarComponent, ProgressbarStructureDirective} from './components/progressbar/progressbar.component';
import {ToastBodyDirective, ToastComponent, ToastHeaderDirective, ToastStructureDirective} from './components/toast/toast.component';
import {CollapseDirective} from './components/collapse';
/* istanbul ignore next */
const components = [
SlotDirective,
Expand Down Expand Up @@ -75,6 +76,7 @@ const components = [
ToastStructureDirective,
ToastBodyDirective,
ToastHeaderDirective,
CollapseDirective,
];

@NgModule({
Expand Down
91 changes: 91 additions & 0 deletions angular/bootstrap/src/components/collapse/collapse.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {auBooleanAttribute, BaseWidgetDirective, useDirectiveForHost} from '@agnos-ui/angular-headless';
import type {CollapseWidget} from '@agnos-ui/core-bootstrap/components/collapse';
import {createCollapse} from '@agnos-ui/core-bootstrap/components/collapse';
import {Directive, EventEmitter, Input, Output} from '@angular/core';
import {callWidgetFactory} from '../../config';

@Directive({
selector: '[auCollapse]',
standalone: true,
exportAs: 'auCollapse',
})
export class CollapseDirective extends BaseWidgetDirective<CollapseWidget> {
/**
* If `true`, collapse opening will be animated at init time.
*
* @defaultValue `false`
*/
@Input({alias: 'auAnimatedOnInit', transform: auBooleanAttribute}) animatedOnInit: boolean | undefined;

/**
* If `true`, collapse closing and opening will be animated.
*
* @defaultValue `true`
*/
@Input({alias: 'auAnimated', transform: auBooleanAttribute}) animated: boolean | undefined;

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

/**
* If `true`, collapse will be done horizontally.
*
* @defaultValue `false`
*/
@Input({alias: 'auHorizontal', transform: auBooleanAttribute}) horizontal: boolean | undefined;

/**
* If `true` the collapse is visible to the user
*
* @defaultValue `true`
*/
@Input({alias: 'auVisible', transform: auBooleanAttribute}) visible: boolean | undefined;

/**
* Callback called when the collapse visibility changed.
*
* @defaultValue
* ```ts
* () => {}
* ```
*/
@Output('auVisibleChange') visibleChange = new EventEmitter<boolean>();

/**
* Callback called when the collapse is hidden.
*
* @defaultValue
* ```ts
* () => {}
* ```
*/
@Output('auHidden') hidden = new EventEmitter<void>();

/**
* Callback called when the collapse is shown.
*
* @defaultValue
* ```ts
* () => {}
* ```
*/
@Output('auShown') shown = new EventEmitter<void>();

readonly _widget = callWidgetFactory({
factory: createCollapse,
widgetName: 'collapse',
defaultConfig: {},
events: {
onVisibleChange: (event) => this.visibleChange.emit(event),
onShown: () => this.shown.emit(),
onHidden: () => this.hidden.emit(),
},
afterInit: () => {
useDirectiveForHost(this._widget.directives.transitionDirective);
},
});
}
2 changes: 2 additions & 0 deletions angular/bootstrap/src/components/collapse/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './collapse.component';
export * from './collapse.gen';
2 changes: 2 additions & 0 deletions angular/bootstrap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export type {AlertContext, AlertProps, AlertState, AlertWidget, AlertApi, AlertD
export {createAlert, getAlertDefaultConfig} from './components/alert';
export * from './components/alert';

export * from './components/collapse';

export type {
ModalContext,
ModalProps,
Expand Down
14 changes: 14 additions & 0 deletions angular/demo/bootstrap/src/app/samples/collapse/default.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {AgnosUIAngularModule} from '@agnos-ui/angular-bootstrap';
import {Component} from '@angular/core';

@Component({
standalone: true,
imports: [AgnosUIAngularModule],
template: `
<button class="btn btn-primary m-2" type="button" (click)="collapse.api.open()">Open collapse</button>
<button class="btn btn-primary m-2" type="button" (click)="collapse.api.close()">Close collapse</button>
<button class="btn btn-primary m-2" type="button" (click)="collapse.api.toggle()">Toggle collapse</button>
<div auCollapse #collapse="auCollapse">Visible content</div>
`,
})
export default class DefaultCollapseComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type {CollapseDirective} from '@agnos-ui/angular-bootstrap';
import {AgnosUIAngularModule, getCollapseDefaultConfig} from '@agnos-ui/angular-bootstrap';
import {Component, ViewChild} from '@angular/core';
import {getUndefinedValues, hashChangeHook, provideHashConfig} from '../../utils';

const undefinedConfig = getUndefinedValues(getCollapseDefaultConfig());

@Component({
standalone: true,
imports: [AgnosUIAngularModule],
providers: provideHashConfig('collapse'),
template: `<div auCollapse #widget="auCollapse">Visible content</div>`,
})
export default class PlaygroundComponent {
@ViewChild('widget') widget!: CollapseDirective;

constructor() {
hashChangeHook((props) => {
this.widget._widget.patch({...undefinedConfig, ...props});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {createSimpleClassTransition, createTransition, UseDirective} from '@agnos-ui/angular-headless';
import {ChangeDetectionStrategy, Component, input, output} from '@angular/core';
/**
* You can create easily your own collapse component with the help of the `createTransition` function
* you will be able to plug the transition event of DaisyUI to your component.
* The `createSimpleClassTransition` is a helper to create a transition that will add a class to the element but you don't have to add classes as this
* DaisyUI CSS is not using this feature.
*/
@Component({
selector: 'app-collapse',
imports: [UseDirective],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div
tabindex="0"
class="collapse bg-base-200"
[auUse]="transition.directives.directive"
(blur)="transition.api.hide()"
(focus)="transition.api.show()"
>
<div class="collapse-title font-medium text-xl">{{ title() }}</div>
<div class="collapse-content"><ng-content /></div>
</div>
`,
})
export class CollapseDaisyComponent {
readonly title = input('Focus me to see content');

// The advantage of AgnosUI here is that it plug the transition state to some possible callbacks
onShown = output();
onHidden = output();

transition = createTransition({
props: {
visible: false, // could be something in an input that also add the collapse-open class
transition: createSimpleClassTransition({}),
onShown: () => this.onShown.emit(),
onHidden: () => this.onHidden.emit(),
},
});
}
13 changes: 13 additions & 0 deletions angular/demo/daisyui/src/app/samples/collapse/default.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Component} from '@angular/core';
import {CollapseDaisyComponent} from './collapse.component';

@Component({
standalone: true,
imports: [CollapseDaisyComponent],
template: ` <app-collapse (onHidden)="onHidden()"><p>tabindex necessary is already put</p></app-collapse> `,
})
export default class DefaultAlertComponent {
onHidden() {
console.log('Hidden');
}
}
3 changes: 3 additions & 0 deletions core-bootstrap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,8 @@
"dependencies": {
"@agnos-ui/core": "0.0.0"
},
"peerDependencies": {
"@amadeus-it-group/tansu": "^1.0.0"
},
"sideEffects": false
}
169 changes: 169 additions & 0 deletions core-bootstrap/src/components/collapse/collapse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {createTransition} from '@agnos-ui/core/services/transitions/baseTransitions';
import type {ConfigValidator, Directive, PropsConfig, Widget} from '@agnos-ui/core/types';
import {stateStores, writablesForProps} from '@agnos-ui/core/utils/stores';
import {bindDirectiveNoArg} from '@agnos-ui/core/utils/directive';
import {typeBoolean} from '@agnos-ui/core/utils/writables';
import {collapseHorizontalTransition, collapseVerticalTransition} from '../../services/transitions/collapse';
import {asWritable, computed} from '@amadeus-it-group/tansu';

export interface CollapseCommonPropsAndState {
/**
* CSS classes to be applied on the widget main container
*
* @defaultValue `''`
*/
className: string;
/**
* If `true`, collapse will be done horizontally.
*
* @defaultValue `false`
*/
horizontal: boolean;
/**
* If `true` the collapse is visible to the user
*
* @defaultValue `true`
*/
visible: boolean;
}

export interface CollapseState extends CollapseCommonPropsAndState {
/**
* Is `true` when the collapse is hidden. Compared to `visible`, this is updated after the transition is executed.
*/
hidden: boolean;
}

export interface CollapseProps extends CollapseCommonPropsAndState {
/**
* Callback called when the collapse visibility changed.
*
* @defaultValue
* ```ts
* () => {}
* ```
*/
onVisibleChange: (visible: boolean) => void;

/**
* Callback called when the collapse is hidden.
*
* @defaultValue
* ```ts
* () => {}
* ```
*/
onHidden: () => void;

/**
* Callback called when the collapse is shown.
*
* @defaultValue
* ```ts
* () => {}
* ```
*/
onShown: () => void;

/**
* If `true`, collapse opening will be animated at init time.
*
* @defaultValue `false`
*/
animatedOnInit: boolean;
/**
* If `true`, collapse closing and opening will be animated.
*
* @defaultValue `true`
*/
animated: boolean;
}

export interface CollapseApi {
/**
* Triggers collapse closing programmatically.
*/
close(): void;

/**
* Triggers the collapse content to be displayed for the user.
*/
open(): void;

/**
* Toggles the collapse content visibility.
*/
toggle(): void;
}

export interface CollapseDirectives {
/**
* the transition directive, piloting what is the visual effect of going from hidden to visible
*/
transitionDirective: Directive;
}

export type CollapseWidget = Widget<CollapseProps, CollapseState, CollapseApi, object, CollapseDirectives>;

const defaultCollapseConfig: CollapseProps = {
visible: true,
horizontal: false,
onVisibleChange: () => {},
onShown: () => {},
onHidden: () => {},
animated: true,
animatedOnInit: false,
className: '',
};

/**
* Retrieve a shallow copy of the default collapse config
* @returns the default collapse config
*/
export function getCollapseDefaultConfig(): CollapseProps {
return {...defaultCollapseConfig};
}

const commonCollapseConfigValidator: ConfigValidator<CollapseProps> = {
horizontal: typeBoolean,
};

/**
* Create an CollapseWidget with given config props
* @param config - an optional collapse config
* @returns an CollapseWidget
*/
export function createCollapse(config?: PropsConfig<CollapseProps>): CollapseWidget {
const [{animatedOnInit$, animated$, visible$: requestedVisible$, onVisibleChange$, onHidden$, onShown$, horizontal$, ...stateProps}, patch] =
writablesForProps(defaultCollapseConfig, config, commonCollapseConfigValidator);

const currentTransitionFn$ = asWritable(computed(() => (horizontal$() ? collapseHorizontalTransition : collapseVerticalTransition)));

const transition = createTransition({
props: {
transition: currentTransitionFn$,
visible: requestedVisible$,
animated: animated$,
animatedOnInit: animatedOnInit$,
onVisibleChange: onVisibleChange$,
onHidden: onHidden$,
onShown: onShown$,
},
});

const visible$ = transition.stores.visible$;
const hidden$ = transition.stores.hidden$;
return {
...stateStores({...stateProps, visible$, hidden$, horizontal$}),
patch,
api: {
open: transition.api.show,
close: transition.api.hide,
toggle: transition.api.toggle,
},
directives: {
transitionDirective: bindDirectiveNoArg(transition.directives.directive),
},
actions: {},
};
}
1 change: 1 addition & 0 deletions core-bootstrap/src/components/collapse/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './collapse';
Loading

0 comments on commit 39cfe8b

Please sign in to comment.