<ng-template #popTemplate>
- <ng-container *ngIf="customBadges">
- <form name="form"
- #formDir="ngForm"
- [formGroup]="form"
- novalidate>
- <div [ngClass]="{'has-error': form.showError('customBadge', formDir)}">
- <input type="text"
- formControlName="customBadge"
- i18n-placeholder
- [placeholder]="customBadgeMessage"
- (keyup)="$event.keyCode == 13 ? addCustomOption(customBadge) : null"
- class="form-control text-center"/>
- <ng-container *ngFor="let error of Object.keys(errorMessages.custom.validation)">
- <span
- i18n
- class="help-block text-center"
- *ngIf="form.showError('customBadge', formDir) && customBadge.hasError(error)">
- {{ errorMessages.custom.validation[error] }}
- </span>
- </ng-container>
- <span i18n
- class="help-block text-center"
- *ngIf="form.showError('customBadge', formDir) && customBadge.hasError('duplicate')">
- {{ errorMessages.custom.duplicate }}
+ <form name="form"
+ #formDir="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div [ngClass]="{'has-error': form.showError('filter', formDir)}">
+ <input type="text"
+ formControlName="filter"
+ i18n-placeholder
+ [placeholder]="messages.filter"
+ (keyup)="$event.keyCode == 13 ? selectOption() : updateFilter()"
+ class="form-control text-center"/>
+ <ng-container *ngFor="let error of Object.keys(messages.customValidations)">
+ <span
+ i18n
+ class="help-block text-center"
+ *ngIf="form.showError('filter', formDir) && filter.hasError(error)">
+ {{ messages.customValidations[error] }}
</span>
- </div>
- </form>
- </ng-container>
- <div *ngFor="let option of options"
+ </ng-container>
+ </div>
+ </form>
+ <div *ngFor="let option of filteredOptions"
class="select-menu-item"
+ [class.disabled]="data.length === selectionLimit && !option.selected"
(click)="triggerSelection(option)">
<div class="select-menu-item-icon">
<i class="fa fa-check" aria-hidden="true"
</ng-container>
</div>
</div>
- <span i18n
- class="help-block text-center"
- *ngIf="data.length === selectionLimit">
- {{ errorMessages.selectionLimit }}
- </span>
+ <div *ngIf="isCreatable()"
+ class="select-menu-item"
+ (click)="addCustomOption(filter.value)">
+ <div class="select-menu-item-icon">
+ <i class="fa fa-tag" aria-hidden="true"></i>
+
+ </div>
+ <div class="select-menu-item-content">
+ {{ messages.add }} '{{ filter.value }}'
+ </div>
+ </div>
+ <div class="has-warning"
+ *ngIf="data.length === selectionLimit">
+ <span i18n
+ class="help-block text-center text-warning"
+ [tooltip]="messages.selectionLimit.tooltip"
+ *ngIf="data.length === selectionLimit">
+ {{ messages.selectionLimit.text }}
+ </span>
+ </div>
</ng-template>
<a class="margin-right-sm select-menu-edit"
<span class="text-muted"
*ngIf="data.length === 0"
i18n>
- {{ errorMessages.empty }}
+ {{ messages.empty }}
</span>
<span *ngFor="let dataItem of data">
<span class="badge badge-pill badge-primary margin-right-sm">
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule, Validators } from '@angular/forms';
-import { PopoverModule } from 'ngx-bootstrap';
+import { PopoverModule, TooltipModule } from 'ngx-bootstrap';
-import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { configureTestBed } from '../../../../testing/unit-test-helper';
import { SelectBadgesOption } from './select-badges-option.model';
import { SelectBadgesComponent } from './select-badges.component';
let component: SelectBadgesComponent;
let fixture: ComponentFixture<SelectBadgesComponent>;
+ const selectOption = (filter: string) => {
+ component.filter.setValue(filter);
+ component.updateFilter();
+ component.selectOption();
+ };
+
configureTestBed({
declarations: [SelectBadgesComponent],
- imports: [PopoverModule.forRoot(), FormsModule, ReactiveFormsModule]
+ imports: [PopoverModule.forRoot(), TooltipModule, ReactiveFormsModule]
});
beforeEach(() => {
expect(component.data).toEqual(['option1']);
});
+ describe('filter values', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ });
+
+ it('shows all options with no value set', () => {
+ expect(component.filteredOptions).toEqual(component.options);
+ });
+
+ it('shows one option that it filtered for', () => {
+ component.filter.setValue('2');
+ component.updateFilter();
+ expect(component.filteredOptions).toEqual([component.options[1]]);
+ });
+
+ it('shows all options after selecting something', () => {
+ component.filter.setValue('2');
+ component.updateFilter();
+ component.selectOption();
+ expect(component.filteredOptions).toEqual(component.options);
+ });
+
+ it('is not able to create by default with no value set', () => {
+ component.updateFilter();
+ expect(component.isCreatable()).toBeFalsy();
+ });
+
+ it('is not able to create by default with a value set', () => {
+ component.filter.setValue('2');
+ component.updateFilter();
+ expect(component.isCreatable()).toBeFalsy();
+ });
+ });
+
describe('automatically add selected options if not in options array', () => {
beforeEach(() => {
component.data = ['option1', 'option4'];
});
});
+ describe('sorted array and options', () => {
+ beforeEach(() => {
+ component.customBadges = true;
+ component.customBadgeValidators = [Validators.pattern('[A-Za-z0-9_]+')];
+ component.data = ['c', 'b'];
+ component.options = [
+ new SelectBadgesOption(true, 'd', ''),
+ new SelectBadgesOption(true, 'a', '')
+ ];
+ component.ngOnInit();
+ });
+
+ it('has a sorted selection', () => {
+ expect(component.data).toEqual(['a', 'b', 'c', 'd']);
+ });
+
+ it('has a sorted options', () => {
+ const sortedOptions = [
+ new SelectBadgesOption(true, 'a', ''),
+ new SelectBadgesOption(true, 'b', ''),
+ new SelectBadgesOption(true, 'c', ''),
+ new SelectBadgesOption(true, 'd', '')
+ ];
+ expect(component.options).toEqual(sortedOptions);
+ });
+
+ it('has a sorted selection after adding an item', () => {
+ selectOption('block');
+ expect(component.data).toEqual(['a', 'b', 'block', 'c', 'd']);
+ });
+
+ it('has a sorted options after adding an item', () => {
+ selectOption('block');
+ const sortedOptions = [
+ new SelectBadgesOption(true, 'a', ''),
+ new SelectBadgesOption(true, 'b', ''),
+ new SelectBadgesOption(true, 'block', ''),
+ new SelectBadgesOption(true, 'c', ''),
+ new SelectBadgesOption(true, 'd', '')
+ ];
+ expect(component.options).toEqual(sortedOptions);
+ });
+ });
+
describe('with custom options', () => {
beforeEach(() => {
component.customBadges = true;
component.customBadgeValidators = [Validators.pattern('[A-Za-z0-9_]+')];
component.ngOnInit();
- component.customBadge.setValue('customOption');
- component.addCustomOption();
+ });
+
+ it('is not able to create with no value set', () => {
+ component.updateFilter();
+ expect(component.isCreatable()).toBeFalsy();
+ });
+
+ it('is able to create with a valid value set', () => {
+ component.filter.setValue('2');
+ component.updateFilter();
+ expect(component.isCreatable()).toBeTruthy();
+ });
+
+ it('is not able to create with a value set that already exist', () => {
+ component.filter.setValue('option2');
+ component.updateFilter();
+ expect(component.isCreatable()).toBeFalsy();
});
it('adds custom option', () => {
- expect(component.options[3]).toEqual({
+ selectOption('customOption');
+ expect(component.options[0]).toEqual({
name: 'customOption',
description: '',
selected: true
});
+ expect(component.options.length).toBe(4);
expect(component.data).toEqual(['customOption']);
});
it('will not add an option that did not pass the validation', () => {
- component.customBadge.setValue(' this does not pass ');
- component.addCustomOption();
- expect(component.options.length).toBe(4);
- expect(component.data).toEqual(['customOption']);
- expect(component.customBadge.invalid).toBeTruthy();
+ selectOption(' this does not pass ');
+ expect(component.options.length).toBe(3);
+ expect(component.data).toEqual([]);
+ expect(component.filter.invalid).toBeTruthy();
});
it('removes custom item selection by name', () => {
+ selectOption('customOption');
component.removeItem('customOption');
expect(component.data).toEqual([]);
- expect(component.options[3]).toEqual({
+ expect(component.options.length).toBe(4);
+ expect(component.options[0]).toEqual({
name: 'customOption',
description: '',
selected: false
});
it('will not add an option that is already there', () => {
- component.customBadge.setValue('option2');
- component.addCustomOption();
- expect(component.options.length).toBe(4);
- expect(component.data).toEqual(['customOption']);
+ selectOption('option2');
+ expect(component.options.length).toBe(3);
+ expect(component.data).toEqual(['option2']);
});
it('will not add an option twice after each other', () => {
- component.customBadge.setValue('onlyOnce');
- component.addCustomOption();
- component.addCustomOption();
- expect(component.data).toEqual(['customOption', 'onlyOnce']);
- expect(component.options.length).toBe(5);
+ selectOption('onlyOnce');
+ expect(component.data).toEqual(['onlyOnce']);
+ selectOption('onlyOnce');
+ expect(component.data).toEqual([]);
+ selectOption('onlyOnce');
+ expect(component.data).toEqual(['onlyOnce']);
+ expect(component.options.length).toBe(4);
});
});
-import { Component, OnChanges, OnInit } from '@angular/core';
-import { Input } from '@angular/core';
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { FormControl, ValidatorFn } from '@angular/forms';
+
+import * as _ from 'lodash';
+
import { CdFormGroup } from '../../forms/cd-form-group';
-import { CdValidators } from '../../forms/cd-validators';
+import { SelectBadgesMessages } from './select-badges-messages.model';
import { SelectBadgesOption } from './select-badges-option.model';
@Component({
styleUrls: ['./select-badges.component.scss']
})
export class SelectBadgesComponent implements OnInit, OnChanges {
- @Input() data: Array<string> = [];
- @Input() options: Array<SelectBadgesOption> = [];
@Input()
- errorMessages = {
- empty: 'There are no items.',
- selectionLimit: 'Selection limit reached',
- custom: {
- validation: {},
- duplicate: 'Already exits'
- }
- };
- @Input() selectionLimit: number;
- @Input() customBadges = false;
- @Input() customBadgeValidators: ValidatorFn[] = [];
- @Input() customBadgeMessage = 'Use custom tag';
+ data: Array<string> = [];
+ @Input()
+ options: Array<SelectBadgesOption> = [];
+ @Input()
+ messages = new SelectBadgesMessages({});
+ @Input()
+ selectionLimit: number;
+ @Input()
+ customBadges = false;
+ @Input()
+ customBadgeValidators: ValidatorFn[] = [];
form: CdFormGroup;
- customBadge: FormControl;
+ filter: FormControl;
Object = Object;
+ filteredOptions: Array<SelectBadgesOption> = [];
constructor() {}
ngOnInit() {
- if (this.customBadges) {
- this.initCustomBadges();
- }
+ this.initFilter();
if (this.data.length > 0) {
this.initMissingOptions();
}
+ this.options = _.sortBy(this.options, ['name']);
+ this.updateOptions();
}
- private initCustomBadges() {
- this.customBadgeValidators.push(
- CdValidators.custom(
- 'duplicate',
- (badge) => this.options && this.options.some((option) => option.name === badge)
- )
- );
- this.customBadge = new FormControl('', { validators: this.customBadgeValidators });
- this.form = new CdFormGroup({ customBadge: this.customBadge });
+ private initFilter() {
+ this.filter = new FormControl('', { validators: this.customBadgeValidators });
+ this.form = new CdFormGroup({ filter: this.filter });
+ this.filteredOptions = [...this.options];
}
private initMissingOptions() {
private addOption(name: string) {
this.options.push(new SelectBadgesOption(false, name, ''));
- this.triggerSelection(this.options[this.options.length - 1]);
+ this.options = _.sortBy(this.options, ['name']);
+ this.triggerSelection(this.options.find((option) => option.name === name));
}
- private triggerSelection(option: SelectBadgesOption) {
+ triggerSelection(option: SelectBadgesOption) {
if (
!option ||
(this.selectionLimit && !option.selected && this.data.length >= this.selectionLimit)
this.data.push(option.name);
}
});
+ this.updateFilter();
+ }
+
+ updateFilter() {
+ this.filteredOptions = this.options.filter((option) => option.name.includes(this.filter.value));
}
private forceOptionsToReflectData() {
this.forceOptionsToReflectData();
}
+ selectOption() {
+ if (this.filteredOptions.length === 0) {
+ this.addCustomOption();
+ } else {
+ this.triggerSelection(this.filteredOptions[0]);
+ this.resetFilter();
+ }
+ }
+
addCustomOption() {
- if (this.customBadge.invalid || this.customBadge.value.length === 0) {
+ if (!this.isCreatable()) {
return;
}
- this.addOption(this.customBadge.value);
- this.customBadge.setValue('');
+ this.addOption(this.filter.value);
+ this.resetFilter();
+ }
+
+ isCreatable() {
+ return (
+ this.customBadges &&
+ this.filter.valid &&
+ this.filter.value.length > 0 &&
+ this.filteredOptions.every((option) => option.name !== this.filter.value)
+ );
+ }
+
+ private resetFilter() {
+ this.filter.setValue('');
+ this.updateFilter();
}
removeItem(item: string) {