[CST-3088] Created abstract form component to handle vocabularies

This commit is contained in:
Giuseppe Digilio
2020-06-29 22:20:52 +02:00
parent 4cc1a3eecd
commit 3225966600
15 changed files with 649 additions and 250 deletions

View File

@@ -0,0 +1,119 @@
import { EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicFormControlComponent,
DynamicFormLayoutService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
import { map } from 'rxjs/operators';
import { Observable, of as observableOf } from 'rxjs';
import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service';
import { isNotEmpty } from '../../../../empty.util';
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
import { VocabularyEntry } from '../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { DsDynamicInputModel } from './ds-dynamic-input.model';
import { PageInfo } from '../../../../../core/shared/page-info.model';
/**
* An abstract class to be extended by form components that handle vocabulary
*/
export abstract class DsDynamicVocabularyComponent extends DynamicFormControlComponent {
@Input() abstract bindId = true;
@Input() abstract group: FormGroup;
@Input() abstract model: DsDynamicInputModel;
@Output() abstract blur: EventEmitter<any> = new EventEmitter<any>();
@Output() abstract change: EventEmitter<any> = new EventEmitter<any>();
@Output() abstract focus: EventEmitter<any> = new EventEmitter<any>();
public abstract pageInfo: PageInfo;
constructor(protected vocabularyService: VocabularyService,
protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService
) {
super(layoutService, validationService);
}
/**
* Sets the current value with the given value.
* @param value The value to set.
* @param init Representing if is init value or not.
*/
public abstract setCurrentValue(value: any, init?: boolean);
/**
* Retrieves the init form value from model
*/
getInitValueFromModel(): Observable<FormFieldMetadataValueObject> {
let initValue$: Observable<FormFieldMetadataValueObject>;
if (isNotEmpty(this.model.value) && (this.model.value instanceof FormFieldMetadataValueObject)) {
let initEntry$: Observable<VocabularyEntry>;
if (this.model.value.hasAuthority()) {
initEntry$ = this.vocabularyService.getVocabularyEntryByID(this.model.value.authority, this.model.vocabularyOptions)
} else {
initEntry$ = this.vocabularyService.getVocabularyEntryByValue(this.model.value.value, this.model.vocabularyOptions)
}
initValue$ = initEntry$.pipe(map((initEntry: VocabularyEntry) => {
if (isNotEmpty(initEntry)) {
return new FormFieldMetadataValueObject(
initEntry.value,
null,
initEntry.authority,
initEntry.display
);
} else {
return this.model.value as any;
}
}));
} else {
initValue$ = observableOf(new FormFieldMetadataValueObject(this.model.value));
}
return initValue$;
}
/**
* Emits a blur event containing a given value.
* @param event The value to emit.
*/
onBlur(event: Event) {
this.blur.emit(event);
}
/**
* Emits a focus event containing a given value.
* @param event The value to emit.
*/
onFocus(event) {
this.focus.emit(event);
}
/**
* Emits a change event and updates model value.
* @param updateValue
*/
dispatchUpdate(updateValue: any) {
this.model.valueUpdates.next(updateValue);
this.change.emit(updateValue);
}
/**
* Update the page info object
* @param elementsPerPage
* @param currentPage
* @param totalElements
* @param totalPages
*/
protected updatePageInfo(elementsPerPage: number, currentPage: number, totalElements?: number, totalPages?: number) {
this.pageInfo = Object.assign(new PageInfo(), {
elementsPerPage: elementsPerPage,
currentPage: currentPage,
totalElements: totalElements,
totalPages: totalPages
});
}
}

View File

@@ -9,7 +9,6 @@ import {
} from '@ng-dynamic-forms/core';
import { findKey } from 'lodash';
import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model';
import { hasValue, isNotEmpty } from '../../../../../empty.util';
import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model';
import { FormBuilderService } from '../../../form-builder.service';
@@ -18,6 +17,7 @@ import { VocabularyService } from '../../../../../../core/submission/vocabularie
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
import { PaginatedList } from '../../../../../../core/data/paginated-list';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { PageInfo } from '../../../../../../core/shared/page-info.model';
export interface ListItem {
id: string,
@@ -26,12 +26,14 @@ export interface ListItem {
index: number
}
/**
* Component representing a list input field
*/
@Component({
selector: 'ds-dynamic-list',
styleUrls: ['./dynamic-list.component.scss'],
templateUrl: './dynamic-list.component.html'
})
export class DsDynamicListComponent extends DynamicFormControlComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@@ -43,7 +45,6 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
public items: ListItem[][] = [];
protected optionsList: VocabularyEntry[];
protected searchOptions: VocabularyFindOptions;
constructor(private vocabularyService: VocabularyService,
private cdr: ChangeDetectorRef,
@@ -56,14 +57,6 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
ngOnInit() {
if (this.hasAuthorityOptions()) {
// TODO Replace max elements 1000 with a paginated request when pagination bug is resolved
this.searchOptions = new VocabularyFindOptions(
this.model.vocabularyOptions.scope,
this.model.vocabularyOptions.name,
this.model.vocabularyOptions.metadata,
'',
1000, // Max elements
1);// Current Page
this.setOptionsFromAuthority();
}
}
@@ -99,7 +92,10 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
protected setOptionsFromAuthority() {
if (this.model.vocabularyOptions.name && this.model.vocabularyOptions.name.length > 0) {
const listGroup = this.group.controls[this.model.id] as FormGroup;
this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe(
const pageInfo: PageInfo = new PageInfo({
elementsPerPage: Number.MAX_VALUE, currentPage: 1
} as PageInfo);
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, pageInfo).pipe(
getFirstSucceededRemoteDataPayload()
).subscribe((entries: PaginatedList<VocabularyEntry>) => {
let groupCounter = 0;

View File

@@ -21,8 +21,8 @@
[placeholder]="model.placeholder | translate"
[readonly]="model.readOnly"
(change)="onChange($event)"
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
(blur)="onBlur($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocus($event); $event.stopPropagation(); sdRef.close();"
(click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();">
</div>
@@ -40,8 +40,8 @@
[placeholder]="model.secondPlaceholder | translate"
[readonly]="model.readOnly"
(change)="onChange($event)"
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
(blur)="onBlur($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocus($event); $event.stopPropagation(); sdRef.close();"
(click)="$event.stopPropagation(); sdRef.close();">
</div>
<div class="col-auto text-center">

View File

@@ -4,6 +4,7 @@ import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angul
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { of as observableOf } from 'rxjs';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
@@ -309,7 +310,13 @@ describe('Dynamic Lookup component', () => {
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
lookupComp.model.value = new FormFieldMetadataValueObject('test', null, 'test001');
const entry = observableOf(Object.assign(new VocabularyEntry(), {
authority: null,
value: 'test',
display: 'testDisplay'
}));
spyOn((lookupComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry);
(lookupComp.model as any).value = new FormFieldMetadataValueObject('test', null, null, 'testDisplay');
lookupFixture.detectChanges();
// spyOn(store, 'dispatch');
@@ -318,9 +325,52 @@ describe('Dynamic Lookup component', () => {
lookupFixture.destroy();
lookupComp = null;
});
it('should init component properly', () => {
expect(lookupComp.firstInputValue).toBe('test');
it('should init component properly', fakeAsync(() => {
tick();
expect(lookupComp.firstInputValue).toBe('testDisplay');
expect((lookupComp as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled();
}));
it('should have search button disabled on edit mode', () => {
lookupComp.editMode = true;
lookupFixture.detectChanges();
const de = lookupFixture.debugElement.queryAll(By.css('button'));
const searchBtnEl = de[0].nativeElement;
const saveBtnEl = de[1].nativeElement;
expect(searchBtnEl.disabled).toBe(true);
expect(saveBtnEl.disabled).toBe(false);
expect(saveBtnEl.textContent.trim()).toBe('form.save');
});
});
describe('and init model value is not empty with authority', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
const entry = observableOf(Object.assign(new VocabularyEntry(), {
authority: 'test001',
value: 'test',
display: 'testDisplay'
}));
spyOn((lookupComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry);
lookupComp.model.value = new FormFieldMetadataValueObject('test', null, 'test001', 'testDisplay');
lookupFixture.detectChanges();
// spyOn(store, 'dispatch');
});
afterEach(() => {
lookupFixture.destroy();
lookupComp = null;
});
it('should init component properly', fakeAsync(() => {
tick();
expect(lookupComp.firstInputValue).toBe('testDisplay');
expect((lookupComp as any).vocabularyService.getVocabularyEntryByID).toHaveBeenCalled();
}));
it('should have search button disabled on edit mode', () => {
lookupComp.editMode = true;
@@ -430,6 +480,13 @@ describe('Dynamic Lookup component', () => {
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG);
lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001');
const entry = observableOf(Object.assign(new VocabularyEntry(), {
authority: null,
value: 'Name, Lastname',
display: 'Name, Lastname'
}));
spyOn((lookupComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry);
(lookupComp.model as any).value = new FormFieldMetadataValueObject('Name, Lastname', null, null, 'Name, Lastname');
lookupFixture.detectChanges();
});
@@ -437,10 +494,55 @@ describe('Dynamic Lookup component', () => {
lookupFixture.destroy();
lookupComp = null;
});
it('should init component properly', () => {
it('should init component properly', fakeAsync(() => {
tick();
expect(lookupComp.firstInputValue).toBe('Name');
expect(lookupComp.secondInputValue).toBe('Lastname');
expect((lookupComp as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled();
}));
it('should have search button disabled on edit mode', () => {
lookupComp.editMode = true;
lookupFixture.detectChanges();
const de = lookupFixture.debugElement.queryAll(By.css('button'));
const searchBtnEl = de[0].nativeElement;
const saveBtnEl = de[1].nativeElement;
expect(searchBtnEl.disabled).toBe(true);
expect(saveBtnEl.disabled).toBe(false);
expect(saveBtnEl.textContent.trim()).toBe('form.save');
});
});
describe('and init model value is not empty with authority', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG);
lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001');
const entry = observableOf(Object.assign(new VocabularyEntry(), {
authority: 'test001',
value: 'Name, Lastname',
display: 'Name, Lastname'
}));
spyOn((lookupComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry);
lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001', 'Name, Lastname');
lookupFixture.detectChanges();
});
afterEach(() => {
lookupFixture.destroy();
lookupComp = null;
});
it('should init component properly', fakeAsync(() => {
tick();
expect(lookupComp.firstInputValue).toBe('Name');
expect(lookupComp.secondInputValue).toBe('Lastname');
expect((lookupComp as any).vocabularyService.getVocabularyEntryByID).toHaveBeenCalled();
}));
it('should have search button disabled on edit mode', () => {
lookupComp.editMode = true;

View File

@@ -4,15 +4,10 @@ import { FormGroup } from '@angular/forms';
import { of as observableOf, Subscription } from 'rxjs';
import { catchError, distinctUntilChanged } from 'rxjs/operators';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import {
DynamicFormControlComponent,
DynamicFormLayoutService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model';
import { hasValue, isEmpty, isNotEmpty, isNull, isUndefined } from '../../../../../empty.util';
import { hasValue, isEmpty, isNotEmpty } from '../../../../../empty.util';
import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
@@ -20,16 +15,21 @@ import { DynamicLookupNameModel } from './dynamic-lookup-name.model';
import { ConfidenceType } from '../../../../../../core/shared/confidence-type';
import { PaginatedList } from '../../../../../../core/data/paginated-list';
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
import { DynamicLookupModel } from './dynamic-lookup.model';
/**
* Component representing a lookup or lookup-name input field
*/
@Component({
selector: 'ds-dynamic-lookup',
styleUrls: ['./dynamic-lookup.component.scss'],
templateUrl: './dynamic-lookup.component.html'
})
export class DsDynamicLookupComponent extends DynamicFormControlComponent implements OnDestroy, OnInit {
export class DsDynamicLookupComponent extends DsDynamicVocabularyComponent implements OnDestroy, OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: any;
@Input() model: DynamicLookupModel | DynamicLookupNameModel;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@@ -42,89 +42,97 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
public pageInfo: PageInfo;
public optionsList: any;
protected searchOptions: VocabularyFindOptions;
protected subs: Subscription[] = [];
constructor(private vocabularyService: VocabularyService,
constructor(protected vocabularyService: VocabularyService,
private cdr: ChangeDetectorRef,
protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService
) {
super(layoutService, validationService);
super(vocabularyService, layoutService, validationService);
}
/**
* Converts an item from the result list to a `string` to display in the `<input>` field.
*/
inputFormatter = (x: { display: string }, y: number) => {
return y === 1 ? this.firstInputValue : this.secondInputValue;
};
/**
* Initialize the component, setting up the init form value
*/
ngOnInit() {
this.searchOptions = new VocabularyFindOptions(
this.model.vocabularyOptions.scope,
this.model.vocabularyOptions.name,
this.model.vocabularyOptions.metadata,
'',
this.model.maxOptions,
1);
this.setInputsValue(this.model.value);
if (isNotEmpty(this.model.value)) {
this.setCurrentValue(this.model.value, true);
}
this.subs.push(this.model.valueUpdates
.subscribe((value) => {
if (isEmpty(value)) {
this.resetFields();
} else if (!this.editMode) {
this.setInputsValue(this.model.value);
this.setCurrentValue(this.model.value);
}
}));
}
public formatItemForInput(item: any, field: number): string {
if (isUndefined(item) || isNull(item)) {
return '';
}
return (typeof item === 'string') ? item : this.inputFormatter(item, field);
}
/**
* Check if model value has an authority
*/
public hasAuthorityValue() {
return hasValue(this.model.value)
&& this.model.value.hasAuthority();
}
/**
* Check if current value has an authority
*/
public hasEmptyValue() {
return isNotEmpty(this.getCurrentValue());
}
/**
* Clear inputs whether there is no results and authority is closed
*/
public clearFields() {
// Clear inputs whether there is no results and authority is closed
if (this.model.vocabularyOptions.closed) {
this.resetFields();
}
}
/**
* Check if edit button is disabled
*/
public isEditDisabled() {
return !this.hasAuthorityValue();
}
/**
* Check if input is disabled
*/
public isInputDisabled() {
return (this.model.vocabularyOptions.closed && this.hasAuthorityValue() && !this.editMode);
}
/**
* Check if model is instanceof DynamicLookupNameModel
*/
public isLookupName() {
return (this.model instanceof DynamicLookupNameModel);
}
/**
* Check if search button is disabled
*/
public isSearchDisabled() {
return isEmpty(this.firstInputValue) || this.editMode;
}
public onBlurEvent(event: Event) {
this.blur.emit(event);
}
public onFocusEvent(event) {
this.focus.emit(event);
}
/**
* Update model value with the typed text if vocabulary is not closed
* @param event the typed text
*/
public onChange(event) {
event.preventDefault();
if (!this.model.vocabularyOptions.closed) {
@@ -139,31 +147,51 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
}
}
/**
* Load more result entries
*/
public onScroll() {
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
this.searchOptions.currentPage++;
this.updatePageInfo(
this.pageInfo.elementsPerPage,
this.pageInfo.currentPage + 1,
this.pageInfo.totalElements,
this.pageInfo.totalPages
);
this.search();
}
}
/**
* Update model value with selected entry
* @param event the selected entry
*/
public onSelect(event) {
this.updateModel(event);
}
/**
* Reset the current value when dropdown toggle
*/
public openChange(isOpened: boolean) {
if (!isOpened) {
if (this.model.vocabularyOptions.closed && !this.hasAuthorityValue()) {
this.setInputsValue('');
this.setCurrentValue('');
}
}
}
/**
* Reset the model value
*/
public remove() {
this.group.markAsPristine();
this.model.valueUpdates.next(null);
this.change.emit(null);
this.dispatchUpdate(null)
}
/**
* Saves all changes
*/
public saveChanges() {
if (isNotEmpty(this.getCurrentValue())) {
const newValue = Object.assign(new VocabularyEntry(), this.model.value, {
@@ -177,15 +205,21 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
this.switchEditMode();
}
/**
* Converts a stream of text values from the `<input>` element to the stream of the array of items
* to display in the result list.
*/
public search() {
this.optionsList = null;
this.pageInfo = null;
// Query
this.searchOptions.query = this.getCurrentValue();
this.updatePageInfo(this.model.maxOptions, 1);
this.loading = true;
this.subs.push(this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe(
this.subs.push(this.vocabularyService.getVocabularyEntriesByValue(
this.getCurrentValue(),
false,
this.model.vocabularyOptions,
this.pageInfo
).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() =>
observableOf(new PaginatedList(
@@ -195,18 +229,28 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
),
distinctUntilChanged())
.subscribe((list: PaginatedList<VocabularyEntry>) => {
console.log(list);
this.optionsList = list.page;
this.pageInfo = list.pageInfo;
this.updatePageInfo(
list.pageInfo.elementsPerPage,
list.pageInfo.currentPage,
list.pageInfo.totalElements,
list.pageInfo.totalPages
);
this.loading = false;
this.cdr.detectChanges();
}));
}
/**
* Changes the edit mode flag
*/
public switchEditMode() {
this.editMode = !this.editMode;
}
/**
* Callback functions for whenClickOnConfidenceNotAccepted event
*/
public whenClickOnConfidenceNotAccepted(sdRef: NgbDropdown, confidence: ConfidenceType) {
if (!this.model.readOnly) {
sdRef.open();
@@ -220,6 +264,38 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
.forEach((sub) => sub.unsubscribe());
}
/**
* Sets the current value with the given value.
* @param value The value to set.
* @param init Representing if is init value or not.
*/
public setCurrentValue(value: any, init = false) {
if (init) {
this.getInitValueFromModel()
.subscribe((value: FormFieldMetadataValueObject) => this.setDisplayInputValue(value.display));
} else if (hasValue(value)) {
if (value instanceof FormFieldMetadataValueObject || value instanceof VocabularyEntry) {
this.setDisplayInputValue(value.display);
}
}
}
protected setDisplayInputValue(displayValue: string) {
if (hasValue(displayValue)) {
if (this.isLookupName()) {
const values = displayValue.split((this.model as DynamicLookupNameModel).separator);
this.firstInputValue = (values[0] || '').trim();
this.secondInputValue = (values[1] || '').trim();
} else {
this.firstInputValue = displayValue || '';
}
}
}
/**
* Gets the current text present in the input field(s)
*/
protected getCurrentValue(): string {
let result = '';
if (!this.isLookupName()) {
@@ -237,6 +313,9 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
return result;
}
/**
* Clear text present in the input field(s)
*/
protected resetFields() {
this.firstInputValue = '';
if (this.isLookupName()) {
@@ -244,32 +323,12 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
}
}
protected setInputsValue(value) {
if (hasValue(value)) {
let displayValue = value;
if (value instanceof FormFieldMetadataValueObject || value instanceof VocabularyEntry) {
displayValue = value.display;
}
if (hasValue(displayValue)) {
if (this.isLookupName()) {
const values = displayValue.split((this.model as DynamicLookupNameModel).separator);
this.firstInputValue = (values[0] || '').trim();
this.secondInputValue = (values[1] || '').trim();
} else {
this.firstInputValue = displayValue || '';
}
}
}
}
protected updateModel(value) {
this.group.markAsDirty();
this.model.valueUpdates.next(value);
this.setInputsValue(value);
this.change.emit(value);
this.dispatchUpdate(value);
this.setCurrentValue(value);
this.optionsList = null;
this.pageInfo = null;
}
}

View File

@@ -23,13 +23,15 @@ import { hasValue, isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.u
import { shrinkInOut } from '../../../../../animations/shrink';
import { ChipsItem } from '../../../../../chips/models/chips-item.model';
import { hasOnlyEmptyProperties } from '../../../../../object.util';
import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { environment } from '../../../../../../../environments/environment';
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
/**
* Component representing a group input field
*/
@Component({
selector: 'ds-dynamic-relation-group',
styleUrls: ['./dynamic-relation-group.component.scss'],

View File

@@ -1,5 +1,5 @@
<div #sdRef="ngbDropdown" ngbDropdown class="input-group w-100">
<input class="form-control"
<div #sdRef="ngbDropdown" ngbDropdown class="w-100">
<input ngbDropdownToggle class="form-control custom-select"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
@@ -11,12 +11,6 @@
(click)="$event.stopPropagation(); openDropdown(sdRef);"
(focus)="onFocus($event)"
(keypress)="$event.preventDefault()">
<button aria-describedby="collectionControlsMenuLabel"
class="ds-form-input-btn btn btn-outline-primary"
id="scrollableDropdownMenuButton_{{model.id}}"
ngbDropdownToggle
[disabled]="model.readOnly"
(click)="onToggle(sdRef); $event.stopPropagation();"></button>
<div ngbDropdownMenu
class="dropdown-menu scrollable-dropdown-menu w-100"

View File

@@ -126,7 +126,7 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => {
});
it('should display dropdown menu entries', () => {
const de = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn'));
const de = scrollableDropdownFixture.debugElement.query(By.css('input.custom-select'));
const btnEl = de.nativeElement;
const deMenu = scrollableDropdownFixture.debugElement.query(By.css('div.scrollable-dropdown-menu'));
@@ -153,7 +153,7 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => {
it('should select a results entry properly', fakeAsync(() => {
const selectedValue = Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 });
let de: any = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn'));
let de: any = scrollableDropdownFixture.debugElement.query(By.css('input.custom-select'));
let btnEl = de.nativeElement;
btnEl.click();

View File

@@ -2,29 +2,29 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } fro
import { FormGroup } from '@angular/forms';
import { Observable, of as observableOf } from 'rxjs';
import { catchError, distinctUntilChanged, tap } from 'rxjs/operators';
import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import {
DynamicFormControlComponent,
DynamicFormLayoutService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model';
import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { isNull, isUndefined } from '../../../../../empty.util';
import { isEmpty } from '../../../../../empty.util';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model';
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
import { PaginatedList } from '../../../../../../core/data/paginated-list';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
/**
* Component representing a dropdown input field
*/
@Component({
selector: 'ds-dynamic-scrollable-dropdown',
styleUrls: ['./dynamic-scrollable-dropdown.component.scss'],
templateUrl: './dynamic-scrollable-dropdown.component.html'
})
export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComponent implements OnInit {
export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicScrollableDropdownModel;
@@ -38,25 +38,20 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp
public pageInfo: PageInfo;
public optionsList: any;
protected searchOptions: VocabularyFindOptions;
constructor(private vocabularyService: VocabularyService,
private cdr: ChangeDetectorRef,
constructor(protected vocabularyService: VocabularyService,
protected cdr: ChangeDetectorRef,
protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService
) {
super(layoutService, validationService);
super(vocabularyService, layoutService, validationService);
}
/**
* Initialize the component, setting up the init form value
*/
ngOnInit() {
this.searchOptions = new VocabularyFindOptions(
this.model.vocabularyOptions.scope,
this.model.vocabularyOptions.name,
this.model.vocabularyOptions.metadata,
'',
this.model.maxOptions,
1);
this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe(
this.updatePageInfo(this.model.maxOptions, 1)
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() => observableOf(new PaginatedList(
new PageInfo(),
@@ -66,9 +61,15 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp
.subscribe((list: PaginatedList<VocabularyEntry>) => {
this.optionsList = list.page;
if (this.model.value) {
this.setCurrentValue(this.model.value);
this.setCurrentValue(this.model.value, true);
}
this.pageInfo = list.pageInfo;
this.updatePageInfo(
list.pageInfo.elementsPerPage,
list.pageInfo.currentPage,
list.pageInfo.totalElements,
list.pageInfo.totalPages
);
this.cdr.detectChanges();
});
@@ -76,22 +77,37 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp
.subscribe((value) => {
this.setCurrentValue(value);
});
}
/**
* Converts an item from the result list to a `string` to display in the `<input>` field.
*/
inputFormatter = (x: VocabularyEntry): string => x.display || x.value;
/**
* Opens dropdown menu
* @param sdRef The reference of the NgbDropdown.
*/
openDropdown(sdRef: NgbDropdown) {
if (!this.model.readOnly) {
this.group.markAsUntouched();
sdRef.open();
}
}
/**
* Loads any new entries
*/
onScroll() {
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
this.loading = true;
this.searchOptions.currentPage++;
this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe(
this.updatePageInfo(
this.pageInfo.elementsPerPage,
this.pageInfo.currentPage + 1,
this.pageInfo.totalElements,
this.pageInfo.totalPages
);
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() => observableOf(new PaginatedList(
new PageInfo(),
@@ -101,49 +117,50 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp
tap(() => this.loading = false))
.subscribe((list: PaginatedList<VocabularyEntry>) => {
this.optionsList = this.optionsList.concat(list.page);
this.pageInfo = list.pageInfo;
this.updatePageInfo(
list.pageInfo.elementsPerPage,
list.pageInfo.currentPage,
list.pageInfo.totalElements,
list.pageInfo.totalPages
);
this.cdr.detectChanges();
})
}
}
onBlur(event: Event) {
this.blur.emit(event);
}
onFocus(event) {
this.focus.emit(event);
}
/**
* Emits a change event and set the current value with the given value.
* @param event The value to emit.
*/
onSelect(event) {
this.group.markAsDirty();
this.model.valueUpdates.next(event);
this.change.emit(event);
this.dispatchUpdate(event);
this.setCurrentValue(event);
}
onToggle(sdRef: NgbDropdown) {
if (sdRef.isOpen()) {
this.focus.emit(event);
} else {
this.blur.emit(event);
}
}
/**
* Sets the current value with the given value.
* @param value The value to set.
* @param init Representing if is init value or not.
*/
setCurrentValue(value: any, init = false): void {
let result: Observable<string>;
setCurrentValue(value): void {
let result: string;
if (isUndefined(value) || isNull(value)) {
result = '';
} else if (typeof value === 'string') {
result = value;
if (init) {
result = this.getInitValueFromModel().pipe(
map((value: FormFieldMetadataValueObject) => value.display)
);
} else {
for (const item of this.optionsList) {
if (value.value === (item as any).value) {
result = this.inputFormatter(item);
break;
}
if (isEmpty(value)) {
result = observableOf('');
} else if (typeof value === 'string') {
result = observableOf(value);
} else {
result = observableOf(value.display)
}
}
this.currentValue = observableOf(result);
this.currentValue = result;
}
}

View File

@@ -142,15 +142,13 @@ describe('DsDynamicTagComponent test suite', () => {
it('should init component properly', () => {
chips = new Chips([], 'display');
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
expect(tagComp.searchOptions).toBeDefined();
});
it('should search when 3+ characters typed', fakeAsync(() => {
spyOn((tagComp as any).vocabularyService, 'getVocabularyEntries').and.callThrough();
spyOn((tagComp as any).vocabularyService, 'getVocabularyEntriesByValue').and.callThrough();
tagComp.search(observableOf('test')).subscribe(() => {
expect((tagComp as any).vocabularyService.getVocabularyEntries).toHaveBeenCalled();
expect((tagComp as any).vocabularyService.getVocabularyEntriesByValue).toHaveBeenCalled();
});
}));
@@ -232,7 +230,6 @@ describe('DsDynamicTagComponent test suite', () => {
it('should init component properly', () => {
chips = new Chips(modelValue, 'display');
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
expect(tagComp.searchOptions).toBeDefined();
});
});
@@ -259,7 +256,6 @@ describe('DsDynamicTagComponent test suite', () => {
it('should init component properly', () => {
chips = new Chips([], 'display');
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
expect(tagComp.searchOptions).not.toBeDefined();
});
it('should add an item on ENTER or key press is \',\' or \';\'', fakeAsync(() => {

View File

@@ -1,11 +1,7 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicFormControlComponent,
DynamicFormLayoutService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { Observable, of as observableOf } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, merge, switchMap, tap } from 'rxjs/operators';
import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
@@ -13,7 +9,6 @@ import { isEqual } from 'lodash';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { DynamicTagModel } from './dynamic-tag.model';
import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model';
import { Chips } from '../../../../../chips/models/chips.model';
import { hasValue, isNotEmpty } from '../../../../../empty.util';
import { environment } from '../../../../../../../environments/environment';
@@ -21,13 +16,18 @@ import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/share
import { PaginatedList } from '../../../../../../core/data/paginated-list';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
/**
* Component representing a tag input field
*/
@Component({
selector: 'ds-dynamic-tag',
styleUrls: ['./dynamic-tag.component.scss'],
templateUrl: './dynamic-tag.component.html'
})
export class DsDynamicTagComponent extends DynamicFormControlComponent implements OnInit {
export class DsDynamicTagComponent extends DsDynamicVocabularyComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicTagModel;
@@ -42,21 +42,28 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
hasAuthority: boolean;
searching = false;
searchOptions: VocabularyFindOptions;
searchFailed = false;
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false));
currentValue: any;
public pageInfo: PageInfo;
constructor(private vocabularyService: VocabularyService,
constructor(protected vocabularyService: VocabularyService,
private cdr: ChangeDetectorRef,
protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService
) {
super(layoutService, validationService);
super(vocabularyService, layoutService, validationService);
}
/**
* Converts an item from the result list to a `string` to display in the `<input>` field.
*/
formatter = (x: { display: string }) => x.display;
/**
* Converts a stream of text values from the `<input>` element to the stream of the array of items
* to display in the typeahead popup.
*/
search = (text$: Observable<string>) =>
text$.pipe(
debounceTime(300),
@@ -66,8 +73,7 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
if (term === '' || term.length < this.model.minChars) {
return observableOf({ list: [] });
} else {
this.searchOptions.query = term;
return this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe(
return this.vocabularyService.getVocabularyEntriesByValue(term, false, this.model.vocabularyOptions, new PageInfo()).pipe(
getFirstSucceededRemoteDataPayload(),
tap(() => this.searchFailed = false),
catchError(() => {
@@ -83,16 +89,12 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
tap(() => this.changeSearchingStatus(false)),
merge(this.hideSearchingWhenUnsubscribed));
/**
* Initialize the component, setting up the init form value
*/
ngOnInit() {
this.hasAuthority = this.model.vocabularyOptions && hasValue(this.model.vocabularyOptions.name);
if (this.hasAuthority) {
this.searchOptions = new VocabularyFindOptions(
this.model.vocabularyOptions.scope,
this.model.vocabularyOptions.name,
this.model.vocabularyOptions.metadata);
}
this.chips = new Chips(
this.model.value,
'display',
@@ -104,17 +106,24 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
const items = this.chips.getChipsItems();
// Does not emit change if model value is equal to the current value
if (!isEqual(items, this.model.value)) {
this.model.valueUpdates.next(items);
this.change.emit(event);
this.dispatchUpdate(items);
}
});
}
/**
* Changes the searching status
* @param status
*/
changeSearchingStatus(status: boolean) {
this.searching = status;
this.cdr.detectChanges();
}
/**
* Mark form group as dirty on input
* @param event
*/
onInput(event) {
if (event.data) {
this.group.markAsDirty();
@@ -122,6 +131,10 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
this.cdr.detectChanges();
}
/**
* Emits a blur event containing a given value and add all tags to chips.
* @param event The value to emit.
*/
onBlur(event: Event) {
if (isNotEmpty(this.currentValue) && !this.instance.isPopupOpen()) {
this.addTagsToChips();
@@ -129,10 +142,10 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
this.blur.emit(event);
}
onFocus(event) {
this.focus.emit(event);
}
/**
* Updates model value with the selected value and add a new tag to chips.
* @param event The value to set.
*/
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
this.chips.add(event.item);
// this.group.controls[this.model.id].setValue(this.model.value);
@@ -140,25 +153,34 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
setTimeout(() => {
// Reset the input text after x ms, mandatory or the formatter overwrite it
this.currentValue = null;
this.setCurrentValue(null);
this.cdr.detectChanges();
}, 50);
}
updateModel(event) {
this.model.valueUpdates.next(this.chips.getChipsItems());
this.change.emit(event);
/* this.model.valueUpdates.next(this.chips.getChipsItems());
this.change.emit(event);*/
this.dispatchUpdate(this.chips.getChipsItems());
}
/**
* Add a new tag with typed text when typing 'Enter' or ',' or ';'
* @param event the keyUp event
*/
onKeyUp(event) {
if (event.keyCode === 13 || event.keyCode === 188) {
event.preventDefault();
// Key: Enter or ',' or ';'
// Key: 'Enter' or ',' or ';'
this.addTagsToChips();
event.stopPropagation();
}
}
/**
* Prevent propagation of a key event in case of return key is pressed
* @param event the key event
*/
preventEventsPropagation(event) {
event.stopPropagation();
if (event.keyCode === 13) {
@@ -166,6 +188,15 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
}
}
/**
* Sets the current value with the given value.
* @param value The value to set.
* @param init Representing if is init value or not.
*/
public setCurrentValue(value: any, init = false) {
this.currentValue = value;
}
private addTagsToChips() {
if (hasValue(this.currentValue) && (!this.hasAuthority || !this.model.vocabularyOptions.closed)) {
let res: string[] = [];
@@ -188,7 +219,7 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
// this.currentValue = '';
setTimeout(() => {
// Reset the input text after x ms, mandatory or the formatter overwrite it
this.currentValue = null;
this.setCurrentValue(null);
this.cdr.detectChanges();
}, 50);
this.updateModel(event);

View File

@@ -19,6 +19,7 @@ import { FormFieldMetadataValueObject } from '../../../models/form-field-metadat
import { createTestComponent } from '../../../../../testing/utils.test';
import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive';
import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
export let TYPEAHEAD_TEST_GROUP;
@@ -134,14 +135,14 @@ describe('DsDynamicTypeaheadComponent test suite', () => {
it('should search when 3+ characters typed', fakeAsync(() => {
spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntries').and.callThrough();
spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntriesByValue').and.callThrough();
typeaheadComp.search(observableOf('test')).subscribe();
tick(300);
typeaheadFixture.detectChanges();
expect((typeaheadComp as any).vocabularyService.getVocabularyEntries).toHaveBeenCalled();
expect((typeaheadComp as any).vocabularyService.getVocabularyEntriesByValue).toHaveBeenCalled();
}));
it('should set model.value on input type when VocabularyOptions.closed is false', () => {
@@ -229,13 +230,20 @@ describe('DsDynamicTypeaheadComponent test suite', () => {
});
describe('and init model value is not empty', () => {
describe('when init model value is not empty', () => {
beforeEach(() => {
typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent);
typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance
typeaheadComp.group = TYPEAHEAD_TEST_GROUP;
typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
(typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, 'test001');
const entry = observableOf(Object.assign(new VocabularyEntry(), {
authority: null,
value: 'test',
display: 'testDisplay'
}));
spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry);
spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry);
(typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, null, 'testDisplay');
typeaheadFixture.detectChanges();
});
@@ -244,9 +252,11 @@ describe('DsDynamicTypeaheadComponent test suite', () => {
typeaheadComp = null;
});
it('should init component properly', () => {
expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, 'test001'));
});
it('should init component properly', fakeAsync(() => {
tick();
expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, null, 'testDisplay'));
expect((typeaheadComp as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled();
}));
it('should emit change Event onChange and currentValue is empty', () => {
typeaheadComp.currentValue = null;
@@ -257,6 +267,42 @@ describe('DsDynamicTypeaheadComponent test suite', () => {
});
});
describe('when init model value is not empty and has authority', () => {
beforeEach(() => {
typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent);
typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance
typeaheadComp.group = TYPEAHEAD_TEST_GROUP;
typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
const entry = observableOf(Object.assign(new VocabularyEntry(), {
authority: 'test001',
value: 'test001',
display: 'test'
}));
spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry);
spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry);
(typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, 'test001');
typeaheadFixture.detectChanges();
});
afterEach(() => {
typeaheadFixture.destroy();
typeaheadComp = null;
});
it('should init component properly', fakeAsync(() => {
tick();
expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test001', null, 'test001', 'test'));
expect((typeaheadComp as any).vocabularyService.getVocabularyEntryByID).toHaveBeenCalled();
}));
it('should emit change Event onChange and currentValue is empty', () => {
typeaheadComp.currentValue = null;
spyOn(typeaheadComp.change, 'emit');
typeaheadComp.onChange(new Event('change'));
expect(typeaheadComp.change.emit).toHaveBeenCalled();
expect(typeaheadComp.model.value).toBeNull();
});
});
});
});

View File

@@ -1,18 +1,13 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicFormControlComponent,
DynamicFormLayoutService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { catchError, debounceTime, distinctUntilChanged, filter, map, merge, switchMap, tap } from 'rxjs/operators';
import { Observable, of as observableOf, Subject } from 'rxjs';
import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { DynamicTypeaheadModel } from './dynamic-typeahead.model';
import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model';
import { isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.util';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { ConfidenceType } from '../../../../../../core/shared/confidence-type';
@@ -20,13 +15,14 @@ import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/share
import { PaginatedList } from '../../../../../../core/data/paginated-list';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
@Component({
selector: 'ds-dynamic-typeahead',
styleUrls: ['./dynamic-typeahead.component.scss'],
templateUrl: './dynamic-typeahead.component.html'
})
export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent implements OnInit {
export class DsDynamicTypeaheadComponent extends DsDynamicVocabularyComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicTypeaheadModel;
@@ -37,26 +33,33 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
@ViewChild('instance', { static: false }) instance: NgbTypeahead;
pageInfo: PageInfo;
searching = false;
searchOptions: VocabularyFindOptions;
searchFailed = false;
hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.changeSearchingStatus(false));
click$ = new Subject<string>();
currentValue: any;
inputValue: any;
constructor(private vocabularyService: VocabularyService,
constructor(protected vocabularyService: VocabularyService,
private cdr: ChangeDetectorRef,
protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService
) {
super(layoutService, validationService);
super(vocabularyService, layoutService, validationService);
}
/**
* Converts an item from the result list to a `string` to display in the `<input>` field.
*/
formatter = (x: { display: string }) => {
return (typeof x === 'object') ? x.display : x
};
/**
* Converts a stream of text values from the `<input>` element to the stream of the array of items
* to display in the typeahead popup.
*/
search = (text$: Observable<string>) => {
return text$.pipe(
merge(this.click$),
@@ -67,8 +70,11 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
if (term === '' || term.length < this.model.minChars) {
return observableOf({ list: [] });
} else {
this.searchOptions.query = term;
return this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe(
return this.vocabularyService.getVocabularyEntriesByValue(
term,
false,
this.model.vocabularyOptions,
this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
tap(() => this.searchFailed = false),
catchError(() => {
@@ -86,36 +92,49 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
)
};
/**
* Initialize the component, setting up the init form value
*/
ngOnInit() {
this.currentValue = this.model.value;
this.searchOptions = new VocabularyFindOptions(
this.model.vocabularyOptions.scope,
this.model.vocabularyOptions.name,
this.model.vocabularyOptions.metadata);
if (this.model.value) {
this.setCurrentValue(this.model.value, true);
}
this.group.get(this.model.id).valueChanges.pipe(
filter((value) => this.currentValue !== value))
.subscribe((value) => {
this.currentValue = value;
this.setCurrentValue(this.model.value);
});
}
/**
* Changes the searching status
* @param status
*/
changeSearchingStatus(status: boolean) {
this.searching = status;
this.cdr.detectChanges();
}
/**
* Update the input value with a FormFieldMetadataValueObject
* @param event
*/
onInput(event) {
if (!this.model.vocabularyOptions.closed && isNotEmpty(event.target.value)) {
this.inputValue = new FormFieldMetadataValueObject(event.target.value);
}
}
/**
* Emits a blur event containing a given value.
* @param event The value to emit.
*/
onBlur(event: Event) {
if (!this.instance.isPopupOpen()) {
if (!this.model.vocabularyOptions.closed && isNotEmpty(this.inputValue)) {
if (isNotNull(this.inputValue) && this.model.value !== this.inputValue) {
this.model.valueUpdates.next(this.inputValue);
this.change.emit(this.inputValue);
this.dispatchUpdate(this.inputValue);
}
this.inputValue = null;
}
@@ -129,29 +148,60 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp
}
}
/**
* Updates model value with the current value
* @param event The change event.
*/
onChange(event: Event) {
event.stopPropagation();
if (isEmpty(this.currentValue)) {
this.model.valueUpdates.next(null);
this.change.emit(null);
this.dispatchUpdate(null);
}
}
onFocus(event) {
this.focus.emit(event);
}
/**
* Updates current value and model value with the selected value.
* @param event The value to set.
*/
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
this.inputValue = null;
this.currentValue = event.item;
this.model.valueUpdates.next(event.item);
this.change.emit(event.item);
this.setCurrentValue(event.item);
this.dispatchUpdate(event.item);
}
/**
* Callback functions for whenClickOnConfidenceNotAccepted event
*/
public whenClickOnConfidenceNotAccepted(confidence: ConfidenceType) {
if (!this.model.readOnly) {
this.click$.next(this.formatter(this.currentValue));
}
}
/**
* Sets the current value with the given value.
* @param value The value to set.
* @param init Representing if is init value or not.
*/
setCurrentValue(value: any, init = false): void {
let result: string;
if (init) {
this.getInitValueFromModel()
.subscribe((value: FormFieldMetadataValueObject) => {
this.currentValue = value;
});
} else {
if (isEmpty(value)) {
result = '';
} else if (typeof value === 'string') {
result = value;
} else {
result = value.display;
}
this.currentValue = result;
}
}
}

View File

@@ -1,12 +1,10 @@
import { FieldParser } from './field-parser';
import { isNotEmpty } from '../../../empty.util';
import { VocabularyFindOptions } from '../../../../core/submission/vocabularies/models/vocabulary-find-options.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { DynamicListCheckboxGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model';
import { DynamicListRadioGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model';
export class ListFieldParser extends FieldParser {
searchOptions: VocabularyFindOptions;
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
const listModelConfig = this.initModel(null, label);

View File

@@ -1,22 +1,13 @@
import { Injectable, Injector } from '@angular/core';
import {
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
DynamicFormGroupModelConfig
} from '@ng-dynamic-forms/core';
import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core';
import { uniqueId } from 'lodash';
import { VocabularyFindOptions } from '../../../../core/submission/vocabularies/models/vocabulary-find-options.model';
import { isEmpty } from '../../../empty.util';
import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
import { FormFieldModel } from '../models/form-field.model';
import {
CONFIG_DATA,
FieldParser,
INIT_FORM_VALUES,
PARSER_OPTIONS,
SUBMISSION_ID
} from './field-parser';
import { CONFIG_DATA, FieldParser, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser';
import { ParserFactory } from './parser-factory';
import { ParserOptions } from './parser-options';
import { ParserType } from './parser-type';
@@ -48,8 +39,6 @@ export class RowParser {
group: [],
};
const vocabularyOptions = new VocabularyFindOptions(scopeUUID);
const scopedFields: FormFieldModel[] = this.filterScopedFields(rowData.fields, submissionScope);
const layoutDefaultGridClass = ' col-sm-' + Math.trunc(12 / scopedFields.length);
@@ -58,7 +47,7 @@ export class RowParser {
const parserOptions: ParserOptions = {
readOnly: readOnly,
submissionScope: submissionScope,
collectionUUID: vocabularyOptions.collection
collectionUUID: scopeUUID
};
// Iterate over row's fields