@mediacologne/ng-data
News
- Achtung! Dies ist eine Angular >= 6 Library!
Installation
Install the NPM Module
$ npm install @mediacologne/ng-data
Using the Library
Nach der Installation muss die Library durch das Importieren des DataModule
verfügbar gemacht werden.
// Import your library
import { DataModule } from '@mediacologne/ng-data';
@NgModule({
imports: [
DataModule.forRoot({
store: {
subscriptions: {
unsubscribe: false
}
}
})
]
})
export class AppModule { }
Documentation
forRoot
Configuration via Wahrscheinlich wird bei Verwendung des Modules die fromObservable()
Methode genutzt. Dieser wird i.d.R. ein HttpClient request übergeben, welcher
intern in dem Module subscribe
d wird. Mit Hilfe der configuration store.subscribtions.unsubscribe
(default = false) kann spezifiziert werden, dass diese Requests nach einem erfolgreichen
subscribe()
sofort wieder unsubscribed()
werden sollen.
Concepts
Es folgt eine kurze Erklärung über die Konzepte der einzelnen Komponenten dieser Library.
StoreManager
Der StoreManager
kümmert sich um die Instanziierung eines DataStore
s bzw. um die Verwaltung der DataStore
Instanzen. Alle über den StoreManager
erzeugten DataStore
s werden von diesem in einem Service Container vorgehalten.
DataStore
Der DataStore
ist das zentrale Element der Library. Innerhalb des DataStores werden alle Daten gehalten. Jeder DataStore
verfügt über Methoden zum Hinzufügen, Löschen, Updaten von Daten.
Injectable DataStores
In version 8.1.0 support for injectable DataStore
s was added.
The injectable DataStore
allows you to get an (singleton) instance of a DataStore
injected in your constructor. To use this feature, you have to provide the store via your Modules provides section.
Let's see an example:
Provide your injectable DataStore with the DatastoreFactory
@NgModule({
declarations: [],
imports: [],
providers: [
{
provide: 'CustomerStore',
useFactory: DataStoreFactory.create('customer')
}
],
})
export class AppModule {
}
Inject the DataStore in your constructor
export class AppComponent {
constructor(@Inject('CustomerStore') private customerStore: DataStore) {
// Hint: no need to call StoreManager.get('customer')
}
}
Generic Types for DataStore<T>
In version 8.1.0 support for Generics
(<T>
) was added to the DataStore
.
You can decide for yourself if you want to use the new feature or not. In both cases this is not a breaking change.
The benefits of using the new Generics
is type safety and intellisense on all DataStore
operations.
Let's see an example:
interface Customer {
id: number;
name: string
}
export class AppComponent {
// Use Generics <T> with the new injectable DataStores
constructor(@Inject('CustomerStore') private customerStore: DataStore<Customer>) {
}
// Or use the StoreManager to get an instance
public customerStore: DataStore<Customer>;
constructor() {
this.customerStore = StoreManager.get('customer');
}
}
Predicate
Der DataStore nutzt zur Identifikation von DataItems
standardmäßig die id
Property.
Diese wird verwendet um bspw. beim Hinzufügen (Method: add()
) von DataItems
zu erkennen, ob dieses DataItem
bereits im DataStore ist und geupdated werden soll anstatt erneut hinzuzufügen.
Wenn die DataItems
über keine id
Property verfügen, entstehen Duplikate. In diesem Fall kann über den StoreManager
ein IdentityPredicate
übergeben werden.
Dieses überschreibt den internen Wert der IdentityPredicate
.
this.identityLessStore = StoreManager.get('identityLess', {
identityPredicate: "lastname" // Property 'lastname' should be used to identify DataItems
});
Queries
Queries
bieten eine Möglichkeit, innerhalb der Daten eines DataStore
s zu Suchen. Eine Besonderheit der Suchmöglichkeiten sind die StoredQueries
, welche einerseits zum Zeitpunkt der Suche ausgeführt werden, als auch ein weiteres Mal ausgeführt werden sobald sich das Suchergebnis verändert haben könnte. Wird beispielsweise über eine Queries
ein bestimmtes Objekt gesucht welches sich nicht im DataStore
befindet, wird die Queries
automatisch ein weiteres Mal ausgeführt sobald sich neue Daten im Store befinden.
DataStoreView
Mit Hilfe der DataStoreView
s werden die Daten eines DataStore
s auf verschiedene Weisen/Sichten representiert. Der DataStoreView
wird eine DataStore
Instanz zugewiesen, auf welcher die DataStoreView
anschließend operiert.
Die DataStoreView
bietet sogenannte storeFilter
an.
StoreFilter
Mit Hilfe von storeFilter
s können die Daten eines DataStore
s auf verschiedene Sichten representiert werden. Eine storeFilter wird wie ein Filter über die Daten gelegt und steht anschließend innerhalb von Typescript als auch den Templates über eine spezielle StoreFilterPipe
zur Verfügung.
Der Unterschied zwischen StoreFilter
s und Queries
ist im wesentlichen, dass Queries
einmalig ausgeführt werden um ein bestimmtes Ergebnis zu erhalten während ein StoreFilter
vorzugsweise innerhalb eines Templates verwendet wird
ImmutableStoreAdaptor (für ngx-datatable)
Der ImmutableStoreAdapter
wird als Adapter über einen normalen DataStore
gelegt und bietet ebenso die Methoden asArray()
als auch asObservable()
.
Der entscheidende Unterschied zwischen den Methoden des DataStore
's und ImmutableStoreAdaptor
liegt in der immutabilität der Daten beim ImmutableStoreAdaptor
.
Immutable Daten werden zum Beispiel bei Componenten benötigt, welche auf die changeDetection: ChangeDetectionStrategy.OnPush
von Angular setzen und eine Änderung der Daten im DataStore daher nicht mitbekommen können. Durch den ImmutableStoreAdaptor
werden Änderungen am DataStore erkannt und als geklontes Array (mittels ES6 Syntax) [...dataStore.asArray()]
zurück gegeben.
ngx-datatable Searching
Der häufigste Verwendungszweck des ImmutableStoreAdaptor
s ist wahrscheinlich die Verwendung mit einer @swimlane/ngx-datatable
.
Für durchsuchbare Tabellen bietet der ImmutableStoreAdaptor
ein paar Besonderheiten bzw. Features.
Beispiel
Hier wird ein Suchfeld über der Tabelle definiert dessen value
der .search()
Methode des customerStoreAdaptor
übergeben wird.
<input type="text" #searchInput (keyup)="customerStoreAdaptor.search(searchInput.value)">
<ngx-datatable [rows]="(customerStoreAdaptor.asObservable() | async)">
<!-- Column definitions.... -->
</ngx-datatable>
Search Complete Promise
Die customerStoreAdaptor.search(searchInput.value)
gibt einen Promise zurück, welcher resolved sobald die Suche abgeschlossen ist und liefert als Parameter die gefilterten Einträge.
Dies ist sinnvoll, wenn die .search()
Methode programmatisch aufgerufen wird und auf das Ergebnis der Suche gewartet werden möchte.
this.customerStoreAdaptor.search(searchQuery).then((filteredItems: any[]) => { /* do something */ });
Dem Constructor
(bzw. der setSearchFn()
) des ImmutableStoreAdaptors
kann ein Argument vom Typ searchFn | ISearchConfig
übergeben werden.
Siehe nächste Abschnitte für weitere Informationen
SearchFn
Wird eine Function vom Typ searchFn
übergeben, wird diese bei einer Änderung des Suchbegriffs automatisch ausgeführt und erhält als Paramater
einzeln die Einträge des DataStores sowie dem eingegeben Suchbegriff. Die searchFn
wird also für jedes Item im DataStore aufgerufen.
Die Eingabe des Suchbegriffs wird automatisch mittels debounceTime(300)
und distinctUntilChange
verzögert (sofern der Suchbegriff keinem leeren String entspricht).
Die searchFn
könnte z.B. wie folgt aussehen:
new ImmutableStoreAdaptor(this.customerStore, (item: any, searchQuery: string) => {
return item.name.toLowerCase().indexOf(searchQuery) !== -1;
});
SearchFn via ISearchConfig
Anstatt die searchFn
direkt als Argument zu übergeben, kann diese auch via ISearchConfig
übergeben werden.
Dies bietet den Vorteil, dass zusätzliche Konfigurationen übergeben werden können.
Als zusätzliche Option steht in Kombination mit der searchFn derzeit nur debounceTime
zur Verfügung.
Hiermit kann die Zeit in Milisekunden angegeben werden bzw. mittels Wert 0
komplett deaktiviert werden (falls kein debouncing gewünscht ist (nicht empfohlen!)).
new ImmutableStoreAdaptor(this.customerStore, {
searchFn: (item: any, searchQuery: string) => {
return item.name.toLowerCase().indexOf(searchQuery) !== -1;
},
debounceTime: 300 // additionalConfig
});
searchProperties via ISearchConfig
Für die meisten Tabellen reichen einfache Suchalgorithmen aus, bei welchen einzelne Properties der Items im DataStore durchsucht werden sollen.
Dies resultiert in der Praxis aus immer (fast) identisch aussehenden searchFn
. Aus diesem Grund gibt es die searchProperties
!
Hinweis: searchProperties
kann nicht verwendet werden wenn eine searchFn
definiert wurde!
new ImmutableStoreAdaptor(this.customerStore, {
searchProperties: ['name', 'address.city', 'contact'],
debounceTime: 300 // additionalConfig
});
Werden searchProperties
angegeben, findet die Suche automatisch nur innerhalb dieser Properties statt.
Wir stellen uns vor, der DataStore wäre mit Kunden folgender Datenstruktur gefüllt:
interface Customer {
name: string,
logo: string,
address: {
city: string,
zipcode: number
},
contact: {
phone: string,
email: string,
contactPerson: {
name: string,
gender: string
}
}
}
Davon ausgehend, dass die searchProperties
wie folgt definiert wurden: ['name', 'address.city', 'contact']
werden nun folgende Properties durchsucht:
name, address.city, contact.phone, contact.email
.
Hinweis: Handelt es sich bei einer Property um ein Objekt (z.B. contact
), werden dessen Sub-Properties komplett durchsucht. Allerdings findet hier keine rekursion statt!
Deshalb wird contactPerson
oder dessen Sub-Properties (name
oder gender
) nicht durchsucht!
Special Formatter
Für jede searchProperty kann, wenn gewünscht, optional ein formatter angegeben werden, welche vor dem Durchsuchen der Property aufgerufen wird.
Hier ist ein Beispiel, bei dem Umlaute ersetzt werden. Köln würde man nun nur noch mittels Koeln schreibweise finden.
searchProperties: [
'name',
{name: 'address.city', formatter: (value: any) => { return value.replace('ö', 'oe') }},
'address.zipcode',
'start'
]
compareFn via ISearchConfig
Werden bei der Konfiguration die searchProperties
angegeben,
basiert die Durchsuchung des Suchbegriffs innerhalb der Properties standardmäßig auf einem indexOf() !== -1
.
Über die compareFn
lässt sich eine Function angeben, welche Suche durchführen soll.
Dabei wird diese Function pro Item, pro definierter Property einzeln aufgerufen (Anzahl Items x Anzahl searchProperties
), daher sollte diese relativ performant sein!
Beispiel
new ImmutableStoreAdaptor(this.customerStore, {
searchProperties: ['name', 'address.city', 'contact'],
compareFn: (value: any, searchQuery: string) => value.indexOf(searchQuery) !== -1 // Default! ('contains match')
compareFn: (value: any, searchQuery: string) => value == searchQuery // Example for an 'exact match'
compareFn: (value: any, searchQuery: string) => value != searchQuery // Example for a 'not match'
});
Code
// Get a DataStore Instance
this.customerStore = StoreManager.get("customer");
this.customerStore.fromObservable(getDataFromObservable()).then(() => {
// DataStore is filled with data from the given source
});
// Create a DataStoreView from customerStore
this.customerView = new DataStoreView(this.customerStore);
this.customerView.addSelection("fromKoeln", {city: "Köln"});
// Query an object
this.customerView.query({city: "Köln"}).subscribe(result => {
console.log("Result", result);
});
// Query an object as a StoredQuery
this.customerView.queryAsObservable({city: "Köln"}).subscribe(result => {
console.log("Result", result);
});
<!--
Access an Presentation from a DataStoreView Synchronously (as an Array)
-->
{{((customerView | selection:'fromKoeln':false)[0].zipcode}}
<!--
Access an Presentation from a DataStoreView Asynchronously (as an Observable)
This Asynchronously method is preferred if the DataStoreView could contain a large DataStore
-->
{{((customerView | selection:'fromKoeln') | async)[0].zipcode}}