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";
31 } from "rxjs/operators";
32 import {EntityService} from "@app/model/entity-service";
33 import {FieldToggle} from "@app/model/field-toggle";
34 import {PageQuery} from "../model/page-query";
35 import {LoadingValue} from '../model/loading-value';
36 import {PagedResult} from "@app/model/paged-result";
40 * This component has a search field and pagination section. Entering data in the search field, or
41 * a button click on the pagination triggers a call to a service method, that returns the entity data.
42 * The service must implement the {@link EntityService} interface.
44 * The content is displayed between the search input and the pagination section. To use the data, you should
45 * add an identifier and refer to the item$ variable:
47 * <app-paginated-entities #parent>
49 * <tr ngFor="let entity in parent.item$ | async" >
50 * <td>{{entity.id}}</td>
53 * </app-paginated-entities>
56 * @typeparam T The type of the retrieved entity elements.
59 selector: 'app-paginated-entities',
60 templateUrl: './paginated-entities.component.html',
61 styleUrls: ['./paginated-entities.component.scss']
63 export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, AfterViewInit {
68 * This must be set, if you use the component. This service retrieves the entity data.
70 @Input() service: EntityService<T>;
73 * The number of elements per page retrieved
75 @Input() pageSize = 10;
78 * Two-Way-Binding attribute for sorting field
80 @Input() sortField = [];
82 * Two-Way Binding attribute for sort order
84 @Input() sortOrder = "asc";
89 @Input() pagination = {
97 * If true, all controls are displayed, if the total count is 0
100 displayIfEmpty:boolean=true;
102 * Sets the translation key, for the text to display, if displayIfEmpty=false and the total count is 0.
105 displayKeyIfEmpty:string='form.emptyContent';
108 * If set to true, all controls are displayed, even if there is only one page to display.
109 * Otherwise the controls are not displayed, if there is only a single page of results.
112 displayControlsIfSinglePage:boolean=true;
117 * The current page that is selected
121 * The current search term entered in the search field
126 * Event thrown, if the page value changes
128 @Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
130 * Event thrown, if the search term changes
132 @Output() searchTermChange: EventEmitter<string> = new EventEmitter<string>();
134 @Output() sortFieldChange: EventEmitter<string[]> = new EventEmitter<string[]>();
136 @Output() sortOrderChange: EventEmitter<string> = new EventEmitter<string>();
139 * The total number of elements available for the given search term
141 public total$: Observable<number>;
143 * The entity items retrieved from the service
145 public items$: Observable<LoadingValue<PagedResult<T>>>;
148 * true, if the current page result value represents a result with multiple pages,
151 public multiplePages$:Observable<boolean>;
153 private pageStream: Subject<number> = new Subject<number>();
154 private searchTermStream: Subject<string> = new Subject<string>();
157 // console.log("Construct " + this.id);
160 this.multiplePages$=null;
164 console.log("Pag Init " + this.id);
165 // We combine the sources for the page and the search input field to a observable 'source'
166 const pageSource = this.pageStream.pipe(map(pageNumber => {
167 return new PageQuery(this.searchTerm, pageNumber);
169 const searchSource = this.searchTermStream.pipe(
171 distinctUntilChanged(),
173 this.searchTerm = searchTerm;
174 return new PageQuery(searchTerm, 1)
176 const source = merge(pageSource, searchSource).pipe(
177 startWith(new PageQuery(this.searchTerm, this.page)),
178 switchMap((params: PageQuery) =>
180 of(LoadingValue.start<PagedResult<T>>()),
181 this.service(params.search, (params.page - 1) * this.pageSize, this.pageSize, this.sortField, this.sortOrder)
182 .pipe(map(pagedResult=>LoadingValue.finish<PagedResult<T>>(pagedResult)))
185 // This is to avoid multiple REST calls, without each subscriber would
186 // cause a REST call.
187 multicast(new Subject()),
190 this.total$ = source.pipe(filter(val=>val.hasValue()),map(val=>val.value),
191 pluck('pagination', 'total_count'));
192 this.multiplePages$ = source.pipe(filter(val => val.hasValue()),
193 map(val => val.value.pagination.total_count > val.value.pagination.limit));
194 this.items$ = source;
197 search(terms: string) {
198 // console.log("Keystroke " + terms);
199 this.searchTermChange.emit(terms);
200 this.searchTermStream.next(terms)
203 public changePage(pageNumber: number) {
204 // console.log("Page change " +pageNumber);
205 this.pageChange.emit(pageNumber);
206 this.pageStream.next(pageNumber);
209 private compareArrays(a1: string[], a2: string[]) {
212 if (a1[i] !== a2[i]) return false;
217 toggleSortField(fieldName: string) {
218 this.toggleField([fieldName]);
221 toggleField(fieldArray: string[]) {
222 // console.log("Changing sort field " + fieldArray);
223 let sortOrderChanged: boolean = false;
224 let sortFieldChanged: boolean = false;
225 if (!this.compareArrays(this.sortField, fieldArray)) {
226 // console.log("Fields differ: " + this.sortField + " - " + fieldArray);
227 this.sortField = fieldArray;
228 if (this.sortOrder != 'asc') {
229 this.sortOrder = 'asc';
230 sortOrderChanged = true;
232 sortFieldChanged = true;
234 if (this.sortOrder == "asc") {
235 this.sortOrder = "desc";
237 this.sortOrder = "asc";
239 // console.log("Toggled sort order: " + this.sortOrder);
240 sortOrderChanged = true;
242 if (sortOrderChanged) {
243 //console.log("Sort order changed: "+this.sortOrder)
244 this.sortOrderChange.emit(this.sortOrder);
246 if (sortFieldChanged) {
247 this.sortFieldChange.emit(this.sortField);
249 if (sortFieldChanged || sortOrderChanged) {
251 this.changePage(this.page);
255 ngAfterViewInit(): void {
256 // console.log("Pag afterViewInit " + this.id);
257 // We emit the current value to push them to the containing reading components
258 this.sortOrderChange.emit(this.sortOrder);
259 this.sortFieldChange.emit(this.sortField);
263 public changeService(newService : EntityService<T>): void {
264 this.service = newService;