npm package: @mediacologne/ng-data
Latest version: 13.2.0

# @mediacologne/ng-data


# News


- Achtung! Dies ist eine Angular >= 6 Library!


# Installation

## Install the NPM Module

```bash
$ npm install @mediacologne/ng-data
```

## Using the Library

Nach der Installation muss die Library durch das Importieren des `DataModule` verfügbar gemacht werden.

```typescript
// Import your library
import { DataModule } from '@mediacologne/ng-data';

@NgModule({
  imports: [
    DataModule.forRoot({
        store: {
            subscriptions: {
                unsubscribe: false
            }
        }
    })
  ]
})
export class AppModule { }
```

# Documentation

## Configuration via `forRoot`

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**
```typescript
@NgModule({
  declarations: [],
  imports: [],
  providers: [
      {
          provide: 'CustomerStore',
          useFactory: DataStoreFactory.create('customer')
      }
  ],
})
export class AppModule {
}
```

**Inject the DataStore in your constructor**
```typescript
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:

```typescript
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`.

```typescript
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.

```html
<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.

```typescript
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:

```typescript
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!)).

```typescript
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!**

```typescript
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:
```typescript
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.

```typescript
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**
```typescript
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


```typescript
// 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);
});
```

```html
<!-- 
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}}
```