<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 }}
+ </span>
+ </div>
+ </form>
+ </ng-container>
<div *ngFor="let option of options"
class="select-menu-item"
- (click)="selectOption(option)">
+ (click)="triggerSelection(option)">
<div class="select-menu-item-icon">
<i class="fa fa-check" aria-hidden="true"
*ngIf="option.selected"></i>
</div>
<div class="select-menu-item-content">
{{ option.name }}
- <br>
- <small class="text-muted">
- {{ option.description }}
- </small>
+ <ng-container *ngIf="option.description">
+ <br>
+ <small class="text-muted">
+ {{ option.description }}
+ </small>
+ </ng-container>
</div>
</div>
+ <span i18n
+ class="help-block text-center"
+ *ngIf="data.length === selectionLimit">
+ {{ errorMessages.selectionLimit }}
+ </span>
</ng-template>
<a class="margin-right-sm select-menu-edit"
<span class="text-muted"
*ngIf="data.length === 0"
i18n>
- {{ emptyMessage }}
+ {{ errorMessages.empty }}
</span>
<span *ngFor="let dataItem of data">
<span class="badge badge-pill badge-primary margin-right-sm">
import { PopoverModule } 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';
describe('SelectBadgesComponent', () => {
configureTestBed({
declarations: [SelectBadgesComponent],
- imports: [PopoverModule.forRoot()]
+ imports: [PopoverModule.forRoot(), FormsModule, ReactiveFormsModule]
});
beforeEach(() => {
fixture = TestBed.createComponent(SelectBadgesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
+ component.options = [
+ { name: 'option1', description: '', selected: false },
+ { name: 'option2', description: '', selected: false },
+ { name: 'option3', description: '', selected: false }
+ ];
});
it('should create', () => {
});
it('should add item', () => {
- component.options = [
- { name: 'option1', description: '', selected: false },
- { name: 'option2', description: '', selected: false }
- ];
component.data = [];
- component.selectOption(component.options[1]);
+ component.triggerSelection(component.options[1]);
expect(component.data).toEqual(['option2']);
});
it('should update selected', () => {
- component.options = [
- { name: 'option1', description: '', selected: false },
- { name: 'option2', description: '', selected: false }
- ];
component.data = ['option2'];
component.ngOnChanges();
expect(component.options[0].selected).toBe(false);
});
it('should remove item', () => {
- component.options = [
- { name: 'option1', description: '', selected: true },
- { name: 'option2', description: '', selected: true }
- ];
- component.data = ['option1', 'option2'];
+ component.options.map((option) => {
+ option.selected = true;
+ return option;
+ });
+ component.data = ['option1', 'option2', 'option3'];
component.removeItem('option1');
- expect(component.data).toEqual(['option2']);
+ expect(component.data).toEqual(['option2', 'option3']);
+ });
+
+ it('should not remove item that is not selected', () => {
+ component.options[0].selected = true;
+ component.data = ['option1'];
+ component.removeItem('option2');
+ expect(component.data).toEqual(['option1']);
+ });
+
+ describe('automatically add selected options if not in options array', () => {
+ beforeEach(() => {
+ component.data = ['option1', 'option4'];
+ expect(component.options.length).toBe(3);
+ });
+
+ const expectedResult = () => {
+ expect(component.options.length).toBe(4);
+ expect(component.options[3]).toEqual(new SelectBadgesOption(true, 'option4', ''));
+ };
+
+ it('with no extra settings', () => {
+ component.ngOnInit();
+ expectedResult();
+ });
+
+ it('with custom badges', () => {
+ component.customBadges = true;
+ component.ngOnInit();
+ expectedResult();
+ });
+
+ it('with limit higher than selected', () => {
+ component.selectionLimit = 3;
+ component.ngOnInit();
+ expectedResult();
+ });
+
+ it('with limit equal to selected', () => {
+ component.selectionLimit = 2;
+ component.ngOnInit();
+ expectedResult();
+ });
+
+ it('with limit lower than selected', () => {
+ component.selectionLimit = 1;
+ component.ngOnInit();
+ expectedResult();
+ });
+ });
+
+ 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('adds custom option', () => {
+ expect(component.options[3]).toEqual({
+ name: 'customOption',
+ description: '',
+ selected: true
+ });
+ 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();
+ });
+
+ it('removes custom item selection by name', () => {
+ component.removeItem('customOption');
+ expect(component.data).toEqual([]);
+ expect(component.options[3]).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']);
+ });
+
+ 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);
+ });
+ });
+
+ describe('if the selection limit is reached', function() {
+ beforeEach(() => {
+ component.selectionLimit = 2;
+ component.triggerSelection(component.options[0]);
+ component.triggerSelection(component.options[1]);
+ });
+
+ it('will not select more options', () => {
+ component.triggerSelection(component.options[2]);
+ expect(component.data).toEqual(['option1', 'option2']);
+ });
+
+ it('will unselect options that are selected', () => {
+ component.triggerSelection(component.options[1]);
+ expect(component.data).toEqual(['option1']);
+ });
});
});
-import { Component, OnChanges } from '@angular/core';
+import { Component, OnChanges, OnInit } from '@angular/core';
import { Input } from '@angular/core';
+import { FormControl, ValidatorFn } from '@angular/forms';
+import { CdFormGroup } from '../../forms/cd-form-group';
+import { CdValidators } from '../../forms/cd-validators';
+import { SelectBadgesOption } from './select-badges-option.model';
@Component({
selector: 'cd-select-badges',
templateUrl: './select-badges.component.html',
styleUrls: ['./select-badges.component.scss']
})
-export class SelectBadgesComponent implements OnChanges {
+export class SelectBadgesComponent implements OnInit, OnChanges {
+ @Input() data: Array<string> = [];
+ @Input() options: Array<SelectBadgesOption> = [];
@Input()
- data: Array<string> = [];
- @Input()
- options: Array<SelectBadgesOption> = [];
- @Input()
- emptyMessage = 'There are no items.';
+ 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';
+ form: CdFormGroup;
+ customBadge: FormControl;
+ Object = Object;
constructor() {}
- ngOnChanges() {
- if (!this.options || !this.data || this.data.length === 0) {
+ ngOnInit() {
+ if (this.customBadges) {
+ this.initCustomBadges();
+ }
+ if (this.data.length > 0) {
+ this.initMissingOptions();
+ }
+ }
+
+ 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 initMissingOptions() {
+ const options = this.options.map((option) => option.name);
+ const needToCreate = this.data.filter((option) => options.indexOf(option) === -1);
+ needToCreate.forEach((option) => this.addOption(option));
+ this.forceOptionsToReflectData();
+ }
+
+ private addOption(name: string) {
+ this.options.push(new SelectBadgesOption(false, name, ''));
+ this.triggerSelection(this.options[this.options.length - 1]);
+ }
+
+ private triggerSelection(option: SelectBadgesOption) {
+ if (
+ !option ||
+ (this.selectionLimit && !option.selected && this.data.length >= this.selectionLimit)
+ ) {
return;
}
- this.options.forEach((option) => {
- if (this.data.indexOf(option.name) !== -1) {
- option.selected = true;
- }
- });
+ option.selected = !option.selected;
+ this.updateOptions();
}
private updateOptions() {
});
}
- selectOption(option: SelectBadgesOption) {
- option.selected = !option.selected;
- this.updateOptions();
+ private forceOptionsToReflectData() {
+ this.options.forEach((option) => {
+ if (this.data.indexOf(option.name) !== -1) {
+ option.selected = true;
+ }
+ });
+ }
+
+ ngOnChanges() {
+ if (!this.options || !this.data || this.data.length === 0) {
+ return;
+ }
+ this.forceOptionsToReflectData();
+ }
+
+ addCustomOption() {
+ if (this.customBadge.invalid || this.customBadge.value.length === 0) {
+ return;
+ }
+ this.addOption(this.customBadge.value);
+ this.customBadge.setValue('');
}
removeItem(item: string) {
- const optionToRemove = this.options.find((option: SelectBadgesOption) => {
- return option.name === item;
- });
- optionToRemove.selected = false;
- this.updateOptions();
+ this.triggerSelection(
+ this.options.find((option: SelectBadgesOption) => option.name === item && option.selected)
+ );
}
}