Skip to content

Commit 10dfedd

Browse files
111731: Created search label loader and created new date label component
1 parent 9d40225 commit 10dfedd

12 files changed

Lines changed: 354 additions & 31 deletions

src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
119119
setAppliedFilter(allFacetValues: FacetValues[]): void {
120120
const appliedFilters: AppliedFilter[] = [].concat(...allFacetValues.map((facetValues: FacetValues) => facetValues.appliedFilters))
121121
.filter((appliedFilter: AppliedFilter) => hasValue(appliedFilter))
122-
.filter((appliedFilter: AppliedFilter) => appliedFilter.filter === this.filterConfig.name);
122+
.filter((appliedFilter: AppliedFilter) => appliedFilter.filter === this.filterConfig.name)
123+
// TODO this should ideally be fixed in the backend
124+
.map((appliedFilter: AppliedFilter) => Object.assign({}, appliedFilter, {
125+
operator: 'range',
126+
}));
123127

124128
this.selectedAppliedFilters$ = observableOf(appliedFilters);
125129
this.changeAppliedFilters.emit(appliedFilters);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Directive, ViewContainerRef } from '@angular/core';
2+
3+
/**
4+
* Directive used as a hook to know where to inject the dynamic loaded component
5+
*/
6+
@Directive({
7+
selector: '[dsSearchLabelLoader]'
8+
})
9+
export class SearchLabelLoaderDirective {
10+
11+
constructor(
12+
public viewContainerRef: ViewContainerRef,
13+
) {
14+
}
15+
16+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<ng-template dsSearchLabelLoader></ng-template>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { Component, ComponentRef, OnChanges, OnDestroy, OnInit, ViewChild, ViewContainerRef, SimpleChanges, Input } from '@angular/core';
2+
import { Subscription } from 'rxjs';
3+
import { GenericConstructor } from 'src/app/core/shared/generic-constructor';
4+
import { hasValue, isNotEmpty } from 'src/app/shared/empty.util';
5+
import { ThemeService } from '../../../theme-support/theme.service';
6+
import { SearchLabelLoaderDirective } from './search-label-loader-directive.directive';
7+
import { getSearchLabelByOperator } from './search-label-loader.decorator';
8+
import { AppliedFilter } from '../../models/applied-filter.model';
9+
10+
@Component({
11+
selector: 'ds-search-label-loader',
12+
templateUrl: './search-label-loader.component.html',
13+
})
14+
export class SearchLabelLoaderComponent implements OnInit, OnChanges, OnDestroy {
15+
16+
@Input() inPlaceSearch: boolean;
17+
18+
@Input() appliedFilter: AppliedFilter;
19+
20+
/**
21+
* Directive to determine where the dynamic child component is located
22+
*/
23+
@ViewChild(SearchLabelLoaderDirective, { static: true }) componentDirective: SearchLabelLoaderDirective;
24+
25+
/**
26+
* The reference to the dynamic component
27+
*/
28+
protected compRef: ComponentRef<Component>;
29+
30+
/**
31+
* Array to track all subscriptions and unsubscribe them onDestroy
32+
*/
33+
protected subs: Subscription[] = [];
34+
35+
/**
36+
* The @Input() that are used to find the matching component using {@link getComponent}. When the value of
37+
* one of these @Input() change this loader needs to retrieve the best matching component again using the
38+
* {@link getComponent} method.
39+
*/
40+
protected inputNamesDependentForComponent: (keyof this & string)[] = [];
41+
42+
/**
43+
* The list of the @Input() names that should be passed down to the dynamically created components.
44+
*/
45+
protected inputNames: (keyof this & string)[] = [
46+
'inPlaceSearch',
47+
'appliedFilter',
48+
];
49+
50+
constructor(
51+
protected themeService: ThemeService,
52+
) {
53+
}
54+
55+
/**
56+
* Set up the dynamic child component
57+
*/
58+
ngOnInit(): void {
59+
this.instantiateComponent();
60+
}
61+
62+
/**
63+
* Whenever the inputs change, update the inputs of the dynamic component
64+
*/
65+
ngOnChanges(changes: SimpleChanges): void {
66+
if (hasValue(this.compRef)) {
67+
if (this.inputNamesDependentForComponent.some((name: keyof this & string) => hasValue(changes[name]) && changes[name].previousValue !== changes[name].currentValue)) {
68+
// Recreate the component when the @Input()s used by getComponent() aren't up-to-date anymore
69+
this.destroyComponentInstance();
70+
this.instantiateComponent();
71+
} else {
72+
this.connectInputsAndOutputs();
73+
}
74+
}
75+
}
76+
77+
ngOnDestroy(): void {
78+
this.subs
79+
.filter((subscription: Subscription) => hasValue(subscription))
80+
.forEach((subscription: Subscription) => subscription.unsubscribe());
81+
this.destroyComponentInstance();
82+
}
83+
84+
/**
85+
* Creates the component and connects the @Input() & @Output() from the ThemedComponent to its child Component.
86+
*/
87+
public instantiateComponent(): void {
88+
const component: GenericConstructor<Component> = this.getComponent();
89+
90+
const viewContainerRef: ViewContainerRef = this.componentDirective.viewContainerRef;
91+
viewContainerRef.clear();
92+
93+
this.compRef = viewContainerRef.createComponent(
94+
component, {
95+
index: 0,
96+
injector: undefined,
97+
},
98+
);
99+
100+
this.connectInputsAndOutputs();
101+
}
102+
103+
/**
104+
* Destroys the themed component and calls it's `ngOnDestroy`
105+
*/
106+
public destroyComponentInstance(): void {
107+
if (hasValue(this.compRef)) {
108+
this.compRef.destroy();
109+
this.compRef = null;
110+
}
111+
}
112+
113+
/**
114+
* Fetch the component depending on the item's entity type, metadata representation type and context
115+
*/
116+
public getComponent(): GenericConstructor<Component> {
117+
return getSearchLabelByOperator(this.appliedFilter.operator);
118+
}
119+
120+
/**
121+
* Connect the inputs and outputs of this component to the dynamic component,
122+
* to ensure they're in sync
123+
*/
124+
public connectInputsAndOutputs(): void {
125+
if (isNotEmpty(this.inputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
126+
this.inputNames.filter((name: string) => this[name] !== undefined).filter((name: string) => this[name] !== this.compRef.instance[name]).forEach((name: string) => {
127+
this.compRef.instance[name] = this[name];
128+
});
129+
}
130+
}
131+
132+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Component } from '@angular/core';
2+
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
3+
4+
export const map: Map<string, GenericConstructor<Component>> = new Map();
5+
6+
export function renderSearchLabelFor(operator: string) {
7+
return function decorator(objectElement: any) {
8+
if (!objectElement) {
9+
return;
10+
}
11+
map.set(operator, objectElement);
12+
};
13+
}
14+
15+
export function getSearchLabelByOperator(operator: string): GenericConstructor<Component> {
16+
return map.get(operator);
17+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<a *ngIf="min !== '*'"
2+
[routerLink]="searchLink"
3+
[queryParams]="(removeParametersMin | async)"
4+
class="badge badge-primary mr-1 mb-1 text-capitalize">
5+
{{('search.filters.applied.f.' + appliedFilter.filter + '.min') | translate}}: {{ min }}
6+
<span> ×</span>
7+
</a>
8+
<a *ngIf="max !== '*'"
9+
[routerLink]="searchLink"
10+
[queryParams]="(removeParametersMax | async)"
11+
class="badge badge-primary mr-1 mb-1 text-capitalize">
12+
{{('search.filters.applied.f.' + appliedFilter.filter + '.max') | translate}}: {{ max }}
13+
<span> ×</span>
14+
</a>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2+
import { TranslateModule } from '@ngx-translate/core';
3+
import { Params, ActivatedRoute } from '@angular/router';
4+
import { SearchLabelRangeComponent } from './search-label-range.component';
5+
import { SearchServiceStub } from '../../../testing/search-service.stub';
6+
import { SearchService } from '../../../../core/shared/search/search.service';
7+
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
8+
import { AppliedFilter } from '../../models/applied-filter.model';
9+
import { addOperatorToFilterValue } from '../../search.utils';
10+
import { RouterTestingModule } from '@angular/router/testing';
11+
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
12+
import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub';
13+
14+
describe('SearchLabelComponent', () => {
15+
let comp: SearchLabelRangeComponent;
16+
let fixture: ComponentFixture<SearchLabelRangeComponent>;
17+
18+
let route: ActivatedRouteStub;
19+
let searchConfigurationService: SearchConfigurationServiceStub;
20+
21+
const searchLink = '/search';
22+
let appliedFilter: AppliedFilter;
23+
let initialRouteParams: Params;
24+
25+
function init(): void {
26+
appliedFilter = Object.assign(new AppliedFilter(), {
27+
filter: 'author',
28+
operator: 'authority',
29+
value: '1282121b-5394-4689-ab93-78d537764052',
30+
label: 'Odinson, Thor',
31+
});
32+
initialRouteParams = {
33+
'query': '',
34+
'spc.page': '1',
35+
'f.author': addOperatorToFilterValue(appliedFilter.value, appliedFilter.operator),
36+
'f.has_content_in_original_bundle': addOperatorToFilterValue('true', 'equals'),
37+
};
38+
}
39+
40+
beforeEach(waitForAsync(async () => {
41+
init();
42+
route = new ActivatedRouteStub(initialRouteParams);
43+
searchConfigurationService = new SearchConfigurationServiceStub();
44+
45+
await TestBed.configureTestingModule({
46+
imports: [
47+
RouterTestingModule,
48+
TranslateModule.forRoot(),
49+
],
50+
declarations: [
51+
SearchLabelRangeComponent,
52+
],
53+
providers: [
54+
{ provide: SearchConfigurationService, useValue: searchConfigurationService },
55+
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
56+
{ provide: ActivatedRoute, useValue: route },
57+
],
58+
}).compileComponents();
59+
}));
60+
61+
beforeEach(() => {
62+
fixture = TestBed.createComponent(SearchLabelRangeComponent);
63+
comp = fixture.componentInstance;
64+
comp.appliedFilter = appliedFilter;
65+
fixture.detectChanges();
66+
});
67+
68+
it('should create', () => {
69+
expect(comp).toBeTruthy();
70+
});
71+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Component, Input, OnInit } from '@angular/core';
2+
import { Observable } from 'rxjs';
3+
import { Params, Router } from '@angular/router';
4+
import { SearchService } from '../../../../core/shared/search/search.service';
5+
import { currentPath } from '../../../utils/route.utils';
6+
import { AppliedFilter } from '../../models/applied-filter.model';
7+
import { renderSearchLabelFor } from '../search-label-loader/search-label-loader.decorator';
8+
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
9+
10+
/**
11+
* Component that represents the label containing the currently active filters
12+
*/
13+
@Component({
14+
selector: 'ds-search-label-range',
15+
templateUrl: './search-label-range.component.html',
16+
})
17+
@renderSearchLabelFor('range')
18+
export class SearchLabelRangeComponent implements OnInit {
19+
20+
@Input() inPlaceSearch: boolean;
21+
22+
@Input() appliedFilter: AppliedFilter;
23+
24+
searchLink: string;
25+
26+
removeParametersMin: Observable<Params>;
27+
28+
removeParametersMax: Observable<Params>;
29+
30+
min: string;
31+
32+
max: string;
33+
34+
constructor(
35+
protected searchConfigurationService: SearchConfigurationService,
36+
protected searchService: SearchService,
37+
protected router: Router,
38+
) {
39+
}
40+
41+
ngOnInit(): void {
42+
this.searchLink = this.getSearchLink();
43+
this.min = this.appliedFilter.value.substring(1, this.appliedFilter.value.indexOf('TO') - 1);
44+
this.max = this.appliedFilter.value.substring(this.appliedFilter.value.indexOf('TO') + 3, this.appliedFilter.value.length - 1);
45+
this.removeParametersMin = this.searchConfigurationService.getParamsWithoutAppliedFilter(`${this.appliedFilter.filter}.min`, this.min);
46+
this.removeParametersMax = this.searchConfigurationService.getParamsWithoutAppliedFilter(`${this.appliedFilter.filter}.max`, this.max);
47+
}
48+
49+
/**
50+
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
51+
*/
52+
private getSearchLink(): string {
53+
if (this.inPlaceSearch) {
54+
return currentPath(this.router);
55+
}
56+
return this.searchService.getSearchLink();
57+
}
58+
59+
}

src/app/shared/search/search-labels/search-label/search-label.component.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ import { SearchService } from '../../../../core/shared/search/search.service';
55
import { currentPath } from '../../../utils/route.utils';
66
import { AppliedFilter } from '../../models/applied-filter.model';
77
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
8+
import { renderSearchLabelFor } from '../search-label-loader/search-label-loader.decorator';
89

10+
/**
11+
* Component that represents the label containing the currently active filters
12+
*/
913
@Component({
1014
selector: 'ds-search-label',
1115
templateUrl: './search-label.component.html',
1216
})
13-
14-
/**
15-
* Component that represents the label containing the currently active filters
16-
*/
17+
@renderSearchLabelFor('equals')
18+
@renderSearchLabelFor('notequals')
19+
@renderSearchLabelFor('authority')
20+
@renderSearchLabelFor('notauthority')
21+
@renderSearchLabelFor('contains')
22+
@renderSearchLabelFor('notcontains')
23+
@renderSearchLabelFor('query')
1724
export class SearchLabelComponent implements OnInit {
1825
@Input() inPlaceSearch: boolean;
1926
@Input() appliedFilter: AppliedFilter;
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<div class="labels">
22
<ng-container *ngFor="let appliedFilters of (appliedFilters | keyvalue)">
3-
<ds-search-label *ngFor="let appliedFilter of appliedFilters.value" [inPlaceSearch]="inPlaceSearch" [appliedFilter]="appliedFilter"></ds-search-label>
3+
<ds-search-label-loader *ngFor="let appliedFilter of appliedFilters.value"
4+
[appliedFilter]="appliedFilter"
5+
[inPlaceSearch]="inPlaceSearch">
6+
</ds-search-label-loader>
47
</ng-container>
58
</div>

0 commit comments

Comments
 (0)