59334: added validation

This commit is contained in:
lotte
2019-02-12 12:48:32 +01:00
parent 45c699e4d8
commit 714811dc07
17 changed files with 226 additions and 52 deletions

View File

@@ -233,6 +233,9 @@
"language": "Lang", "language": "Lang",
"edit": "Edit" "edit": "Edit"
}, },
"metadatafield": {
"invalid": "Please choose a valid metadata field"
},
"notifications": { "notifications": {
"outdated": { "outdated": {
"title": "Changed outdated", "title": "Changed outdated",
@@ -241,6 +244,10 @@
"discarded": { "discarded": {
"title": "Changed discarded", "title": "Changed discarded",
"content": "Your changes were discarded. To reinstate your changes click the 'Undo' button" "content": "Your changes were discarded. To reinstate your changes click the 'Undo' button"
},
"invalid": {
"title": "Metadata invalid",
"content": "Please make sure all fields are valid"
} }
} }
} }

View File

@@ -13,10 +13,14 @@
[(ngModel)]="metadata.key" [(ngModel)]="metadata.key"
(submitSuggestion)="update()" (submitSuggestion)="update()"
(clickSuggestion)="update()" (clickSuggestion)="update()"
(typeSuggestion)="update()"
(findSuggestions)="findMetadataFieldSuggestions($event)" (findSuggestions)="findMetadataFieldSuggestions($event)"
[formControl]="formControl"
ngDefaultControl ngDefaultControl
></ds-input-suggestions> ></ds-input-suggestions>
</div> </div>
<small class="text-danger"
*ngIf="!(valid | async)">{{"item.edit.metadata.metadatafield.invalid" | translate}}</small>
</td> </td>
<td class="col-7"> <td class="col-7">
<div *ngIf="!(editable | async)"> <div *ngIf="!(editable | async)">
@@ -38,10 +42,14 @@
</td> </td>
<td class="col-1 text-center"> <td class="col-1 text-center">
<div> <div>
<i *ngIf="canSetEditable() | async" class="fas fa-edit fa-fw text-primary" (click)="setEditable(true)"></i> <i *ngIf="canSetEditable() | async" class="fas fa-edit fa-fw text-primary"
<i *ngIf="canSetUneditable() | async" class="fas fa-check fa-fw text-success" (click)="setEditable(false)"></i> (click)="setEditable(true)"></i>
<i *ngIf="canRemove() | async" class="fas fa-trash-alt fa-fw text-danger" (click)="remove()"></i> <i *ngIf="canSetUneditable() | async" class="fas fa-check fa-fw text-success"
<i *ngIf="canUndo() | async" class="fas fa-undo-alt fa-fw text-warning" (click)="removeChangesFromField()"></i> (click)="setEditable(false)"></i>
<i *ngIf="canRemove() | async" class="fas fa-trash-alt fa-fw text-danger"
(click)="remove()"></i>
<i *ngIf="canUndo() | async" class="fas fa-undo-alt fa-fw text-warning"
(click)="removeChangesFromField()"></i>
</div> </div>
</td> </td>
</div> </div>

View File

@@ -0,0 +1 @@
@import '../../../../../styles/variables.scss';

View File

@@ -11,6 +11,9 @@ import { FieldChangeType } from '../../../../core/data/object-updates/object-upd
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { inListValidator } from '../../../../shared/utils/validator.functions';
import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { FormControl, ValidationErrors, ValidatorFn } from '@angular/forms';
@Component({ @Component({
selector: 'ds-edit-in-place-field', selector: 'ds-edit-in-place-field',
@@ -21,7 +24,6 @@ import { ObjectUpdatesService } from '../../../../core/data/object-updates/objec
* Component that displays a single metadatum of an item on the edit page * Component that displays a single metadatum of an item on the edit page
*/ */
export class EditInPlaceFieldComponent implements OnInit, OnChanges { export class EditInPlaceFieldComponent implements OnInit, OnChanges {
/** /**
* The current field, value and state of the metadatum * The current field, value and state of the metadatum
*/ */
@@ -39,22 +41,43 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
*/ */
editable: Observable<boolean>; editable: Observable<boolean>;
/**
* Emits whether or not this field is currently valid
*/
valid: Observable<boolean>;
/** /**
* The current suggestions for the metadatafield when editing * The current suggestions for the metadatafield when editing
*/ */
metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]); metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]);
formControl: FormControl;
constructor( constructor(
private metadataFieldService: RegistryService, private metadataFieldService: RegistryService,
private objectUpdatesService: ObjectUpdatesService, private objectUpdatesService: ObjectUpdatesService,
) { ) {
} }
/**
* Sets up an observable that keeps track of the current editable and valid state of this field
* Also creates a form control object for the input suggestions
*/
ngOnInit(): void {
this.editable = this.objectUpdatesService.isEditable(this.route, this.metadata.uuid);
this.valid = this.objectUpdatesService.isValid(this.route, this.metadata.uuid);
this.findMetadataFields().pipe(take(1)).subscribe((metadataFields: string[]) => {
const validator = inListValidator(metadataFields);
this.formControl = new FormControl('', validator);
});
}
/** /**
* Sends a new change update for this field to the object updates service * Sends a new change update for this field to the object updates service
*/ */
update() { update() {
this.objectUpdatesService.saveChangeFieldUpdate(this.route, this.metadata); this.objectUpdatesService.saveChangeFieldUpdate(this.route, this.metadata);
this.objectUpdatesService.setValidFieldUpdate(this.route, this.metadata.uuid, this.formControl.valid);
} }
/** /**
@@ -79,13 +102,6 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
this.objectUpdatesService.removeSingleFieldUpdate(this.route, this.metadata.uuid); this.objectUpdatesService.removeSingleFieldUpdate(this.route, this.metadata.uuid);
} }
/**
* Sets up an observable that keeps track of the current editable state of this field
*/
ngOnInit(): void {
this.editable = this.objectUpdatesService.isEditable(this.route, this.metadata.uuid);
}
/** /**
* Sets the current metadatafield based on the fieldUpdate input field * Sets the current metadatafield based on the fieldUpdate input field
*/ */
@@ -115,6 +131,13 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
); );
} }
findMetadataFields(): Observable<string[]> {
return this.metadataFieldService.getAllMetadataFields().pipe(
getSucceededRemoteData(),
take(1),
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
}
/** /**
* Check if a user should be allowed to edit this field * Check if a user should be allowed to edit this field
* @return an observable that emits true when the user should be able to edit this field and false when they should not * @return an observable that emits true when the user should be able to edit this field and false when they should not

View File

@@ -0,0 +1,5 @@
@import '../../../../styles/variables.scss';
.button-row .btn {
min-width: $button-min-width;
}

View File

@@ -11,7 +11,7 @@ import {
Identifiable Identifiable
} from '../../../core/data/object-updates/object-updates.reducer'; } from '../../../core/data/object-updates/object-updates.reducer';
import { Metadatum } from '../../../core/shared/metadatum.model'; import { Metadatum } from '../../../core/shared/metadatum.model';
import { first, switchMap, tap } from 'rxjs/operators'; import { first, map, switchMap, tap } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../core/shared/operators'; import { getSucceededRemoteData } from '../../../core/shared/operators';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -20,6 +20,7 @@ import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'ds-item-metadata', selector: 'ds-item-metadata',
styleUrls: ['./item-metadata.component.scss'],
templateUrl: './item-metadata.component.html', templateUrl: './item-metadata.component.html',
}) })
/** /**
@@ -114,22 +115,30 @@ export class ItemMetadataComponent implements OnInit {
* Makes sure the new version of the item is rendered on the page * Makes sure the new version of the item is rendered on the page
*/ */
submit() { submit() {
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.route, this.item.metadata) as Observable<Metadatum[]>; this.isValid().pipe(first()).subscribe((isValid) => {
metadata$.pipe( if (isValid) {
first(), const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.route, this.item.metadata) as Observable<Metadatum[]>;
switchMap((metadata: Metadatum[]) => { metadata$.pipe(
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata }); first(),
return this.itemService.update(updatedItem); switchMap((metadata: Metadatum[]) => {
}), const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata });
tap(() => this.itemService.commitUpdates()), return this.itemService.update(updatedItem);
getSucceededRemoteData() }),
).subscribe( tap(() => this.itemService.commitUpdates()),
(rd: RemoteData<Item>) => { getSucceededRemoteData()
this.item = rd.payload; ).subscribe(
this.initializeOriginalFields(); (rd: RemoteData<Item>) => {
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata); this.item = rd.payload;
} this.initializeOriginalFields();
) this.updates$ = this.objectUpdatesService.getFieldUpdates(this.route, this.item.metadata);
}
)
} else {
const title = this.translateService.instant('item.edit.metadata.notifications.invalid.title');
const content = this.translateService.instant('item.edit.metadata.notifications.invalid.content');
this.notificationsService.error(title, content);
}
});
} }
/** /**
@@ -163,4 +172,8 @@ export class ItemMetadataComponent implements OnInit {
} }
); );
} }
private isValid() {
return this.objectUpdatesService.isValidPage(this.route);
}
} }

View File

@@ -38,7 +38,7 @@ const ITEM_EDIT_PATH = ':id/edit';
{ {
path: ITEM_EDIT_PATH, path: ITEM_EDIT_PATH,
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule', loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
canActivate: [AuthenticatedGuard] // canActivate: [AuthenticatedGuard]
} }
]) ])
], ],

View File

@@ -1,8 +1,6 @@
import { import {
ActionReducerMap, ActionReducerMap,
createFeatureSelector, createFeatureSelector,
createSelector,
MemoizedSelector
} from '@ngrx/store'; } from '@ngrx/store';
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer'; import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
@@ -14,8 +12,6 @@ import {
objectUpdatesReducer, objectUpdatesReducer,
ObjectUpdatesState ObjectUpdatesState
} from './data/object-updates/object-updates.reducer'; } from './data/object-updates/object-updates.reducer';
import { hasValue } from '../shared/empty.util';
import { AppState } from '../app.reducer';
export interface CoreState { export interface CoreState {
'cache/object': ObjectCacheState, 'cache/object': ObjectCacheState,
@@ -35,4 +31,4 @@ export const coreReducers: ActionReducerMap<CoreState> = {
'auth': authReducer, 'auth': authReducer,
}; };
export const coreSelector = createFeatureSelector<CoreState>('core'); export const coreSelector = createFeatureSelector<CoreState>('core');

View File

@@ -9,6 +9,7 @@ import { INotification } from '../../../shared/notifications/models/notification
export const ObjectUpdatesActionTypes = { export const ObjectUpdatesActionTypes = {
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'), INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'), SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'), ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
DISCARD: type('dspace/core/cache/object-updates/DISCARD'), DISCARD: type('dspace/core/cache/object-updates/DISCARD'),
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'), REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
@@ -109,6 +110,33 @@ export class SetEditableFieldUpdateAction implements Action {
} }
} }
/**
* An ngrx action to set the isValid state of an existing field in the ObjectUpdates state for a certain page url
*/
export class SetValidFieldUpdateAction implements Action {
type = ObjectUpdatesActionTypes.SET_VALID_FIELD;
payload: {
url: string,
uuid: string,
isValid: boolean,
};
/**
* Create a new SetEditableFieldUpdateAction
*
* @param url
* the unique url of the page
* @param fieldUUID The UUID of the field of which
* @param isValid The new isValid value for the field
*/
constructor(
url: string,
fieldUUID: string,
isValid: boolean) {
this.payload = { url, uuid: fieldUUID, isValid };
}
}
/** /**
* An ngrx action to discard all existing updates in the ObjectUpdates state for a certain page url * An ngrx action to discard all existing updates in the ObjectUpdates state for a certain page url
*/ */

View File

@@ -7,7 +7,7 @@ import {
ObjectUpdatesActionTypes, ObjectUpdatesActionTypes,
ReinstateObjectUpdatesAction, ReinstateObjectUpdatesAction,
RemoveFieldUpdateAction, RemoveFieldUpdateAction,
RemoveObjectUpdatesAction, SetEditableFieldUpdateAction RemoveObjectUpdatesAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction
} from './object-updates.actions'; } from './object-updates.actions';
import { hasNoValue, hasValue } from '../../../shared/empty.util'; import { hasNoValue, hasValue } from '../../../shared/empty.util';
@@ -15,7 +15,8 @@ export const OBJECT_UPDATES_TRASH_PATH = '/trash';
export interface FieldState { export interface FieldState {
editable: boolean, editable: boolean,
isNew: boolean isNew: boolean,
isValid: boolean
} }
export interface FieldStates { export interface FieldStates {
@@ -46,8 +47,8 @@ export interface ObjectUpdatesState {
[url: string]: ObjectUpdatesEntry; [url: string]: ObjectUpdatesEntry;
} }
const initialFieldState = { editable: false, isNew: false }; const initialFieldState = { editable: false, isNew: false, isValid: true };
const initialNewFieldState = { editable: true, isNew: true }; const initialNewFieldState = { editable: true, isNew: true, isValid: true };
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState = Object.create(null); const initialState = Object.create(null);
@@ -80,6 +81,9 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
case ObjectUpdatesActionTypes.SET_EDITABLE_FIELD: { case ObjectUpdatesActionTypes.SET_EDITABLE_FIELD: {
return setEditableFieldUpdate(state, action as SetEditableFieldUpdateAction); return setEditableFieldUpdate(state, action as SetEditableFieldUpdateAction);
} }
case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
}
default: { default: {
return state; return state;
} }
@@ -147,8 +151,8 @@ function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) {
Object.keys(pageState.fieldStates).forEach((uuid: string) => { Object.keys(pageState.fieldStates).forEach((uuid: string) => {
const fieldState: FieldState = pageState.fieldStates[uuid]; const fieldState: FieldState = pageState.fieldStates[uuid];
if (!fieldState.isNew) { if (!fieldState.isNew) {
/* After discarding we don't want the reset fields to stay editable */ /* After discarding we don't want the reset fields to stay editable or invalid */
newFieldStates[uuid] = Object.assign({}, fieldState, { editable: false }); newFieldStates[uuid] = Object.assign({}, fieldState, { editable: false, isValid: true });
} }
}); });
@@ -215,7 +219,7 @@ function removeFieldUpdate(state: any, action: RemoveFieldUpdateAction) {
/* If this field was added, just throw it away */ /* If this field was added, just throw it away */
delete newFieldStates[uuid]; delete newFieldStates[uuid];
} else { } else {
newFieldStates[uuid] = Object.assign({}, newFieldStates[uuid], { editable: false }); newFieldStates[uuid] = Object.assign({}, newFieldStates[uuid], { editable: false, isValid: true });
} }
} }
newPageState = Object.assign({}, state[url], { newPageState = Object.assign({}, state[url], {
@@ -243,7 +247,7 @@ function determineChangeType(oldType: FieldChangeType, newType: FieldChangeType)
} }
/** /**
* Set the state of a specific action's url and uuid to false or true * Set the editable state of a specific action's url and uuid to false or true
* @param state The current state * @param state The current state
* @param action The action to perform on the current state * @param action The action to perform on the current state
*/ */
@@ -264,6 +268,28 @@ function setEditableFieldUpdate(state: any, action: SetEditableFieldUpdateAction
return Object.assign({}, state, { [url]: newPageState }); return Object.assign({}, state, { [url]: newPageState });
} }
/**
* Set the isValid state of a specific action's url and uuid to false or true
* @param state The current state
* @param action The action to perform on the current state
*/
function setValidFieldUpdate(state: any, action: SetValidFieldUpdateAction) {
const url: string = action.payload.url;
const uuid: string = action.payload.uuid;
const isValid: boolean = action.payload.isValid;
const pageState: ObjectUpdatesEntry = state[url];
const fieldState = pageState.fieldStates[uuid];
const newFieldState = Object.assign({}, fieldState, { isValid });
const newFieldStates = Object.assign({}, pageState.fieldStates, { [uuid]: newFieldState });
const newPageState = Object.assign({}, pageState, { fieldStates: newFieldStates });
return Object.assign({}, state, { [url]: newPageState });
}
/** /**
* Method to create an initial FieldStates object based on a list of Identifiable objects * Method to create an initial FieldStates object based on a list of Identifiable objects
* @param fields Identifiable objects * @param fields Identifiable objects

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { coreSelector, CoreState } from '../../core.reducers'; import { coreSelector, CoreState } from '../../core.reducers';
import { import {
FieldState,
FieldUpdates, FieldUpdates,
Identifiable, OBJECT_UPDATES_TRASH_PATH, Identifiable, OBJECT_UPDATES_TRASH_PATH,
ObjectUpdatesEntry, ObjectUpdatesEntry,
@@ -15,7 +16,7 @@ import {
InitializeFieldsAction, InitializeFieldsAction,
ReinstateObjectUpdatesAction, ReinstateObjectUpdatesAction,
RemoveFieldUpdateAction, RemoveFieldUpdateAction,
SetEditableFieldUpdateAction SetEditableFieldUpdateAction, SetValidFieldUpdateAction
} from './object-updates.actions'; } from './object-updates.actions';
import { filter, map } from 'rxjs/operators'; import { filter, map } from 'rxjs/operators';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
@@ -102,6 +103,33 @@ export class ObjectUpdatesService {
) )
} }
/**
* Method to check if a specific field is currently valid in the store
* @param url The URL of the page on which the field resides
* @param uuid The UUID of the field
*/
isValid(url: string, uuid: string): Observable<boolean> {
const objectUpdates = this.getObjectEntry(url);
return objectUpdates.pipe(
filter((objectEntry) => hasValue(objectEntry.fieldStates[uuid])),
map((objectEntry) => objectEntry.fieldStates[uuid].isValid
)
)
}
/**
* Method to check if a specific page is currently valid in the store
* @param url The URL of the page
*/
isValidPage(url: string): Observable<boolean> {
const objectUpdates = this.getObjectEntry(url);
return objectUpdates.pipe(
map((entry: ObjectUpdatesEntry) => {
return Object.values(entry.fieldStates).findIndex((state: FieldState) => !state.isValid) < 0
})
)
}
/** /**
* Calls the saveFieldUpdate method with FieldChangeType.ADD * Calls the saveFieldUpdate method with FieldChangeType.ADD
* @param url The page's URL for which the changes are saved * @param url The page's URL for which the changes are saved
@@ -139,6 +167,16 @@ export class ObjectUpdatesService {
this.store.dispatch(new SetEditableFieldUpdateAction(url, uuid, editable)); this.store.dispatch(new SetEditableFieldUpdateAction(url, uuid, editable));
} }
/**
* Dispatches a SetValidFieldUpdateAction to the store to set a field's isValid state
* @param url The URL of the page on which the field resides
* @param uuid The UUID of the field that should be set
* @param valid The new value of isValid in the store for this field
*/
setValidFieldUpdate(url: string, uuid: string, valid: boolean) {
this.store.dispatch(new SetValidFieldUpdateAction(url, uuid, valid));
}
/** /**
* Method to dispatch an DiscardObjectUpdatesAction to the store * Method to dispatch an DiscardObjectUpdatesAction to the store
* @param url The page's URL for which the changes should be discarded * @param url The page's URL for which the changes should be discarded

View File

@@ -1,3 +1,3 @@
<ds-form *ngIf="formModel" <ds-form *ngIf="formModel"
[formId]="'comcol-form-id'" [formId]="'comcol-form-id'"
[formModel]="formModel" (submitForm)="onSubmit()"></ds-form> [formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>

View File

@@ -50,10 +50,7 @@ describe('ComColFormComponent', () => {
]; ];
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
const locationStub = { const locationStub = jasmine.createSpyObj('location', ['back']);
back: () => {
}
};
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
beforeEach(async(() => { beforeEach(async(() => {
@@ -112,4 +109,11 @@ describe('ComColFormComponent', () => {
); );
}) })
}); });
describe('onCancel', () => {
it('should call the back method on the Location service', () => {
comp.onCancel();
expect(locationStub.back).toHaveBeenCalled();
});
});
}); });

View File

@@ -112,4 +112,8 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
} }
); );
} }
onCancel() {
this.location.back();
}
} }

View File

@@ -1,9 +1,13 @@
import { import {
Component, Component,
ElementRef, EventEmitter, forwardRef, ElementRef,
EventEmitter,
forwardRef,
Input, Input,
OnChanges,
Output, Output,
QueryList, SimpleChanges, QueryList,
SimpleChanges,
ViewChild, ViewChild,
ViewChildren ViewChildren
} from '@angular/core'; } from '@angular/core';
@@ -19,6 +23,8 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
// Usage of forwardRef necessary https://github.com/angular/angular.io/issues/1151
// tslint:disable-next-line:no-forward-ref
useExisting: forwardRef(() => InputSuggestionsComponent), useExisting: forwardRef(() => InputSuggestionsComponent),
multi: true multi: true
} }
@@ -28,7 +34,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
/** /**
* Component representing a form with a autocomplete functionality * Component representing a form with a autocomplete functionality
*/ */
export class InputSuggestionsComponent implements ControlValueAccessor { export class InputSuggestionsComponent implements ControlValueAccessor, OnChanges {
/** /**
* The suggestions that should be shown * The suggestions that should be shown
*/ */
@@ -64,6 +70,11 @@ export class InputSuggestionsComponent implements ControlValueAccessor {
*/ */
@Output() clickSuggestion = new EventEmitter(); @Output() clickSuggestion = new EventEmitter();
/**
* Output for when something is typed in the input field
*/
@Output() typeSuggestion = new EventEmitter();
/** /**
* Output for when new suggestions should be requested * Output for when new suggestions should be requested
*/ */
@@ -195,6 +206,7 @@ export class InputSuggestionsComponent implements ControlValueAccessor {
this.findSuggestions.emit(data); this.findSuggestions.emit(data);
} }
this.blockReopen = false; this.blockReopen = false;
this.typeSuggestion.emit(data);
} }
onSubmit(data) { onSubmit(data) {

View File

@@ -0,0 +1,7 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
export function inListValidator(list: string[]): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
const contains = list.indexOf(control.value) > 0;
return contains ? null : {inList: {value: control.value}} };
}

View File

@@ -1,6 +1,8 @@
$content-spacing: $spacer * 1.5; $content-spacing: $spacer * 1.5;
$button-height: $input-btn-padding-y * 2 + $input-btn-line-height + calculateRem($input-btn-border-width*2); $button-height: $input-btn-padding-y * 2 + $input-btn-line-height + calculateRem($input-btn-border-width*2);
$button-min-width: 100px;
$card-height-percentage:98%; $card-height-percentage:98%;
$card-thumbnail-height:240px; $card-thumbnail-height:240px;
$dropdown-menu-max-height: 200px; $dropdown-menu-max-height: 200px;