Hur man uppgraderar Angular Sorteringsfilter

Introduktion

AngularJS, En av de mest användbara funktionerna i AngularJS ursprungliga erbjudande var förmågan att filtrera och sortera data på sidan med hjälp av endast mallvariabler och filter. Tvåvägs databindning övertygade många konvertiter till AngularJS.

I dag föredrar dock många front-end-utvecklare envägs databindning, och dessa orderBy och filter filter har blivit solnedgång i och med Angular. (Observera: i hela den här artikeln kommer jag att använda ”AngularJS” för att hänvisa till 1.x och bara ”Angular” för att hänvisa till 2+.)

Men hur ska vi kunna uppnå samma effekt? Svaret ligger i våra komponenter, så låt oss titta på ett ngUpgrade-projekt och lära oss hur man gör detta!

Steg 1 – Dra kod från Git

Vi kommer att stegvis uppdatera mallen för en nyskriven komponent. Sedan lägger vi till sortering och filtrering för att återställa all funktionalitet som den hade i AngularJS. Detta är en viktig färdighet att utveckla för ngUpgrade-processen.

För att komma igång tar du dig en stund att klona exempelprojektet som vi kommer att använda (glöm inte att köra npm install i både mapparna public och server). Kolla in denna commit för vår utgångspunkt:

git checkout 9daf9ab1e21dc5b20d15330e202f158b4c065bc3

Detta exempelprojekt är ett ngUpgrade-hybridprojekt som använder både AngularJS 1.6 och Angular 4. Det har ett fungerande Express API och ett Webpack-bygge för både utveckling och produktion. Känn dig fri att utforska, gaffla det och använda mönstren i dina egna projekt. Om du vill titta på en version av det här projektet som använder Angular 5 kan du kolla in den här repo:n. För den här handledningen spelar skillnaderna mellan de två versionerna ingen roll (jag påpekar allt mindre viktigt).

Steg 2 – Ersätt AngularJS-syntaxen

I det här skedet av vår applikation är vår beställningskomponent omskriven i Angular, med alla dess beroenden injicerade och lösta. Om vi skulle försöka köra vår applikation skulle vi dock se fel i konsolen som indikerar problem med vår mall. Det är det som vi måste åtgärda först. Vi ska ersätta AngularJS-syntaxen i ordermallen (orders/orders.html) så att vi kan få rutten att laddas och orderna visas på sidan. Vi fixar filtreringen och sorteringen härnäst.

Det första vi måste göra är att göra oss av med alla instanser av $ctrl i den här mallen. De behövs inte längre i Angular. Vi kan bara göra en sökning och ersätta för att hitta $ctrl. (notera pricken) och ersätta den med ingenting.

Nu ska vi ersätta data-ng-click i vår knapp på rad 13. I Angular använder vi istället för ng-click bara click-händelsen, med parenteser för att ange att det är en händelse. Parenteser indikerar en ingång och parenteser indikerar en utgång eller en händelse.

<button type="button" (click)="goToCreateOrder()" class="btn btn-info">Create Order</button>

Vi säger bara här att vi vid klickhändelsen ska avfyra goToCreateOrder-funktionen på vår beställningskomponent.

Innan vi fortsätter ska vi ta en minut för att bevisa att vår komponent faktiskt laddas. Kommentera ut hela div som laddar våra order (från rad 17 och framåt). För att köra programmet öppnar du en terminal och kör följande kommandon:

cd servernpm start

Detta kommer att starta Express-servern. För att köra Webpack dev-servern öppnar du en annan terminal och kör:

cd publicnpm run dev

(Du kan låta dessa processer fortsätta att köras under resten av den här handledningen.)

Du bör se att vårt program laddas igen. Om du går till beställningsrutten ser du att beställningskomponenten visas korrekt.

Skärmdump av appen

Vi kan också klicka på knappen Skapa beställning och den skickar oss korrekt över till rutten och formuläret Skapa beställning.

Okej, låt oss gå tillbaka till HTML. Ta bort kommentaren div (vår app kommer att gå sönder igen).

Låt oss ersätta alla övriga instanser data-ng-click med (click)händelsehanteraren. Du kan antingen använda Find & Replace eller bara använda din editors genväg för att välja alla förekomster (i VS Code for Windows är detta Ctrl+Shift+L).

Nästan ersätter vi alla förekomster av data-ng-show med *ngIf. Det finns faktiskt ingen direkt motsvarighet till ng-show i Angular, men det är okej. Det är att föredra att använda *ngIf, eftersom du på så sätt faktiskt lägger till och tar bort element från DOM i stället för att bara dölja och visa dem. Så allt vi behöver göra är att hitta våra data-ng-shows och ersätta dem med *ngIf.

Slutligt måste vi göra två saker för att fixa vår tabellkropp. För det första ersätter vi data-ng-repeat med *ngFor="let order of orders". Observera att vi också tar bort filtren orderBy och filter i den raden så att hela tr ser ut så här:

<tr *ngFor="let order of orders">

För det andra kan vi ta bort prefixet data-ng före länken href till rutten för orderdetaljer. AngularJS hanterar fortfarande routingen här, men vi behöver inte använda det prefixet längre eftersom detta nu är en Angular-mall.

Om vi tittar på applikationen igen kan du se att beställningarna laddas korrekt på skärmen:

Orders from app screenshot

Det finns förstås ett par saker som är fel. Sorteringslänkarna fungerar inte längre, och nu är vår valuta lite rörig eftersom valutaröret i Angular är något annorlunda än dess motsvarighet i AngularJS. Vi kommer att ta itu med det. För tillfället är detta ett bra tecken, eftersom det betyder att våra data kommer till komponenten och laddas på sidan. Så vi har fått grunderna i den här mallen konverterade till Angular. Nu är vi redo att ta itu med vår sortering och filtrering!

Steg 3 – lägga till sortering

Vi har våra beställningar som laddas på skärmen, men vi har inget sätt att beställa eller sortera dem ännu. I AngularJS var det väldigt vanligt att använda det inbyggda orderBy-filtret för att sortera data på sidan. Angular har inte längre något orderBy-filter. Detta beror på att det nu starkt uppmuntras att flytta den typen av affärslogik till komponenten i stället för att ha den i mallen. Så det är vad vi ska göra här. (Observera: Vi kommer att använda vanliga funktioner och händelser här, inte ett reaktivt formulär. Detta beror på att vi bara försöker ta små steg för att förstå dessa saker. När du väl har fått in grunderna är du välkommen att ta det vidare med observabler!)

Sortering i komponenten

Vi tog redan bort orderBy-filtret från ng-repeat när vi ändrade det till *ngFor. Nu ska vi göra en sorteringsfunktion på beställningskomponenten. Vi kan använda klickhändelserna på våra tabellrubriker för att anropa den funktionen och skicka in egenskapen som vi vill sortera efter. Vi ska också låta den funktionen växla fram och tillbaka mellan stigande och fallande.

Låt oss öppna orderkomponenten (./orders/orders.component.ts) och lägga till två offentliga egenskaper till klassen. Dessa kommer att matcha de två egenskaper som vår mall redan refererar till. Den första kommer att vara sortType av typen string. Den andra kommer att vara sortReverse av typen boolean och vi ställer in standardvärdet till false. Egenskapen sortReverse håller bara reda på om ordningen ska vändas – tänk inte på den som en synonym för stigande eller fallande.

Så du bör nu ha detta efter deklarationen av titeln i klassen:

sortType: string;sortReverse: boolean = false;

Nästan ska vi lägga till funktionen som vi kommer att använda med prototypfunktionen Array.sort i JavaScript. Lägg till detta efter goToCreateOrder-funktionen (men fortfarande inom klassen):

dynamicSort(property) { return function (a, b) { let result = (a < b) ? -1 : (a > b) ? 1 : 0; return result; } }

Denna dynamiska sorteringsfunktion kommer att jämföra egenskapsvärdet för objekt i en array. Den inbäddade ternära funktionen kan vara lite knepig att förstå vid första anblicken, men den säger i princip bara att om värdet på vår egenskap av A är mindre än B, ska du returnera -1. Om det i annat fall är större, returnerar du 1. Om de två är lika stora returnerar du 0.

Nu är detta inte en super sofistikerad eller djupgående jämförelse. Det finns mycket mer sofistikerade hjälpfunktioner som du kan skriva för att sortera åt dig, och experimentera gärna med hur du kan bryta den här. Det räcker dock för våra syften, och du kan bara byta ut den här logiken mot vilken anpassad sorteringslogik som helst.

Så det är vår hjälpfunktion. Sort-funktionen på Array-prototypen kan få en funktion som den sedan kan använda för att jämföra objekt i en array. Låt oss göra en funktion kallad sortOrders på vår klass som utnyttjar detta med den nya dynamicSort-funktionen:

sortOrders(property) { }

Det första vi behöver göra är att ställa in sortType-egenskapen på vår klass lika med den egenskap som skickas in. Sedan vill vi växla över sortReverse-egenskapen. Vi får detta:

sortOrders(property) { this.sortType = property; this.sortReverse = !this.sortReverse;}

Nu kan vi anropa sort-funktionen på this.orders, men skicka in vår dynamiska sorteringsfunktion med vår egenskap:

sortOrders(property) { this.sortType = property; this.sortReverse = !this.sortReverse; this.orders.sort(this.dynamicSort(property));}

Och det finns en sista sak vi måste göra. Vi måste modifiera vår dynamicSort-funktion bara en liten bit för att kunna vända ordningen i matrisen för stigande eller fallande. För att göra detta knyter vi resultatet av dynamicSort till egenskapen sortReverse på klassen.

Det första vi gör är att deklarera en variabel:

let sortOrder = -1;

Därefter kan vi kontrollera om vår sortReverse egenskap på vår klass är sann eller falsk. Om den är sann sätter vi vår variabel för sorteringsordning lika med 1:

if (this.sortReverse) { sortOrder = 1; }

Vi binder ihop våra funktioner på det här sättet eftersom vi gör en växling i vår sorteringsfunktion för demonstrationens skull. För att vara mer noggrann skulle ett annat tillvägagångssätt vara att ha en variabel som heter sortDescending i stället för sortReverse som styrs genom en separat funktion. Om du går den här vägen gör du det motsatta – sortOrder skulle vara 1 om inte sortDescending var sann.

Vi skulle också kunna kombinera de två sista sakerna till ett ternärt uttryck, men för tydlighetens skull kommer jag att lämna det lite mer utförligt. Och sedan för att bara göra vårt resultat motsatsen till vad det normalt skulle vara, kan jag bara multiplicera result med vårt sortOrder. Så vår dynamicSort-funktion ser nu ut så här:

 dynamicSort(property) { let sortOrder = -1; if (this.sortReverse) { sortOrder = 1; } return function(a, b) { let result = a < b ? -1 : a > b ? 1 : 0; return result * sortOrder; }; }

Det här är återigen en demonstrationsimplementering av sortering, så att du förstår de viktigaste koncepten när det gäller att använda en anpassad sorteringsfunktion på din komponent.

Vi får se om sorteringen fungerar

So långt har vi lagt till en dynamicSort-hjälpfunktion och en sortOrders-funktion till vår klass så att vi kan sortera på vår komponent istället för på vår mall.

För att se om dessa funktioner fungerar lägger vi till en standardsortering till vår ngOnInit-funktion.

Inuti vår forkJoin-prenumeration, efter forEach där vi lägger till egenskapen kundnamn, anropar vi this.sortOrders och skickar in egenskapen totala artiklar:

this.sortOrders('totalItems');

När skärmen uppdateras bör du se att beställningarna sorteras efter de totala artiklarna.

Nu behöver vi bara implementera denna sortering i vår mall genom att anropa funktionen sortOrders i länkarna från tabellhuvudet.

Lägg till sortering i mallen

Vi har fått vår sortOrders-funktion att fungera korrekt på vår beställningskomponent, vilket innebär att vi nu är redo att lägga till den i vår mall så att tabellrubrikerna blir klickbara igen.

Förut ska vi ändra standardsorteringen i vår ngOnInit-funktion till att bara vara ID:

this.sortOrders('id');

Det är lite mer normalt än att använda totala antalet artiklar.

Nu kan vi arbeta med vår mall. Det första vi vill göra är att anropa sortOrders-funktionen i alla våra klickhändelser. Du kan markera instanserna av sortType = och ersätta dem med sortOrders(. Därefter kan du ersätta instanserna av ; sortReverse = !sortReverse med ).

Vi måste också rätta till två av de egenskapsnamn som vi skickar in här, liksom i *ngIf-instanserna. Ersätt de tre instanserna av orderId med id och de tre instanserna av customername med customerName.

Det sista jag behöver göra är att omsluta var och en av href-taggarna i rubrikerna i parenteser så att Angular tar över och dessa länkar faktiskt inte kommer att gå någonstans. Det är klickhändelsen som kommer att utlösas. Så rubrikerna bör följa det här mönstret:

<th> <a ="" (click)="sortOrders('id')"> Order Id <span *ngIf="sortType == 'id' && !sortReverse" class="fa fa-caret-down"></span> <span *ngIf="sortType == 'id' && sortReverse" class="fa fa-caret-up"></span> </a></th>

Hoppa över till webbläsaren och testa alla dina länkar till tabellrubrikerna. Du bör se att var och en av våra egenskaper nu sorteras, både i stigande och fallande ordning. Fantastiskt!

Det här är jättebra, men vi förlorade en sak – vår markör är en selektor, inte en pekare. Vi fixar det med lite CSS.

Fix the Cursor

Vi har fått vår sortering att fungera korrekt på vår beställningssida, men vår markör är nu en selektor i stället för en pekare, och det är irriterande.

Det finns ett par olika sätt att använda CSS för att åtgärda detta:

  • Vi kan skapa en klass i vår SCSS-fil för huvudapplikationen.
  • Vi skulle kunna skriva in-line CSS, även om det nästan aldrig är att föredra.
  • Vi skulle kunna dra nytta av Angulars scoped CSS med hjälp av styles-alternativet i komponentdekoratorn

Vi kommer att välja det sista alternativet, eftersom allt vi behöver göra är att lägga till en regel i våra styles för just den här komponenten.

Öppna upp komponentklassen orders igen. I komponentdekoratorn kan vi lägga till en ny egenskap som heter styles. Styles är en array av strängar, men strängarna är CSS-regler. För att fixa vår markör behöver vi bara skriva ut en regel som säger att om vi i en tabellrad har en länk så ändrar vi marköregenskapen till pekare. Vår dekorator kommer nu att se ut så här:

@Component({ selector: 'orders', template: template, styles: })

Nu, när vi håller muspekaren över våra radsrubriker ser du att vi har pekaren. Det coola med det här tillvägagångssättet är att den här CSS-regeln inte kommer att påverka några andra komponenter. Den kommer bara att gälla för vår beställningskomponent!

Nu ska vi se om vi kan göra något åt vår filtrering. Det där ”filterfiltret” togs bort från Angular, så vi måste vara kreativa och komma på ett sätt att implementera det på vår komponent.

Steg 4 – Lägg till filtrering

Vi är redo att ersätta vår filterruta som använde AngularJS-filtret för att söka igenom ordersamlingen baserat på en sträng som vi sökte. AngularJS-filtret bodde på vår mall och krävde ingen kod i vår controller eller komponent. Numera avråds den typen av logik i mallen. Det är att föredra att göra den typen av sortering och filtrering i vår komponentklass.

Lägg till en filterfunktion

I vår komponent ska vi skapa en ny array av beställningar som heter filteredOrders. Sedan ska vi skicka vår array orders till en filterfunktion som ställer in array filteredOrders. Slutligen använder vi filteredOrders på vår mall i vår *ngFor i stället för vår ursprungliga array. På så sätt ändrar vi aldrig data som kommer tillbaka från servern, vi använder bara en delmängd av den.

Det första vi gör är att deklarera den nya egenskapen på vår klass :

filteredOrders: Order;

Därefter kan vi i vår forkJoin som ställer in vår ursprungliga array av beställningar, ställa in det initiala tillståndet för filteredOrders till vår array av beställningar:

this.filteredOrders = this.orders;

Nu är vi redo att lägga till vår funktion som faktiskt kommer att göra filtreringen åt oss. Klistra in den här funktionen precis efter våra sorteringsfunktioner längst ner i vår komponent:

filterOrders(search: string) { this.filteredOrders = this.orders.filter(o => Object.keys(o).some(k => { if (typeof o === 'string') return o.toLowerCase().includes(search.toLowerCase()); }) ); }

Låt oss prata om vad som händer i den här funktionen. Först ger vi funktionen en strängegenskap på search. Sedan loopar vi genom våra beställningar och hittar sedan alla nycklar till objekten. För alla nycklar ska vi se om det finns some-värden för dessa egenskaper som matchar vår sökterm. Den här delen av JavaScript kan se lite förvirrande ut till en början, men det är i princip vad som händer.

Bemärk att vi i vår if-angivelse uttryckligen testar för strängar. I vårt exempel just nu ska vi bara begränsa vår fråga till strängar. Vi kommer inte att försöka hantera nested properties, number properties eller något liknande. Vår sökterm kommer att matcha vår egenskap kundnamn, och om vi någonsin väljer att visa vår adress eller någon annan strängegenskap kommer den att söka igenom dessa också.

Självklart kan vi också modifiera den här funktionen för att testa för nummer, eller leta igenom ytterligare ett lager av nästlade objekt, och det är helt upp till dig. Precis som med vår sortering kommer vi att börja med en demonstrationsimplementation och låta dig använda din fantasi för att göra det mer komplext.

På tal om sortOrders-funktionen måste vi, innan vi går vidare, göra en sista sak på komponenten. Vi behöver bara ändra sortOrders så att den använder filteredOrders nu och inte vår ursprungliga orders, eftersom vi vill att filtret ska prioriteras framför sorteringen. Ändra det bara till detta:

sortOrders(property) { this.sortType = property; this.sortReverse = !this.sortReverse; this.filteredOrders.sort(this.dynamicSort(property));}

Nu är vi redo att implementera denna filtrering på mallen.

Add Filtering to the Template

Låt oss flytta tillbaka till vår mall och fixa den så att den använder vår filtrering.

Det första vi behöver göra är att ersätta data-ng-model. Istället ska vi använda keyup-händelsen, så vi skriver ”keyup” och omger det med parenteser ((keyup)). Detta är en inbyggd händelse i Angular som låter oss köra en funktion när en inmatning knapps upp. Eftersom vi namngav vår funktion filterOrders, som tidigare var namnet på den egenskap som vi skickade in i AngularJS-filtret, behöver vi bara lägga till parenteser bredvid. Vår indata ser hittills ut så här:

<input type="text" class="form-control" placeholder="Filter Orders (keyup)="filterOrders()">

Men vad skickar vi in i funktionen för filterorder? Tja, som standard passerar events något som heter $event. Detta innehåller något som kallas target, som sedan innehåller värdet på inmatningen. Det finns ett problem med att använda $event. Det är mycket svårt att hålla reda på dessa nebulösa typer eftersom target.value verkligen kan vara vad som helst. Detta gör det svårt att felsöka eller veta vilken typ av värde som förväntas. Istället har Angular en riktigt smart sak som vi kan göra, nämligen att tilldela en mallvariabel till den här inmatningen.

Troligt nog tillhandahåller Angular en metod för oss att göra detta. Efter vår ingångstagg kan vi lägga till hashtecknet (#) och sedan namnet på vår önskade modell. Låt oss kalla den #ordersFilter. Det spelar egentligen ingen roll var i taggen du lägger detta eller vad du kallar det, men jag gillar att lägga det efter inmatningen så att du fångar vilken modell som är associerad med vilken inmatning om jag bara tittar ner på sidan.

Nu kan jag skicka denna variabel till vår filterOrders funktion på keyup händelsen. Vi behöver inte hash-symbolen före den, men vi behöver lägga till .value. Detta kommer att passera modellens faktiska värde och inte hela modellen själv. Vår färdiga indata ser ut så här:

<input #ordersFilter type="text" class="form-control" placeholder="Filter Orders" (keyup)="filterOrders(ordersFilter.value)">

Slutligt måste vi ändra vår *ngFor så att den använder arrayen filteredOrders istället för den vanliga orders arrayen:

<tr *ngFor="let order of filteredOrders">

Inspektera produkten

Du kan se hur mycket renare vår mall är nu när vår filtrering och sortering finns i komponenten.

Nu kan vi kolla upp detta i webbläsaren. Om du skriver in lite text i rutan, till exempel ”sally”, bör du se att våra beställningar ändras och att sorteringen fungerar ovanpå det:
Animation av fungerande app
Awesome, vi har ersatt ytterligare en AngularJS-funktion!

Nu har vi bara en sista sak vi behöver göra på den här komponenten – fixa currency pipe.

Steg 5 – Fixa currency pipe

Vår sista åtgärd är att uppdatera det tidigare valutafiltret, som nu kallas currency pipe i Angular. Vi behöver bara lägga till ett par parametrar till pipen i mallen som vi inte behövde ange i AngularJS. Den här delen skiljer sig åt om du använder Angular 4 eller Angular 5:.

I Angular 4 gör du så här:
<td>{{order.totalSale | currency:'USD':true}}</td>

I Angular 5+ gör du så här:
<td>{{order.totalSale | currency:'USD':'symbol'}}</td>

Det första alternativet är valutakoden (det finns många, du är inte begränsad till amerikanska dollar!). Det andra alternativet är symbolvisningen. I Angular 4 är detta en boolean som anger om valutasymbolen eller koden ska användas. I Angular 5+ är alternativen symbol, code eller symbol-narrow som strängar.

Du bör nu se den förväntade symbolen:
Screenshot av kolumnen Total Sale i appen

Och vi är klara! För att se den färdiga koden, kolla in denna commit.

Slutsats

Du gjorde ett bra jobb genom att hålla fast vid detta till slutet! Här är vad vi har åstadkommit i den här guiden:

  1. Förändra syntaxen för AngularJS-mallar till Angular-syntax
  2. Förflytta sortering till komponenten
  3. Användning av scoped CSS-stilar
  4. Förflytta filtrering till komponenten
  5. Förändra AngularJS-valutafiltret till Angular-valutaröret

Vart ska du gå härifrån? Det finns många saker du skulle kunna göra:

  • Gör sorteringen mer sofistikerad (till exempel: ska ordningen återställas eller förbli densamma när användaren klickar på en ny rubrik?)
  • Gör filtreringen mer sofistikerad (sök efter siffror eller nästlade egenskaper)
  • Ändra till en reaktiv strategi. Du kan lyssna på en observabel av värdeförändringar i stället för keyup-funktionen och göra sortering och filtrering där. Om du använder observabler kan du också göra riktigt häftiga saker som att avbryta inmatningen!