2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing,
12 * software distributed under the License is distributed on an
13 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 * KIND, either express or implied. See the License for the
15 * specific language governing permissions and limitations
19 import {AfterViewInit, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
20 import {concat, merge, Observable, of, Subject} from "rxjs";
21 import {debounceTime, distinctUntilChanged, filter, map, pluck, startWith, switchMap} from "rxjs/operators";
22 import {EntityService} from "@app/model/entity-service";
23 import {FieldToggle} from "@app/model/field-toggle";
24 import {PageQuery} from "../model/page-query";
25 import {LoadingValue} from '../model/loading-value';
26 import {PagedResult} from "@app/model/paged-result";
30 * This component has a search field and pagination section. Entering data in the search field, or
31 * a button click on the pagination triggers a call to a service method, that returns the entity data.
32 * The service must implement the {@link EntityService} interface.
34 * The content is displayed between the search input and the pagination section. To use the data, you should
35 * add an identifier and refer to the item$ variable:
37 * <app-paginated-entities #parent>
39 * <tr ngFor="let entity in parent.item$ | async" >
40 * <td>{{entity.id}}</td>
43 * </app-paginated-entities>
46 * @typeparam T The type of the retrieved entity elements.
49 selector: 'app-paginated-entities',
50 templateUrl: './paginated-entities.component.html',
51 styleUrls: ['./paginated-entities.component.scss']
53 export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, AfterViewInit {
58 * This must be set, if you use the component. This service retrieves the entity data.
60 @Input() service: EntityService<T>;
63 * The number of elements per page retrieved
65 @Input() pageSize = 10;
68 * Two-Way-Binding attribute for sorting field
70 @Input() sortField = [];
72 * Two-Way Binding attribute for sort order
74 @Input() sortOrder = "asc";
79 @Input() pagination = {
87 * If true, all controls are displayed, if the total count is 0
90 displayIfEmpty:boolean=true;
92 * Sets the translation key, for the text to display, if displayIfEmpty=false and the total count is 0.
95 displayKeyIfEmpty:string='form.emptyContent';
98 * If set to true, all controls are displayed, even if there is only one page to display.
99 * Otherwise the controls are not displayed, if there is only a single page of results.
102 displayControlsIfSinglePage:boolean=true;
107 * The current page that is selected
111 * The current search term entered in the search field
116 * Event thrown, if the page value changes
118 @Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
120 * Event thrown, if the search term changes
122 @Output() searchTermChange: EventEmitter<string> = new EventEmitter<string>();
124 @Output() sortFieldChange: EventEmitter<string[]> = new EventEmitter<string[]>();
126 @Output() sortOrderChange: EventEmitter<string> = new EventEmitter<string>();
129 * The total number of elements available for the given search term
131 public total$: Observable<number>;
133 * The entity items retrieved from the service
135 public items$: Observable<LoadingValue<PagedResult<T>>>;
138 * true, if the current page result value represents a result with multiple pages,
141 public multiplePages$:Observable<boolean>;
143 private pageStream: Subject<number> = new Subject<number>();
144 private searchTermStream: Subject<string> = new Subject<string>();
147 // console.log("Construct " + this.id);
150 this.multiplePages$=null;
154 // console.log("Pag Init " + this.id);
155 // We combine the sources for the page and the search input field to a observable 'source'
156 const pageSource = this.pageStream.pipe(map(pageNumber => {
157 return new PageQuery(this.searchTerm, pageNumber);
159 const searchSource = this.searchTermStream.pipe(
161 distinctUntilChanged(),
163 this.searchTerm = searchTerm;
164 return new PageQuery(searchTerm, 1)
166 const source = merge(pageSource, searchSource).pipe(
167 startWith(new PageQuery(this.searchTerm, this.page)),
168 switchMap((params: PageQuery) =>
170 of(LoadingValue.start<PagedResult<T>>()),
171 this.service(params.search, (params.page - 1) * this.pageSize, this.pageSize, this.sortField, this.sortOrder)
172 .pipe(map(pagedResult=>LoadingValue.finish<PagedResult<T>>(pagedResult)))
176 this.total$ = source.pipe(filter(val=>val.hasValue()),map(val=>val.value),
177 pluck('pagination', 'total_count'));
178 this.multiplePages$ = source.pipe(filter(val => val.hasValue()),
179 map(val => val.value.pagination.total_count > val.value.pagination.limit));
180 this.items$ = source;
183 search(terms: string) {
184 // console.log("Keystroke " + terms);
185 this.searchTermChange.emit(terms);
186 this.searchTermStream.next(terms)
189 public changePage(pageNumber: number) {
190 // console.log("Page change " +pageNumber);
191 this.pageChange.emit(pageNumber);
192 this.pageStream.next(pageNumber);
195 private compareArrays(a1: string[], a2: string[]) {
198 if (a1[i] !== a2[i]) return false;
203 toggleSortField(fieldName: string) {
204 this.toggleField([fieldName]);
207 toggleField(fieldArray: string[]) {
208 // console.log("Changing sort field " + fieldArray);
209 let sortOrderChanged: boolean = false;
210 let sortFieldChanged: boolean = false;
211 if (!this.compareArrays(this.sortField, fieldArray)) {
212 // console.log("Fields differ: " + this.sortField + " - " + fieldArray);
213 this.sortField = fieldArray;
214 if (this.sortOrder != 'asc') {
215 this.sortOrder = 'asc';
216 sortOrderChanged = true;
218 sortFieldChanged = true;
220 if (this.sortOrder == "asc") {
221 this.sortOrder = "desc";
223 this.sortOrder = "asc";
225 // console.log("Toggled sort order: " + this.sortOrder);
226 sortOrderChanged = true;
228 if (sortOrderChanged) {
229 //console.log("Sort order changed: "+this.sortOrder)
230 this.sortOrderChange.emit(this.sortOrder);
232 if (sortFieldChanged) {
233 this.sortFieldChange.emit(this.sortField);
235 if (sortFieldChanged || sortOrderChanged) {
237 this.changePage(this.page);
241 ngAfterViewInit(): void {
242 // console.log("Pag afterViewInit " + this.id);
243 // We emit the current value to push them to the containing reading components
244 this.sortOrderChange.emit(this.sortOrder);
245 this.sortFieldChange.emit(this.sortField);
249 public changeService(newService : EntityService<T>): void {
250 this.service = newService;