From bb26b09f7199cfd34ddaa9c9a8a71229af326f45 Mon Sep 17 00:00:00 2001 From: Valerio Date: Thu, 31 Oct 2024 09:42:37 +0100 Subject: [PATCH] feat: component transfer --- .devcontainer/devcontainer.json | 70 +++-- .devcontainer/scripts/postCreateCommand.sh | 11 +- .devcontainer/scripts/postStartCommand.sh | 9 + angular.json | 3 +- .../src/lib/components/form/form.module.ts | 2 + .../form/transfer/store/transfer.reducers.ts | 242 ++++++++++++++++++ .../form/transfer/store/transfer.state.ts | 20 ++ .../form/transfer/store/transfer.store.ts | 88 +++++++ .../transfer-list.component.html | 42 +++ .../transfer-list.component.spec.ts | 31 +++ .../transfer-list/transfer-list.component.ts | 93 +++++++ .../form/transfer/transfer.component.html | 52 ++++ .../form/transfer/transfer.component.spec.ts | 34 +++ .../form/transfer/transfer.component.ts | 157 ++++++++++++ .../form/transfer/transfer.model.ts | 13 + projects/design-angular-kit/src/public_api.ts | 42 +-- src/app/app-routing.module.ts | 1 + .../transfer-default-example.component.html | 9 + .../transfer-default-example.component.ts | 30 +++ .../transfer-examples.component.tpl | 37 +++ .../transfer-examples.component.ts | 7 + .../transfer-index.component.html | 13 + .../transfer-index.component.ts | 14 + ...nsfer-reactive-form-example.component.html | 11 + ...ransfer-reactive-form-example.component.ts | 30 +++ src/app/transfer/transfer-routing.module.ts | 16 ++ ...nsfer-template-form-example.component.html | 9 + ...ransfer-template-form-example.component.ts | 27 ++ src/app/transfer/transfer.module.ts | 22 ++ src/assets/table-of-content.json | 4 + 30 files changed, 1078 insertions(+), 61 deletions(-) create mode 100755 .devcontainer/scripts/postStartCommand.sh create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.reducers.ts create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.state.ts create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.store.ts create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.html create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.spec.ts create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.ts create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.html create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.spec.ts create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.ts create mode 100644 projects/design-angular-kit/src/lib/components/form/transfer/transfer.model.ts create mode 100644 src/app/transfer/transfer-default-example/transfer-default-example.component.html create mode 100644 src/app/transfer/transfer-default-example/transfer-default-example.component.ts create mode 100644 src/app/transfer/transfer-examples/transfer-examples.component.tpl create mode 100644 src/app/transfer/transfer-examples/transfer-examples.component.ts create mode 100644 src/app/transfer/transfer-index/transfer-index.component.html create mode 100644 src/app/transfer/transfer-index/transfer-index.component.ts create mode 100644 src/app/transfer/transfer-reactive-form-example/transfer-reactive-form-example.component.html create mode 100644 src/app/transfer/transfer-reactive-form-example/transfer-reactive-form-example.component.ts create mode 100644 src/app/transfer/transfer-routing.module.ts create mode 100644 src/app/transfer/transfer-template-form-example/transfer-template-form-example.component.html create mode 100644 src/app/transfer/transfer-template-form-example/transfer-template-form-example.component.ts create mode 100644 src/app/transfer/transfer.module.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index edddb14b..bcfe1fb4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,38 +1,36 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node { - "name": "design-angular-kit", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - // "image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm", - "build": { - "dockerfile": "Dockerfile" - }, - // Features to add to the dev container. More info: https://containers.dev/features. - "features": { - "ghcr.io/devcontainers-contrib/features/angular-cli:2": { - "version": "18.0.7" - }, - "ghcr.io/devcontainers-contrib/features/cz-cli:1": {} - }, - "mounts": [ - "source=design-angular-kit-bundle-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume" // deps volume - ], - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "sh .devcontainer/scripts/postCreateCommand.sh", - // Configure tool-specific properties. - "customizations": { - "vscode": { - "extensions": [ - "angular.ng-template", - "cyrilletuzi.angular-schematics", - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint", - "vivaxy.vscode-conventional-commits" - ] - } - } - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + "name": "design-angular-kit", + "build": { + "dockerfile": "Dockerfile" + }, + "mounts": [ + "source=design-angular-kit-bundle-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume" // deps volume + ], + "remoteUser": "root", + "postCreateCommand": "sh .devcontainer/scripts/postCreateCommand.sh", + // "postStartCommand": "/bin/bash -c '.devcontainer/scripts/postStartCommand.sh \"${containerWorkspaceFolder}\"'", + "postStartCommand": "sh .devcontainer/scripts/postStartCommand.sh \"${containerWorkspaceFolder}\"", + "customizations": { + "vscode": { + "extensions": [ + "angular.ng-template", + "cyrilletuzi.angular-schematics", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "vivaxy.vscode-conventional-commits" + ], + "settings": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } + } + } + }, + "features": { + "ghcr.io/devcontainers-contrib/features/angular-cli:2": { + "version": "18.0.7" + }, + "ghcr.io/devcontainers-contrib/features/cz-cli:1": {} + } } diff --git a/.devcontainer/scripts/postCreateCommand.sh b/.devcontainer/scripts/postCreateCommand.sh index 8e3de4ba..3019de0a 100644 --- a/.devcontainer/scripts/postCreateCommand.sh +++ b/.devcontainer/scripts/postCreateCommand.sh @@ -1,9 +1,12 @@ #/bin/bash +echo "lifecycle hook: postCreateCommand => start" -echo "Set node_modules permission" -sudo chown -R node:node node_modules -echo "Set node_modules permission: done" +# echo "Set node_modules permission" +# sudo chown -R node:node node_modules +# echo "Set node_modules permission: done" echo "Installing deps" npm i -echo "Installing deps: done" \ No newline at end of file +echo "Installing deps: done" + +echo "lifecycle hook: postCreateCommand => done" diff --git a/.devcontainer/scripts/postStartCommand.sh b/.devcontainer/scripts/postStartCommand.sh new file mode 100755 index 00000000..83b73b96 --- /dev/null +++ b/.devcontainer/scripts/postStartCommand.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +WORKSPACE_FOLDER=$1 + +echo "lifecycle hook: postStartCommand => start" +echo "Set $WORKSPACE_FOLDER as git safe directory" +git config --global --add safe.directory $WORKSPACE_FOLDER +# git config --global --add safe.directory /workspaces/design-angular-kit +echo "lifecycle hook: postStartCommand => done" diff --git a/angular.json b/angular.json index a58113d2..f9e65bf3 100644 --- a/angular.json +++ b/angular.json @@ -106,7 +106,8 @@ "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "buildTarget": "design-angular-kit-bundle:build" + "buildTarget": "design-angular-kit-bundle:build", + "host": "127.0.0.1" }, "configurations": { "production": { diff --git a/projects/design-angular-kit/src/lib/components/form/form.module.ts b/projects/design-angular-kit/src/lib/components/form/form.module.ts index ba56fcf7..ff00adc9 100644 --- a/projects/design-angular-kit/src/lib/components/form/form.module.ts +++ b/projects/design-angular-kit/src/lib/components/form/form.module.ts @@ -10,6 +10,7 @@ import { ItTextareaComponent } from './textarea/textarea.component'; import { ItUploadDragDropComponent } from './upload-drag-drop/upload-drag-drop.component'; import { ItUploadFileListComponent } from './upload-file-list/upload-file-list.component'; import { ItAutocompleteComponent } from './autocomplete/autocomplete.component'; +import { ItTransferComponent } from './transfer/transfer.component'; const formComponents = [ ItAutocompleteComponent, @@ -21,6 +22,7 @@ const formComponents = [ ItRatingComponent, ItSelectComponent, ItTextareaComponent, + ItTransferComponent, ItUploadDragDropComponent, ItUploadFileListComponent, ]; diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.reducers.ts b/projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.reducers.ts new file mode 100644 index 00000000..644e47e0 --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.reducers.ts @@ -0,0 +1,242 @@ +import { TransferItem, TransferItemSelection } from '../transfer.model'; +import { SelectionState, State } from './transfer.state'; + +//#region private utility functions +const generateSelectAll = (checked: boolean, items: TransferItem[]) => { + const selected = new Set>(); + if (checked) { + items.forEach(item => selected.add(item)); + } + + return selected; +}; + +const updateSelected = (set: Set>, item: TransferItem) => { + if (set.has(item)) { + set.delete(item); + } else { + set.add(item); + } + + return set; +}; +//#endregion + +//#region reducers +const init = (state: State, { source, target }: SelectionState) => ({ + ...state, + initialItems: { + source: [...source], + target: [...target], + }, + current: { + source: [...source], + target: [...target], + }, +}); + +const transfer = (state: State) => { + return { + ...state, + current: { + ...state.current, + source: state.current.source.filter(i => !state.selections.source.has(i)), + target: Array.from(new Set([...state.current.target, ...Array.from(state.selections.source)] as TransferItemSelection)), + }, + selections: { + ...state.selections, + source: new Set>(), + }, + operationsEnabled: { + ...state.operationsEnabled, + transfer: false, + reset: true, + }, + } satisfies State; +}; + +const backtransfer = (state: State) => { + return { + ...state, + current: { + ...state.current, + target: state.current.target.filter(i => !state.selections.target.has(i)), + source: Array.from(new Set([...state.current.source, ...Array.from(state.selections.target)] as TransferItemSelection)), + }, + selections: { + ...state.selections, + target: new Set>(), + }, + operationsEnabled: { + ...state.operationsEnabled, + backtransfer: false, + reset: true, + }, + } satisfies State; +}; + +const reset = (state: State) => { + return { + ...state, + current: { + source: [...state.initialItems.source], + target: [...state.initialItems.target], + }, + operationsEnabled: { + ...state.operationsEnabled, + reset: false, + }, + } satisfies State; +}; + +const selectAllSource = (state: State, { checked }: { checked: boolean }) => { + const items = state.current.source; + const selected = generateSelectAll(checked, items); + const transfer = Boolean(selected.size); + + return { + ...state, + selections: { + ...state.selections, + source: selected, + }, + operationsEnabled: { + ...state.operationsEnabled, + transfer, + }, + } satisfies State; +}; + +const selectAllTarget = (state: State, { checked }: { checked: boolean }) => { + const items = state.current.target; + const selected = generateSelectAll(checked, items); + const backtransfer = Boolean(selected.size); + + return { + ...state, + selections: { + ...state.selections, + target: selected, + }, + operationsEnabled: { + ...state.operationsEnabled, + backtransfer, + }, + } satisfies State; +}; + +const selectionItemSource = (previousState: State, { item }: { item: TransferItem }) => { + const selected = updateSelected(previousState.selections.source, item); + const selectedItems = Array.from(selected); + const transfer = Boolean(selectedItems.length); + const source = new Set([...selectedItems]); + + const state = { + ...previousState, + selections: { + ...previousState.selections, + source, + }, + operationsEnabled: { + ...previousState.operationsEnabled, + transfer, + }, + } satisfies State; + + return state; +}; + +const selectionItemTarget = (previousState: State, { item }: { item: TransferItem }) => { + const selected = updateSelected(previousState.selections.target, item); + const selectedItems = Array.from(selected); + const backtransfer = Boolean(selectedItems.length); + const target = new Set([...selectedItems]); + + const state = { + ...previousState, + selections: { + ...previousState.selections, + target, + }, + operationsEnabled: { + ...previousState.operationsEnabled, + backtransfer, + }, + } satisfies State; + + return state; +}; +//#endregion reducers + +//#region public reducers +const initialStateFn = () => ({ + initialItems: { + source: [], + target: [], + }, + current: { + source: [], + target: [], + }, + selections: { + source: new Set>(), + target: new Set>(), + }, + operationsEnabled: { + transfer: false, + backtransfer: false, + reset: false, + }, +}); +const initFn = + (payload: SelectionState) => + (state: State) => + init(state, payload); + +const transferFn = + () => + (state: State) => + transfer(state); + +const backtransferFn = + () => + (state: State) => + backtransfer(state); + +const resetFn = + () => + (state: State) => + reset(state); + +const selectAllSourceFn = + ({ checked }: { checked: boolean }) => + (state: State) => + selectAllSource(state, { checked }) as State; + +const selectAllTargetFn = + ({ checked }: { checked: boolean }) => + (state: State) => + selectAllTarget(state, { checked }) as State; + +const selectionItemSourceFn = + ({ item }: { item: TransferItem }) => + (state: State) => + selectionItemSource(state, { item }) as State; + +const selectionItemTargetFn = + ({ item }: { item: TransferItem }) => + (state: State) => + selectionItemTarget(state, { item }) as State; +//#endregion + +export default { + initialStateFn, + initFn, + transferFn, + backtransferFn, + resetFn, + selectAllSourceFn, + selectAllTargetFn, + selectionItemSourceFn, + selectionItemTargetFn, +}; diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.state.ts b/projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.state.ts new file mode 100644 index 00000000..5c6962ec --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.state.ts @@ -0,0 +1,20 @@ +import { TransferItem } from '../transfer.model'; + +export interface SelectionState { + source: Array>; + target: Array>; +} + +export interface State { + initialItems: SelectionState; + current: SelectionState; + selections: { + source: Set>; + target: Set>; + }; + operationsEnabled: { + transfer: boolean; + backtransfer: boolean; + reset: boolean; + }; +} diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.store.ts b/projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.store.ts new file mode 100644 index 00000000..81e3cca4 --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/store/transfer.store.ts @@ -0,0 +1,88 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, distinctUntilChanged, map, of } from 'rxjs'; +import { SourceType, TransferItem } from '../transfer.model'; +import reducers from './transfer.reducers'; +import { SelectionState, State } from './transfer.state'; + +@Injectable() +export class TransferStore { + private readonly _state = new BehaviorSubject>(reducers.initialStateFn()); + + private readonly sourceItems = this._state.pipe(map(state => state.current.source)); + + private readonly targetItems = this._state.pipe(map(state => state.current.target)); + + readonly valueChanged = this._state.pipe( + map(state => state.current.target), + distinctUntilChanged() + ); + + readonly selectItems = (sourceType: SourceType) => { + if (sourceType === 'source') { + return this.sourceItems; + } + + if (sourceType === 'target') { + return this.targetItems; + } + + return of[]>([]); + }; + + readonly selectSelectedItems = (sourceType: SourceType) => { + if (sourceType === 'source') { + return this._state.pipe(map(state => state.selections.source)); + } + + if (sourceType === 'target') { + return this._state.pipe(map(state => state.selections.target)); + } + + return of(new Set>()); + }; + + readonly transferEnabled = this._state.pipe(map(state => state.operationsEnabled.transfer)); + + readonly backtransferEnabled = this._state.pipe(map(state => state.operationsEnabled.backtransfer)); + + readonly resetEnabled = this._state.pipe(map(state => state.operationsEnabled.reset)); + + init({ source, target }: SelectionState) { + this.updateState(reducers.initFn({ source, target })); + } + + transfer() { + this.updateState(reducers.transferFn()); + } + backtransfer() { + this.updateState(reducers.backtransferFn()); + } + + reset() { + this.updateState(reducers.resetFn()); + } + + checkboxSelection(item: TransferItem, sourceType: SourceType) { + if (sourceType === 'source') { + this.updateState(reducers.selectionItemSourceFn({ item })); + } + + if (sourceType === 'target') { + this.updateState(reducers.selectionItemTargetFn({ item })); + } + } + + selectAllSelection(checked: boolean, sourceType: SourceType) { + if (sourceType === 'source') { + this.updateState(reducers.selectAllSourceFn({ checked })); + } + + if (sourceType === 'target') { + this.updateState(reducers.selectAllTargetFn({ checked })); + } + } + + private updateState(reducerFn: (state: State) => State) { + this._state.next(reducerFn(this._state.value)); + } +} diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.html b/projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.html new file mode 100644 index 00000000..fdbf34b7 --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.html @@ -0,0 +1,42 @@ +
+
+
+ + +
+ +
+ +
+
+ @for (item of items$ | async; track item.value) { +
+ + +
+ } +
+
+
+ diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.spec.ts b/projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.spec.ts new file mode 100644 index 00000000..a8953f07 --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HostAttributeToken } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { TransferStore } from '../store/transfer.store'; +import { ItTransferListComponent } from './transfer-list.component'; + +describe('ItTransferListComponent', () => { + let component: ItTransferListComponent; + let fixture: ComponentFixture>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ItTransferListComponent], + providers: [ + TransferStore, + TranslateService, + { provide: new HostAttributeToken('title'), useValue: 'title' }, + { provide: new HostAttributeToken('sourceType'), useValue: 'source' }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ItTransferListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.ts b/projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.ts new file mode 100644 index 00000000..3859a446 --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/transfer-list/transfer-list.component.ts @@ -0,0 +1,93 @@ +import { AsyncPipe, TitleCasePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, ElementRef, HostAttributeToken, inject, ViewChild } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { combineLatest, distinctUntilChanged, map, shareReplay, skip, startWith, tap } from 'rxjs'; +import { ItAbstractComponent } from '../../../../abstracts/abstract.component'; +import { TransferStore } from '../store/transfer.store'; + +import { SourceType, TransferItem } from '../transfer.model'; + +interface SelectableTransferItem extends TransferItem { + selected: boolean; +} + +@Component({ + selector: 'it-transfer-list', + standalone: true, + imports: [AsyncPipe, TitleCasePipe], + templateUrl: './transfer-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ItTransferListComponent extends ItAbstractComponent { + /** + * Widget title + */ + readonly title = inject(new HostAttributeToken('title'), { optional: true }); + + readonly sourceType = inject(new HostAttributeToken('sourceType'), { optional: true }) as SourceType; + + private readonly items = this.store.selectItems(this.sourceType).pipe(distinctUntilChanged(), shareReplay()); + private readonly selected = this.store.selectSelectedItems(this.sourceType).pipe(distinctUntilChanged(), shareReplay()); + + readonly numberOfItems$ = this.items.pipe( + map(items => ({ length: items.length })), + startWith({ length: 0 }) + ); + readonly selectAllDisabled = this.items.pipe(map(items => items.length === 0)); + /** + * Items of the list + * @default [] + */ + readonly items$ = combineLatest([this.items, this.selected]).pipe( + map(([items, selected]) => + items.map(item => { + (item as SelectableTransferItem).selected = selected.has(item); + return item as SelectableTransferItem; + }) + ) + ); + + @ViewChild('selectAllCheckbox') + selectAllCheckboxRef!: ElementRef; + + readonly instanceId = this.getInstanceId(); + + constructor(private readonly store: TransferStore) { + super(); + this.onItemsUpdate(); + } + /** + * Checkbox selection click handler + */ + checkboxSelectionHandler(item: TransferItem) { + this.store.checkboxSelection(item, this.sourceType); + } + /** + * Checkbox select all selection handler + */ + checkboxSelectAllHandler(event: Event) { + const checked = ((event as PointerEvent).target as HTMLInputElement).checked; + this.store.selectAllSelection(checked, this.sourceType); + } + + /** + * Items update subscription + */ + private onItemsUpdate() { + this.items + .pipe( + takeUntilDestroyed(), + skip(1), + tap(() => { + if (this.selectAllCheckboxRef) { + this.selectAllCheckboxRef.nativeElement.checked = false; + } + }) + ) + .subscribe(); + } + + private getInstanceId() { + return Math.floor(Math.random() * 100000000).toString(); + } +} diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.html b/projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.html new file mode 100644 index 00000000..61f30baf --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.html @@ -0,0 +1,52 @@ +
+ @if (label) { + + } +
+
+ +
+ +
+ +
+ + + + Etichetta per freccia destra + + + + Etichetta for freccia sinistra + + + + Etichetta per icona di reset +
+
+
+ +
+
+
diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.spec.ts b/projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.spec.ts new file mode 100644 index 00000000..9e7120d1 --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.spec.ts @@ -0,0 +1,34 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TransferStore } from './store/transfer.store'; +import { ItTransferComponent } from './transfer.component'; + +describe('ItTransferComponent', () => { + let component: ItTransferComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ItTransferComponent, + //https://stackoverflow.com/a/52461467/2642723 + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader, + }, + }), + ], + providers: [TransferStore, TranslateService], + }).compileComponents(); + + fixture = TestBed.createComponent(ItTransferComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.ts b/projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.ts new file mode 100644 index 00000000..08c9801b --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/transfer.component.ts @@ -0,0 +1,157 @@ +import { AsyncPipe, NgClass } from '@angular/common'; +import { ChangeDetectionStrategy, Component, DestroyRef, EventEmitter, inject, Input, OnInit, Optional, Output, Self } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControlName, NgControl, NgModel, ReactiveFormsModule } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { tap } from 'rxjs'; +import { ItAbstractFormComponent } from '../../../abstracts/abstract-form.component'; +import { TransferStore } from './store/transfer.store'; +import { ItTransferListComponent } from './transfer-list/transfer-list.component'; +import { TransferItem } from './transfer.model'; + +/** + * Transfer + * @description Component that allows the creation of checkbox lists. + */ +@Component({ + selector: 'it-transfer', + standalone: true, + templateUrl: './transfer.component.html', + imports: [ItTransferListComponent, NgClass, AsyncPipe, ReactiveFormsModule], + providers: [TransferStore], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ItTransferComponent extends ItAbstractFormComponent implements OnInit { + /** + * The select options (left side) + */ + @Input() options = []; + /** + * The selected options (right side) + */ + @Input() selected = []; + /** + * Fired when there is a transfer, a backtransfer or a reset event + */ + @Output() readonly transferChanges = new EventEmitter[]>(); + + /** + * Enable transfer button + * @default false + */ + readonly transferEnabled = this.store.transferEnabled; + /** + * Enable backtransfer button + * @default false + */ + readonly backtransferEnabled = this.store.backtransferEnabled; + /** + * Enable reset button + * @default false + */ + readonly resetEnabled = this.store.resetEnabled; + + private readonly destroyRef = inject(DestroyRef); + + constructor( + @Self() + @Optional() + override readonly _ngControl: NgControl, + override readonly _translateService: TranslateService, + private readonly store: TransferStore + ) { + super(_translateService, _ngControl); + } + + override ngOnInit() { + super.ngOnInit(); + this.storeInit(); + this.onStoreValueChanged(); + } + + /** + * Transfer button click handler + */ + transferClickHandler(event: MouseEvent) { + this.buttonEventHandler(event, () => this.store.transfer()); + } + /** + * Transfer button keypress handler + */ + transferKeyPressHandler(event: KeyboardEvent) { + this.buttonEventHandler(event, () => this.store.transfer()); + } + /** + * Backtransfer button click handler + */ + backtransferClickHandler(event: MouseEvent) { + this.buttonEventHandler(event, () => this.store.backtransfer()); + } + /** + * Backtransfer button keypress handler + */ + backtransferKeyPressHandler(event: KeyboardEvent) { + this.buttonEventHandler(event, () => this.store.backtransfer()); + } + /** + * Reset button click handler + */ + resetClickHandler(event: MouseEvent) { + this.buttonEventHandler(event, () => this.store.reset()); + } + /** + * Reset button keypress handler + */ + resetKeyPressHandler(event: KeyboardEvent) { + this.buttonEventHandler(event, () => this.store.reset()); + } + + private buttonEventHandler(event: Event, updateStoreCb: () => void) { + event.preventDefault(); + updateStoreCb(); + } + + private storeInit() { + let target = []; + const ngControl = this._ngControl; + const isNgControlDefined = Boolean(this._ngControl); + + // if ngControl is defined, take values from it. Input() target will be ignored + if (isNgControlDefined) { + console.debug('ngControl instanceof NgModel:', ngControl instanceof NgModel); + console.debug('ngControl instanceof FormControlName:', ngControl instanceof FormControlName); + + // if ngControl is an ngModel (template-driven form use case), take values from it + if (ngControl instanceof NgModel) { + console.debug('ngControl instanceof NgModel'); + const model = (ngControl as NgModel).model; + target = Array.isArray(model) ? model : []; + } + + // if ngControl is an FormControlName (reactive form use case), take values from it + if (ngControl instanceof FormControlName) { + console.debug('ngControl instanceof FormControlName'); + const model = (ngControl as FormControlName).control.value; + target = Array.isArray(model) ? model : []; + } + + console.debug('ngControl is defined. Input() target will be ignored'); + } else if (this.selected && Array.isArray(this.selected)) { + target = [...this.selected]; + } + + console.debug('target:', this.selected, 'formControl:', this.control.value, 'ngModel:', this._ngControl); + this.store.init({ source: [...this.options], target }); + } + + private onStoreValueChanged() { + this.store.valueChanged + .pipe( + takeUntilDestroyed(this.destroyRef), + tap(value => this.writeValue(value as T)), + tap(value => this.onChange(value as T)), + tap(value => this.transferChanges.emit(value)) + ) + .subscribe(); + } +} diff --git a/projects/design-angular-kit/src/lib/components/form/transfer/transfer.model.ts b/projects/design-angular-kit/src/lib/components/form/transfer/transfer.model.ts new file mode 100644 index 00000000..8d77bc3a --- /dev/null +++ b/projects/design-angular-kit/src/lib/components/form/transfer/transfer.model.ts @@ -0,0 +1,13 @@ +//Qs +//Aria hidden? +//state management with service? +//interface? + +export interface TransferItem { + text: string; + value: ValueType; +} + +export type TransferItemSelection = Array>; + +export type SourceType = 'source' | 'target'; diff --git a/projects/design-angular-kit/src/public_api.ts b/projects/design-angular-kit/src/public_api.ts index b484a609..63fbfc95 100644 --- a/projects/design-angular-kit/src/public_api.ts +++ b/projects/design-angular-kit/src/public_api.ts @@ -2,45 +2,45 @@ * Public API Surface of design-angular-kit */ -export * from './lib/provide-design-angular-kit'; export * from './lib/design-angular-kit.module'; +export * from './lib/provide-design-angular-kit'; // Core components export * from './lib/components/core/accordion/accordion.component'; export * from './lib/components/core/alert/alert.component'; -export * from './lib/components/core/avatar/avatar.module'; -export * from './lib/components/core/avatar/avatar.directive'; export * from './lib/components/core/avatar/avatar-dropdown/avatar-dropdown.component'; export * from './lib/components/core/avatar/avatar-group/avatar-group.component'; +export * from './lib/components/core/avatar/avatar.directive'; +export * from './lib/components/core/avatar/avatar.module'; export * from './lib/components/core/badge/badge.directive'; export * from './lib/components/core/button/button.directive'; export * from './lib/components/core/callout/callout.component'; export * from './lib/components/core/card/card.component'; +export * from './lib/components/core/carousel/carousel-item/carousel-item.component'; export * from './lib/components/core/carousel/carousel.module'; export * from './lib/components/core/carousel/carousel/carousel.component'; -export * from './lib/components/core/carousel/carousel-item/carousel-item.component'; export * from './lib/components/core/chip/chip.component'; export * from './lib/components/core/collapse/collapse.component'; -export * from './lib/components/core/dimmer/dimmer.module'; -export * from './lib/components/core/dimmer/dimmer.component'; export * from './lib/components/core/dimmer/dimmer-buttons/dimmer-buttons.component'; export * from './lib/components/core/dimmer/dimmer-icon/dimmer-icon.component'; +export * from './lib/components/core/dimmer/dimmer.component'; +export * from './lib/components/core/dimmer/dimmer.module'; +export * from './lib/components/core/dropdown/dropdown-item/dropdown-item.component'; export * from './lib/components/core/dropdown/dropdown.module'; export * from './lib/components/core/dropdown/dropdown/dropdown.component'; -export * from './lib/components/core/dropdown/dropdown-item/dropdown-item.component'; export * from './lib/components/core/forward/forward.directive'; export * from './lib/components/core/link/link.component'; +export * from './lib/components/core/list/list-item/list-item.component'; export * from './lib/components/core/list/list.module'; export * from './lib/components/core/list/list/list.component'; -export * from './lib/components/core/list/list-item/list-item.component'; export * from './lib/components/core/modal/modal.component'; export * from './lib/components/core/notifications/notifications.component'; @@ -50,29 +50,29 @@ export * from './lib/components/core/progress-bar/progress-bar.component'; export * from './lib/components/core/progress-button/progress-button.component'; export * from './lib/components/core/spinner/spinner.component'; -export * from './lib/components/core/steppers/steppers.module'; export * from './lib/components/core/steppers/steppers-container/steppers-container.component'; export * from './lib/components/core/steppers/steppers-item/steppers-item.component'; +export * from './lib/components/core/steppers/steppers.module'; -export * from './lib/components/core/tab/tab.module'; export * from './lib/components/core/tab/tab-container/tab-container.component'; export * from './lib/components/core/tab/tab-item/tab-item.component'; +export * from './lib/components/core/tab/tab.module'; -export * from './lib/components/core/table/table.module'; -export * from './lib/components/core/table/table.component'; -export * from './lib/components/core/table/sort/sort.directive'; export * from './lib/components/core/table/sort/sort-header/sort-header.component'; +export * from './lib/components/core/table/sort/sort.directive'; +export * from './lib/components/core/table/table.component'; +export * from './lib/components/core/table/table.module'; -export * from './lib/components/core/timeline/timeline.module'; -export * from './lib/components/core/timeline/timeline.component'; export * from './lib/components/core/timeline/timeline-item/timeline-item.component'; +export * from './lib/components/core/timeline/timeline.component'; +export * from './lib/components/core/timeline/timeline.module'; export * from './lib/components/core/tooltip/tooltip.directive'; // Forms components -export * from './lib/components/form/form.module'; export * from './lib/components/form/autocomplete/autocomplete.component'; export * from './lib/components/form/checkbox/checkbox.component'; +export * from './lib/components/form/form.module'; export * from './lib/components/form/input/input.component'; export * from './lib/components/form/password-input/password-input.component'; export * from './lib/components/form/radio-button/radio-button.component'; @@ -80,6 +80,8 @@ export * from './lib/components/form/range/range.component'; export * from './lib/components/form/rating/rating.component'; export * from './lib/components/form/select/select.component'; export * from './lib/components/form/textarea/textarea.component'; +export * from './lib/components/form/transfer/transfer.component'; +export * from './lib/components/form/transfer/transfer.model'; export * from './lib/components/form/upload-drag-drop/upload-drag-drop.component'; export * from './lib/components/form/upload-file-list/upload-file-list.component'; @@ -87,16 +89,16 @@ export * from './lib/components/form/upload-file-list/upload-file-list.component export * from './lib/components/navigation/back-button/back-button.component'; export * from './lib/components/navigation/back-to-top/back-to-top.component'; -export * from './lib/components/navigation/breadcrumbs/breadcrumbs.module'; -export * from './lib/components/navigation/breadcrumbs/breadcrumb/breadcrumb.component'; export * from './lib/components/navigation/breadcrumbs/breadcrumb-item/breadcrumb-item.component'; +export * from './lib/components/navigation/breadcrumbs/breadcrumb/breadcrumb.component'; +export * from './lib/components/navigation/breadcrumbs/breadcrumbs.module'; export * from './lib/components/navigation/header/header.component'; export * from './lib/components/navigation/megamenu/megamenu.component'; +export * from './lib/components/navigation/navbar/navbar-item/navbar-item.component'; export * from './lib/components/navigation/navbar/navbar.module'; export * from './lib/components/navigation/navbar/navbar/navbar.component'; -export * from './lib/components/navigation/navbar/navbar-item/navbar-item.component'; export * from './lib/components/navigation/sidebar/sidebar.component'; @@ -123,9 +125,9 @@ export * from './lib/interfaces/icon'; export * from './lib/interfaces/utils'; // Utils -export * from './lib/utils/regex'; export * from './lib/utils/date-utils'; export * from './lib/utils/file-utils'; +export * from './lib/utils/regex'; // Validators export * from './lib/validators/it-validators'; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index c6ee44db..ec7d72cb 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -68,6 +68,7 @@ const routes: Routes = [ { path: 'autocomplete', loadChildren: () => import('src/app/autocomplete/autocomplete.module').then(m => m.AutocompleteModule) }, { path: 'sidebar', loadChildren: () => import('src/app/sidebar/sidebar.module').then(m => m.SidebarModule) }, { path: 'timeline', loadChildren: () => import('src/app/timeline/timeline.module').then(m => m.TimelineModule) }, + { path: 'transfer', loadChildren: () => import('src/app/transfer/transfer.module').then(m => m.TransferModule) }, ], }, { path: 'error/not-found', component: ItErrorPageComponent, data: { errorCode: 404 } }, diff --git a/src/app/transfer/transfer-default-example/transfer-default-example.component.html b/src/app/transfer/transfer-default-example/transfer-default-example.component.html new file mode 100644 index 00000000..2fa06b68 --- /dev/null +++ b/src/app/transfer/transfer-default-example/transfer-default-example.component.html @@ -0,0 +1,9 @@ +

Esempio senza form

+
+
+
+
Esempio di default
+ +
+
+
diff --git a/src/app/transfer/transfer-default-example/transfer-default-example.component.ts b/src/app/transfer/transfer-default-example/transfer-default-example.component.ts new file mode 100644 index 00000000..33c9cd35 --- /dev/null +++ b/src/app/transfer/transfer-default-example/transfer-default-example.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { TransferItem } from 'projects/design-angular-kit/src/public_api'; + +@Component({ + selector: 'it-transfer-default-example', + templateUrl: './transfer-default-example.component.html', +}) +export class TransferDefaultExampleComponent { + readonly options: TransferItem[] = [ + { + text: 'Item 1', + value: 1, + }, + { + text: 'Item 2', + value: 2, + }, + ]; + readonly selected: TransferItem[] = [ + { + text: 'Item 3', + value: 3, + }, + ]; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + transferChangesHandler(_: TransferItem[]) { + // handle changing data + } +} diff --git a/src/app/transfer/transfer-examples/transfer-examples.component.tpl b/src/app/transfer/transfer-examples/transfer-examples.component.tpl new file mode 100644 index 00000000..2b02fc50 --- /dev/null +++ b/src/app/transfer/transfer-examples/transfer-examples.component.tpl @@ -0,0 +1,37 @@ +{% from "../../macro.template.njk" import sanitize as sanitize %} + +{% set html %} + {% include "../transfer-default-example/transfer-default-example.component.html" %} +{% endset %} + +{% set typescript %} + {% include "../transfer-default-example/transfer-default-example.component.ts" %} +{% endset %} + + + + + +{% set html %} + {% include "../transfer-template-form-example/transfer-template-form-example.component.html" %} +{% endset %} + +{% set typescript %} + {% include "../transfer-template-form-example/transfer-template-form-example.component.ts" %} +{% endset %} + + + + + +{% set html %} + {% include "../transfer-reactive-form-example/transfer-reactive-form-example.component.html" %} +{% endset %} + +{% set typescript %} + {% include "../transfer-reactive-form-example/transfer-reactive-form-example.component.ts" %} +{% endset %} + + + + \ No newline at end of file diff --git a/src/app/transfer/transfer-examples/transfer-examples.component.ts b/src/app/transfer/transfer-examples/transfer-examples.component.ts new file mode 100644 index 00000000..1658cc03 --- /dev/null +++ b/src/app/transfer/transfer-examples/transfer-examples.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'it-transfer-examples', + templateUrl: './transfer-examples.component.html', +}) +export class TransferExamplesComponent {} diff --git a/src/app/transfer/transfer-index/transfer-index.component.html b/src/app/transfer/transfer-index/transfer-index.component.html new file mode 100644 index 00000000..acf170b1 --- /dev/null +++ b/src/app/transfer/transfer-index/transfer-index.component.html @@ -0,0 +1,13 @@ +

Transfer

+

Componente che consente la creazione di liste di checkbox

+
+ + + + + + +

ItTransferComponent

+ +
+
diff --git a/src/app/transfer/transfer-index/transfer-index.component.ts b/src/app/transfer/transfer-index/transfer-index.component.ts new file mode 100644 index 00000000..b837f641 --- /dev/null +++ b/src/app/transfer/transfer-index/transfer-index.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import Documentation from '../../../assets/documentation.json'; + +@Component({ + selector: 'it-transfer-index', + templateUrl: './transfer-index.component.html', +}) +export class TransferIndexComponent { + component: any; + + constructor() { + this.component = (Documentation).components.find(component => component.name === 'ItTransferComponent'); + } +} diff --git a/src/app/transfer/transfer-reactive-form-example/transfer-reactive-form-example.component.html b/src/app/transfer/transfer-reactive-form-example/transfer-reactive-form-example.component.html new file mode 100644 index 00000000..68e80434 --- /dev/null +++ b/src/app/transfer/transfer-reactive-form-example/transfer-reactive-form-example.component.html @@ -0,0 +1,11 @@ +

Esempio con Reactive Form

+
+
+
+
FormGroup
+
+ +
+
+
+
diff --git a/src/app/transfer/transfer-reactive-form-example/transfer-reactive-form-example.component.ts b/src/app/transfer/transfer-reactive-form-example/transfer-reactive-form-example.component.ts new file mode 100644 index 00000000..c3c4b008 --- /dev/null +++ b/src/app/transfer/transfer-reactive-form-example/transfer-reactive-form-example.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { TransferItem } from 'projects/design-angular-kit/src/public_api'; + +@Component({ + selector: 'it-transfer-reactive-form-example', + templateUrl: './transfer-reactive-form-example.component.html', +}) +export class TransferReactiveFormExampleComponent { + readonly options: TransferItem[] = [ + { + text: 'Item 1', + value: 1, + }, + { + text: 'Item 2', + value: 2, + }, + ]; + readonly selected: TransferItem[] = [ + { + text: 'Item 3', + value: 3, + }, + ]; + + readonly formGroup = inject(FormBuilder).group({ + transfer: [this.selected], + }); +} diff --git a/src/app/transfer/transfer-routing.module.ts b/src/app/transfer/transfer-routing.module.ts new file mode 100644 index 00000000..cd058c97 --- /dev/null +++ b/src/app/transfer/transfer-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { TransferIndexComponent } from './transfer-index/transfer-index.component'; + +const routes = [ + { + path: '', + component: TransferIndexComponent, + }, +] satisfies Routes; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class TransferRoutingModule {} diff --git a/src/app/transfer/transfer-template-form-example/transfer-template-form-example.component.html b/src/app/transfer/transfer-template-form-example/transfer-template-form-example.component.html new file mode 100644 index 00000000..7311b2d2 --- /dev/null +++ b/src/app/transfer/transfer-template-form-example/transfer-template-form-example.component.html @@ -0,0 +1,9 @@ +

Esempio con Template Form

+
+
+
+
NgModel
+ +
+
+
diff --git a/src/app/transfer/transfer-template-form-example/transfer-template-form-example.component.ts b/src/app/transfer/transfer-template-form-example/transfer-template-form-example.component.ts new file mode 100644 index 00000000..b18b4e35 --- /dev/null +++ b/src/app/transfer/transfer-template-form-example/transfer-template-form-example.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { TransferItem } from 'projects/design-angular-kit/src/public_api'; + +@Component({ + selector: 'it-transfer-template-form-example', + templateUrl: './transfer-template-form-example.component.html', +}) +export class TransferTemplateFormExampleComponent { + readonly options: TransferItem[] = [ + { + text: 'Item 1', + value: 1, + }, + { + text: 'Item 2', + value: 2, + }, + ]; + readonly selected: TransferItem[] = [ + { + text: 'Item 3', + value: 3, + }, + ]; + + transferModel = this.selected; +} diff --git a/src/app/transfer/transfer.module.ts b/src/app/transfer/transfer.module.ts new file mode 100644 index 00000000..e54fca8c --- /dev/null +++ b/src/app/transfer/transfer.module.ts @@ -0,0 +1,22 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SharedModule } from '../shared/shared.module'; +import { TransferDefaultExampleComponent } from './transfer-default-example/transfer-default-example.component'; +import { TransferExamplesComponent } from './transfer-examples/transfer-examples.component'; +import { TransferIndexComponent } from './transfer-index/transfer-index.component'; +import { TransferReactiveFormExampleComponent } from './transfer-reactive-form-example/transfer-reactive-form-example.component'; +import { TransferRoutingModule } from './transfer-routing.module'; +import { TransferTemplateFormExampleComponent } from './transfer-template-form-example/transfer-template-form-example.component'; + +@NgModule({ + declarations: [ + TransferIndexComponent, + TransferDefaultExampleComponent, + TransferTemplateFormExampleComponent, + TransferReactiveFormExampleComponent, + TransferExamplesComponent, + ], + imports: [TransferRoutingModule, SharedModule, FormsModule, ReactiveFormsModule, CommonModule], +}) +export class TransferModule {} diff --git a/src/assets/table-of-content.json b/src/assets/table-of-content.json index 172e5c26..cb284d99 100644 --- a/src/assets/table-of-content.json +++ b/src/assets/table-of-content.json @@ -186,6 +186,10 @@ { "label": "Timeline", "link": "/componenti/timeline" + }, + { + "label": "Transfer", + "link": "/componenti/transfer" } ] }