Skip to content

Commit

Permalink
feat: search auto complete
Browse files Browse the repository at this point in the history
Co-authored-by: cristian.borelli <[email protected]>
  • Loading branch information
cri99 and cristian.borelli authored Jan 17, 2023
1 parent 0c2646a commit f608d1a
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
[formControl]="control"
[placeholder]="placeholder"
[readonly]="isReadonly"
(keydown)="onKeyDown()"
[attr.aria-describedby]="id + '-description'"
(blur)="markAsTouched()">

Expand All @@ -62,6 +63,45 @@
</div>

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




<!-- INIZIO gestione AUTOCOMPLETAMENTO -->


<!-- Icona lente per autocompletamento -->
<span class="autocomplete-icon" aria-hidden="true" *ngIf="isAutocompletable()">
<it-icon name = "search" size="sm"></it-icon>
</span>

<ng-container *ngIf="autocompleteResults$ | async as autocomplete">
<!-- Lista di autocompletamento -->
<ul class="autocomplete-list" *ngIf="isAutocompletable()" [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.original | markMatchingText: autocomplete.searchedValue"></span>
<em *ngIf="entry.label">{{entry.label}}</em>
</span>
</ng-template>
</li>
</ul>
</ng-container>


<!-- FINE gestione AUTOCOMPLETAMENTO -->




<div *ngIf="isInvalid" class="form-feedback just-validate-error-label" [id]="id + '-error'">
<div #customError>
<ng-content select="[error]"></ng-content>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Component, Input } from '@angular/core';
import { AbstractFormComponent } from '../../../abstracts/abstract-form-component';
import { InputControlType } from '../../../interfaces/form';
import { AutoCompleteItem, InputControlType } from '../../../interfaces/form';
import { AbstractControl, ValidatorFn, Validators } from '@angular/forms';
import { ItValidators } from '../../../validators/it-validators';
import { BooleanInput, isTrueBooleanInput } from '../../../utils/boolean-input';
import { Observable } from 'rxjs';
import { from, map, Observable, of } from 'rxjs';

@Component({
selector: 'it-input[id]',
Expand Down Expand Up @@ -65,6 +65,19 @@ export class InputComponent extends AbstractFormComponent<string | number> {
*/
@Input() adaptive?: BooleanInput;

/**
* Opzionale.
* Disponibile solo se il type è search.
* Indica la lista di elementi ricercabili su cui basare il sistema di autocompletamento della input
*/
@Input()
set autoCompleteData(value: Array<AutoCompleteItem>) { this._autoCompleteData = value; }
get autoCompleteData(): Array<AutoCompleteItem> { return this._autoCompleteData; }
private _autoCompleteData: Array<AutoCompleteItem> = [];

showAutocompletion = false;


get isActiveLabel(): boolean {
const value = this.control.value;
if ((!!value && value !== 0) || value === 0 || !!this.placeholder) {
Expand Down Expand Up @@ -136,6 +149,10 @@ export class InputComponent extends AbstractFormComponent<string | number> {
return super.invalidMessage;
}

/** Observable da cui vengono emessi i risultati dell'auto completamento */
autocompleteResults$: Observable<{ searchedValue: string, relatedEntries: Array<AutoCompleteItem & { original: string, lowercase: string }>}> = new Observable();


override ngOnInit() {
super.ngOnInit();

Expand Down Expand Up @@ -163,6 +180,7 @@ export class InputComponent extends AbstractFormComponent<string | number> {
}

this.addValidators(validators);
this.autocompleteResults$ = this.getAutocompleteResults$();
}

/**
Expand All @@ -187,4 +205,61 @@ export class InputComponent extends AbstractFormComponent<string | number> {
this.control.setValue(value);
}



getAutocompleteResults$(): Observable<{ searchedValue: string, relatedEntries: Array<AutoCompleteItem & { original: string, lowercase: string }>}> {

if(this.type === 'search') {
return this.control.valueChanges.pipe(map((value) => {
const searchedValue = value;
if (searchedValue) {
const lowercaseValue = searchedValue.toLowerCase();
const lowercaseData = this._autoCompleteData.filter((item) => item.value).map(item => {
return { ...item, original : item.value, lowercase : item.value.toLowerCase() };
});

const relatedEntries: Array<AutoCompleteItem & { original: string, lowercase: string }> = [];
lowercaseData.forEach(lowercaseEntry => {
const matching = (lowercaseEntry.lowercase).includes(lowercaseValue);
if (matching) {
relatedEntries.push(lowercaseEntry);
}
});

return { searchedValue, relatedEntries };
} else {
return { searchedValue, relatedEntries: []};
}
}));
} else {
return of({searchedValue: '', relatedEntries: []});
}

}

isAutocompletable() {
if (this._autoCompleteData && this.type === 'search') {
return this._autoCompleteData.length > 0;
} else {
return false;
}
}

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.control.setValue(entry.value);
this.showAutocompletion = false;
}

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

onKeyDown() {
this.showAutocompletion = this.type === 'search';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {BooleanInput} from "../../../utils/boolean-input";
export class IconComponent implements AfterViewInit {

/**
* The icon size
* The icon name
*/
@Input() name!: IconName;

Expand Down
23 changes: 22 additions & 1 deletion projects/design-angular-kit/src/lib/interfaces/form.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export type InputControlType = 'text' | 'email' | 'number' | 'date' | 'time' | 'tel' | 'color' | 'url';
import { IconName } from "./icon";

export type InputControlType = 'text' | 'email' | 'number' | 'date' | 'time' | 'tel' | 'color' | 'url' | 'search';

export interface SelectControlOption {
value: any,
Expand Down Expand Up @@ -49,3 +51,22 @@ export interface UploadFileListItem {
*/
tooltip?: string
}


/**
* Elemento disponibile per l'autocompletamento del it-form-input
*/
export interface AutoCompleteItem {
/** Valore voce di autocompletamento */
value: string;
/** Opzionale. Path in cui ricercare l'immagine dell'avatar da posizionare a sinistra della voce di autocompletamento */
avatarSrcPath?: string;
/** Opzionale. Testo in alternativa dell'avatar per accessibilità */
avatarAltText?: string;
/** Opzionale. Icona posizionata a sinistra della voce di autocompletamento */
icon?: IconName;
/** Opzionale. Label posizionata a destra della voce di autocompletamento */
label?: string;
/** Opzionale. Link relativo all'elemento */
link?: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { ForwardDirective } from '../components/core/forward/forward.directive';
import { DimmerComponent } from '../components/core/dimmer/dimmer.component';
import { DimmerButtonsComponent } from '../components/core/dimmer/dimmer-buttons/dimmer-buttons.component';
import { DimmerIconComponent } from '../components/core/dimmer/dimmer-icon/dimmer-icon.component';
import { MarkMatchingTextPipe } from '../pipes/mark-matching-text.pipe';


/**
Expand Down Expand Up @@ -120,7 +121,8 @@ const navigation = [
*/
const utils = [
IconComponent,
NotFoundPageComponent
NotFoundPageComponent,
MarkMatchingTextPipe
];

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Pipe, PipeTransform } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";

@Pipe({
name: 'markMatchingText'
})
export class MarkMatchingTextPipe implements PipeTransform {
constructor(private domSanitizer: DomSanitizer) {}

transform(allString: string, searchString: string): any {
if (!searchString) {
return allString;
} else if(!allString) {
return "";
}
// Check if search string is a substring of pivot string (no case-sensitive)
const idxOfMatchString = allString.toLowerCase().indexOf(searchString.toLowerCase());
if(idxOfMatchString !== -1) {
// retrieve the exactly substring
const matchingString = allString.substring(idxOfMatchString, idxOfMatchString + searchString.length);
// Replace original string marking as <strong> (bold) the matchinng substring
const regEx = new RegExp('(' + matchingString + ')', 'gi')
const res = allString.replace(regEx, '<mark>$1</mark>');
return this.domSanitizer.bypassSecurityTrustHtml(res);
}

return allString;

}


}
3 changes: 3 additions & 0 deletions projects/design-angular-kit/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export * from './lib/components/utils/not-found-page/not-found-page.component';
// Services
export * from './lib/services/notifications/notifications.service';

// Pipes
export * from './lib/pipes/mark-matching-text.pipe';

// Interfaces
export * from './lib/interfaces/core';
export * from './lib/interfaces/form';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ <h3>Interazione con Form Input</h3>
[readonly]="readOnly"
[type]="type !== 'password' ? type : 'text'"
[(ngModel)]="value"
[description]="note">
<!-- [autoCompleteData]="autoCompleteData"-->
[description]="note"
[autoCompleteData]="autoCompleteData">
</it-input>
<it-password-input *ngIf="type === 'password'" id="password-input-0"
[label]="label"
Expand Down Expand Up @@ -54,8 +54,8 @@ <h5>Tipo Input</h5>
<it-radio-button id="radio-number" name="number" [(ngModel)]="type" value="number"
label="number"></it-radio-button>
<it-radio-button id="radio-time" name="time" [(ngModel)]="type" value="time" label="time"></it-radio-button>
<!-- <it-radio-button id="radio-search" name="search" [(ngModel)]="type" value="search"-->
<!-- label="search"></it-radio-button>-->
<it-radio-button id="radio-search" name="search" [(ngModel)]="type" value="search"
label="search"></it-radio-button>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { InputControlType } from 'projects/design-angular-kit/src/public_api';
import { AutoCompleteItem, InputControlType } from 'projects/design-angular-kit/src/public_api';

@Component({
selector: 'it-form-input-example',
Expand All @@ -11,8 +11,8 @@ export class FormInputExampleComponent {
i = 0;
readOnly = false;
disabled = false;
type: InputControlType | 'password' = 'text';
icon = 'it-pencil';
type: InputControlType | 'password' = 'search';
icon = 'pencil';
value = 'myNgModel';

get placeholder() {
Expand All @@ -33,42 +33,42 @@ export class FormInputExampleComponent {

hasNote = false;

// get autoCompleteData(): AutoCompleteItem[] {
// return this._autoCompleteData;
// }
// set autoCompleteData(value: AutoCompleteItem[]) {
// this._autoCompleteData = value;
// }
// private _autoCompleteData: AutoCompleteItem[] = [
// {
// value: 'Luisa Neri',
// avatarSrcPath: 'https://randomuser.me/api/portraits/women/44.jpg',
// avatarAltText: 'Luisa Neri',
// label: 'Profilo'
// },
// {
// value: 'Cristian Borelli',
// avatarSrcPath: 'https://randomuser.me/api/portraits/men/1.jpg',
// avatarAltText: 'Cristian Borelli',
// label: 'Profilo'
// },
// {
// value: 'Andrea Stagi',
// avatarSrcPath: 'https://randomuser.me/api/portraits/men/2.jpg',
// avatarAltText: 'Andrea Stagi',
// label: 'Profilo'
// },
// {
// value: 'Comune di Firenze',
// icon: 'it-pa',
// link: 'https://www.comune.fi.it/',
// label: 'Comune'
// },
// {
// value: 'Italia',
// avatarSrcPath: 'https://raw.githubusercontent.com/lipis/flag-icons/main/flags/4x3/it.svg',
// avatarAltText: 'Italia'
// }
// ];
get autoCompleteData(): AutoCompleteItem[] {
return this._autoCompleteData;
}
set autoCompleteData(value: AutoCompleteItem[]) {
this._autoCompleteData = value;
}
private _autoCompleteData: AutoCompleteItem[] = [
{
value: 'Luisa Neri',
avatarSrcPath: 'https://randomuser.me/api/portraits/women/44.jpg',
avatarAltText: 'Luisa Neri',
label: 'Profilo'
},
{
value: 'Cristian Borelli',
avatarSrcPath: 'https://randomuser.me/api/portraits/men/1.jpg',
avatarAltText: 'Cristian Borelli',
label: 'Profilo'
},
{
value: 'Andrea Stagi',
avatarSrcPath: 'https://randomuser.me/api/portraits/men/2.jpg',
avatarAltText: 'Andrea Stagi',
label: 'Profilo'
},
{
value: 'Comune di Firenze',
icon: 'pa',
link: 'https:www.comune.fi.it/',
label: 'Comune'
},
{
value: 'Italia',
avatarSrcPath: 'https:raw.githubusercontent.com/lipis/flag-icons/main/flags/4x3/it.svg',
avatarAltText: 'Italia'
}
];

}

1 comment on commit f608d1a

@vercel
Copy link

@vercel vercel bot commented on f608d1a Jan 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.