Skip to content

Commit

Permalink
feat: split autocomple to another component
Browse files Browse the repository at this point in the history
Refs: #302
  • Loading branch information
AntoninoBonanno committed Jan 16, 2024
1 parent 475941e commit 082ee93
Show file tree
Hide file tree
Showing 15 changed files with 405 additions and 269 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<div class="form-group">
<label *ngIf="label" [for]="id" class="visually-hidden">{{ label }}</label>

<input
[id]="id"
type="search"
class="autocomplete"
[placeholder]="placeholder"
[formControl]="control"
[class.is-invalid]="isInvalid"
[class.is-valid]="isValid"
(blur)="markAsTouched()"
(keydown)="onKeyDown()" />

<span class="autocomplete-icon" aria-hidden="true">
<it-icon name="search" size="sm"></it-icon>
</span>

<ng-container *ngIf="autocompleteResults$ | async as autocomplete">
<ul class="autocomplete-list" [class.autocomplete-list-show]="autocomplete.relatedEntries?.length && showAutocompletion">
<li *ngFor="let entry of autocomplete.relatedEntries; trackBy: autocompleteItemTrackByValueFn">
<a [href]="entry.link" (click)="onEntryClick(entry, $event)">
<div class="avatar size-sm" *ngIf="entry.avatarSrcPath">
<img [src]="entry.avatarSrcPath" [alt]="entry.avatarAltText" />
</div>

<it-icon *ngIf="entry.icon" [name]="entry.icon" size="sm"></it-icon>

<span class="autocomplete-list-text">
<span [innerHTML]="entry.value | markMatchingText: autocomplete.searchedValue"></span>
<em *ngIf="entry.label">{{ entry.label }}</em>
</span>
</a>
</li>
</ul>
</ng-container>

<div *ngIf="isInvalid" class="form-feedback just-validate-error-label" [id]="id + '-error'">
<div #customError>
<ng-content select="[error]"></ng-content>
</div>
<ng-container *ngIf="!customError.hasChildNodes()">{{ invalidMessage | async }}</ng-container>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ItAutocompleteComponent } from './autocomplete.component';

describe('AutocompleteComponent', () => {
let component: ItAutocompleteComponent;
let fixture: ComponentFixture<ItAutocompleteComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ItAutocompleteComponent],
});
fixture = TestBed.createComponent(ItAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ItAbstractFormComponent } from 'design-angular-kit/abstracts/abstract-form.component';
import { AutocompleteItem } from 'design-angular-kit/interfaces/form';
import { debounceTime, distinctUntilChanged, map, Observable, of, switchMap } from 'rxjs';
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { ItIconComponent } from 'design-angular-kit/components/utils/icon/icon.component';
import { MarkMatchingTextPipe } from 'design-angular-kit/pipes/mark-matching-text.pipe';
import { ReactiveFormsModule } from '@angular/forms';

@Component({
standalone: true,
selector: 'it-autocomplete[autocompleteData]',
templateUrl: './autocomplete.component.html',
imports: [AsyncPipe, ItIconComponent, MarkMatchingTextPipe, NgForOf, NgIf, NgTemplateOutlet, ReactiveFormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ItAutocompleteComponent extends ItAbstractFormComponent<string | null | undefined> implements OnInit {
/**
* Indicates the list of searchable elements on which to base the input autocomplete system
* If you need to retrieve items via API, can pass a function of Observable
* @default undefined
*/
@Input({ required: true }) autocompleteData!: Array<AutocompleteItem> | ((search?: string | null) => Observable<Array<AutocompleteItem>>);

/**
* Time span [ms] has passed without another source emission, to delay data filtering.
* Useful when the user is typing multiple letters
* @default 300 [ms]
*/
@Input() debounceTime = 300;

/**
* The input placeholder
*/
@Input() placeholder = '';

/**
* Fired when the Autocomplete Item has been selected
*/
@Output() autocompleteSelectedEvent: EventEmitter<AutocompleteItem> = new EventEmitter();

protected showAutocompletion = false;

/** Observable da cui vengono emessi i risultati dell'auto completamento */
protected autocompleteResults$: Observable<{
searchedValue: string | undefined | null;
relatedEntries: Array<AutocompleteItem>;
}> = new Observable();

override ngOnInit() {
super.ngOnInit();
this.autocompleteResults$ = this.getAutocompleteResults$();
}

/**
* Create the autocomplete list
*/
private getAutocompleteResults$(): Observable<{
searchedValue: string | null | undefined;
relatedEntries: Array<AutocompleteItem>;
}> {
return this.control.valueChanges.pipe(
debounceTime(this.debounceTime), // Delay filter data after time span has passed without another source emission, useful when the user is typing multiple letters
distinctUntilChanged(), // Only if searchValue is distinct in comparison to the last value
switchMap(searchedValue => {
if (!this.autocompleteData) {
return of({
searchedValue,
relatedEntries: <Array<AutocompleteItem>>[],
});
}

const autoCompleteData$ = Array.isArray(this.autocompleteData) ? of(this.autocompleteData) : this.autocompleteData(searchedValue);
return autoCompleteData$.pipe(
map(autocompleteData => {
if (!searchedValue || typeof searchedValue === 'number') {
return { searchedValue, relatedEntries: [] };
}

const lowercaseValue = searchedValue.toLowerCase();
const relatedEntries = autocompleteData.filter(item => item.value?.toLowerCase().includes(lowercaseValue));

return { searchedValue, relatedEntries };
})
);
})
);
}

protected onEntryClick(entry: AutocompleteItem, event: Event) {
// Se non è stato definito un link associato all'elemento dell'autocomplete, probabilmente il desiderata
// non è effettuare la navigazione al default '#', pertanto in tal caso meglio annullare la navigazione.
if (!entry.link) {
event.preventDefault();
}

this.autocompleteSelectedEvent.next(entry);
this.control.setValue(entry.value);
this.showAutocompletion = false;
}

protected autocompleteItemTrackByValueFn(index: number, item: AutocompleteItem) {
return item.value;
}

protected onKeyDown() {
this.showAutocompletion = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { ItSelectComponent } from './select/select.component';
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';

const formComponents = [
ItAutocompleteComponent,
ItCheckboxComponent,
ItInputComponent,
ItPasswordInputComponent,
Expand All @@ -21,11 +22,11 @@ const formComponents = [
ItSelectComponent,
ItTextareaComponent,
ItUploadDragDropComponent,
ItUploadFileListComponent
ItUploadFileListComponent,
];

@NgModule({
imports: formComponents,
exports: formComponents
exports: formComponents,
})
export class ItFormModule { }
export class ItFormModule {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<div class="form-group">

<div class="input-group" [class.disabled]="!control.enabled" [class.input-number]="type === 'number'"
[class.input-number-currency]="currency" [class.input-number-percentage]="percentage"
<div
class="input-group"
[class.disabled]="!control.enabled"
[class.input-number]="type === 'number'"
[class.input-number-currency]="currency"
[class.input-number-percentage]="percentage"
[class.input-number-adaptive]="adaptive">
<div class="input-group-prepend" [class.d-none]="!prependText.hasChildNodes() && !prepend.hasChildNodes()">
<div #prepend>
Expand All @@ -11,32 +14,60 @@
<ng-content select="[prependText]"></ng-content>
</div>
</div>
<label *ngIf="label" [for]="id" [class.active]="isActiveLabel" [class.input-number-label]="type === 'number'"
[class.empty-prepend-label]="!prependText.hasChildNodes() && !prepend.hasChildNodes()">
{{label}}
</label>

<label
*ngIf="label"
[for]="id"
[class.active]="isActiveLabel"
[class.input-number-label]="type === 'number'"
[class.empty-prepend-label]="!prependText.hasChildNodes() && !prepend.hasChildNodes()">
{{ label }}
</label>

<ng-container *ngIf="type === 'number'">
<span *ngIf="currency || percentage" class="input-group-text fw-semibold">{{symbol}}</span>
<input type="number" [id]="id" [step]="step ?? null" [min]="min ?? null" [max]="max ?? null"
[class.form-control]="readonly !== 'plaintext'" [class.form-control-plaintext]="readonly === 'plaintext'"
[class.is-invalid]="isInvalid" [class.is-valid]="isValid" [formControl]="control" [placeholder]="placeholder"
[readonly]="isReadonly" [autocomplete]="autocomplete" [attr.aria-describedby]="id + '-description'"
<span *ngIf="currency || percentage" class="input-group-text fw-semibold">{{ symbol }}</span>
<input
type="number"
[id]="id"
[step]="step ?? null"
[min]="min ?? null"
[max]="max ?? null"
[class.form-control]="readonly !== 'plaintext'"
[class.form-control-plaintext]="readonly === 'plaintext'"
[class.is-invalid]="isInvalid"
[class.is-valid]="isValid"
[formControl]="control"
[placeholder]="placeholder"
[readonly]="isReadonly"
[autocomplete]="autocomplete"
[attr.aria-describedby]="id + '-description'"
(blur)="markAsTouched()" />
<span class="input-group-text align-buttons flex-column">
<button type="button" class="input-number-add" [disabled]="!control.enabled" (click)="incrementNumber()">
<span class="visually-hidden">{{'it.form.increase-value' | translate}}</span>
<span class="visually-hidden">{{ 'it.form.increase-value' | translate }}</span>
</button>
<button type="button" class="input-number-sub" [disabled]="!control.enabled" (click)="incrementNumber(true)">
<span class="visually-hidden">{{'it.form.decrease-value' | translate}}</span>
<span class="visually-hidden">{{ 'it.form.decrease-value' | translate }}</span>
</button>
</span>
</ng-container>
<input *ngIf="type !== 'number'" [id]="id" [type]="type" [max]="type === 'date' ? maxDate : undefined"
[min]="type === 'date' ? minDate : undefined" [class.form-control]="readonly !== 'plaintext'"
[class.form-control-plaintext]="readonly === 'plaintext'" [class.is-invalid]="isInvalid"
[class.is-valid]="isValid" [formControl]="control" [placeholder]="placeholder" [readonly]="isReadonly"
(keydown)="onKeyDown()" [autocomplete]="autocomplete" [attr.aria-describedby]="id + '-description'"
(blur)="markAsTouched()">

<input
*ngIf="type !== 'number'"
[id]="id"
[type]="type"
[max]="type === 'date' ? maxDate : undefined"
[min]="type === 'date' ? minDate : undefined"
[class.form-control]="readonly !== 'plaintext'"
[class.form-control-plaintext]="readonly === 'plaintext'"
[class.is-invalid]="isInvalid"
[class.is-valid]="isValid"
[formControl]="control"
[placeholder]="placeholder"
[readonly]="isReadonly"
[autocomplete]="autocomplete"
[attr.aria-describedby]="id + '-description'"
(blur)="markAsTouched()" />

<div class="input-group-append">
<ng-content select="[append]"></ng-content>
Expand All @@ -47,44 +78,12 @@
</div>
</div>

<small *ngIf="description" [id]="id + '-description'" class="form-text">{{description}}</small>

<!-- INIZIO gestione AUTOCOMPLETAMENTO -->
<ng-container *ngIf="type === 'search'">
<!-- Icona lente per autocompletamento -->
<span class="autocomplete-icon" aria-hidden="true">
<it-icon name="search" size="sm"></it-icon>
</span>

<ng-container *ngIf="autocompleteResults$ | async as autocomplete">
<!-- Lista di autocompletamento -->
<ul class="autocomplete-list"
[class.autocomplete-list-show]="autocomplete.relatedEntries?.length && showAutocompletion">
<li *ngFor="let entry of autocomplete.relatedEntries; trackBy: autocompleteItemTrackByValueFn"
(click)="onEntryClick(entry, $event)">
<a [href]="entry.link">
<ng-container *ngTemplateOutlet="autocompleteItemTemplate"></ng-container>
</a>
<ng-template #autocompleteItemTemplate>
<div class="avatar size-sm" *ngIf="entry.avatarSrcPath">
<img [src]="entry.avatarSrcPath" [alt]="entry.avatarAltText">
</div>
<it-icon *ngIf="entry.icon" [name]="entry.icon" size="sm"></it-icon>
<span class="autocomplete-list-text">
<span [innerHTML]="entry.value | markMatchingText: autocomplete.searchedValue"></span>
<em *ngIf="entry.label">{{entry.label}}</em>
</span>
</ng-template>
</li>
</ul>
</ng-container>
</ng-container>
<!-- FINE gestione AUTOCOMPLETAMENTO -->
<small *ngIf="description" [id]="id + '-description'" class="form-text">{{ description }}</small>

<div *ngIf="isInvalid" class="form-feedback just-validate-error-label" [id]="id + '-error'">
<div #customError>
<ng-content select="[error]"></ng-content>
</div>
<ng-container *ngIf="!customError.hasChildNodes()">{{invalidMessage | async}}</ng-container>
<ng-container *ngIf="!customError.hasChildNodes()">{{ invalidMessage | async }}</ng-container>
</div>
</div>
</div>
Loading

0 comments on commit 082ee93

Please sign in to comment.