Skip to content

Instantly share code, notes, and snippets.

@ukcoderj
Last active May 20, 2021 13:58
Show Gist options
  • Save ukcoderj/574fc154c9e165b40ef2c9835aa3be0d to your computer and use it in GitHub Desktop.
Save ukcoderj/574fc154c9e165b40ef2c9835aa3be0d to your computer and use it in GitHub Desktop.
Angular combobox (a box you can type in, or select an option from - not a typeahead!)
<div class="cbx-container">
<div class="input-group">
<!--combobox style view -->
<input class="form-control cbx-input" type="text"
autocomplete="off"
(click)="closeOptionsDialog()"
[(ngModel)]="state.displayValue"
(input)="onValueTypedChange($event.target.value)"
#inputBox="ngModel" />
<button type="button" class=" btn btn-outline-dark cbx-button"
(click)="showOptions = !showOptions">
<fa-icon *ngIf="!showOptions" [icon]="faChevronDown"></fa-icon>
<fa-icon *ngIf="showOptions" [icon]="faChevronUp"></fa-icon>
</button>
<!-- Options dropdown -->
<div *ngIf="showOptions" class="cbx-options-ctnr">
<button type="button" class="list-group-item list-group-item-action"
[attr.id]="'opt_' + opt.key"
*ngFor="let opt of availableSelections" (click)="onSelectOption(opt)">
{{opt.value}}
</button>
</div>
</div><!--input group -->
</div>
<!-- Validation (drops below) -->
<div *ngIf="parentNgForm.submitted && minLength && !state.displayValue" class="cbx-validation alert alert-danger">
{{ 'ERRORS.REQUIRED' | translate }}
</div>
<div *ngIf="parentNgForm.submitted && minLength && state.displayValue && state.displayValue.length < minLength" class="cbx-validation alert alert-danger">
{{ 'ERRORS.MIN_LENGTH' | translate:{ minlength:minLength } }}
</div>
<!-- Debugging code -->
<!--
<hr />
<div>
FormControl: {{frmControl.value}}
</div>
-->
<!--
<div id="test" style="height:50px; width: 200px; background-color:aquamarine"
*ngFor="let row of availableSelections">
{{row.key}} | {{row.value}}
</div>-->
.cbx-container {
}
.cbx-input {
}
.cbx-input:focus {
box-shadow: none;
}
.cbx-button {
border: 1px solid #ced4da;
}
.cbx-button:hover {
background-color: transparent !important;
color: #212529; /* need this to stop it all going white */
}
.cbx-button:focus {
box-shadow: 0 0 0 0.25rem rgb(36 116 146 / 25%);
}
.cbx-options-ctnr {
margin-left: 1% !important;
width: 98%;
margin-top: 2.4em;
max-height: 160px;
overflow-y: auto;
position: absolute;
z-index: 100;
box-shadow: 0 0 0 0.25rem rgb(36 116 146 / 25%);
}
.cbx-validation {
}
import { Component, Input, NgZone, OnInit, SimpleChanges } from '@angular/core';
import { FormControl, NgForm } from '@angular/forms';
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import { ComboboxState } from './combobox-state';
import { KeyValueStringPair } from './key-value-string-pair';
@Component({
selector: 'app-combobox',
templateUrl: './combobox.component.html',
styleUrls: ['./combobox.component.scss']
})
export class ComboboxComponent implements OnInit {
faChevronDown = faChevronDown;
faChevronUp = faChevronUp;
showOptions = false;
isSettingInComponent = false;
@Input() public parentNgForm: NgForm;
@Input() public availableSelections: KeyValueStringPair[];
@Input() public frmControl: FormControl;
@Input() public minLength: number;
public state: ComboboxState;
constructor(public ngZone: NgZone) {
this.state = new ComboboxState();
}
ngOnInit(): void {
if (this.frmControl.value) {
this.setStateInitial();
}
this.frmControl.valueChanges.subscribe((val: any) => {
if (!this.isSettingInComponent) {
this.setStateInitial();
}
this.isSettingInComponent = false;
});
}
/*
* When a change is made to the FormControl (or language)
* outside this control, this method will bring the value of the
* textbox back into line.
*
* */
public setStateInitial() {
var formVal = this.frmControl.value;
var isFormValueAKey = this.doesKeyExist(formVal);
if (isFormValueAKey) {
this.state.key = formVal;
this.state.displayValue = this.availableSelections.filter(_ => _.key === formVal)[0].value;
this.state.isAPredefinedValue = true;
} else {
this.state.key = '';
this.state.displayValue = this.frmControl.value;
this.state.isAPredefinedValue = false;
}
}
setState() {
const key = this.getKeyFromLookupValues(this.state.displayValue);
if (key) {
this.state.key = key;
this.state.isAPredefinedValue = true;
} else {
this.state.key = '';
this.state.isAPredefinedValue = false;
}
}
/*
* If someone types directly into the textbox, it will no longer be considered a
* predefined value and will not be translated.
* */
onValueTypedChange(val: string) {
this.isSettingInComponent = true;
this.frmControl.setValue(val);
this.setState();
this.showOptions = false;
}
/*
* Selcting an option from the list. This is a pre-defined value
* We show the 'value' but store the 'key' in the background as the value for the form.
* */
onSelectOption(option: KeyValueStringPair) {
this.state.displayValue = option.value;
this.isSettingInComponent = true;
this.frmControl.setValue(option.key);
this.setState();
this.showOptions = false;
}
/**
* Check if the key exists in the lookup
* @param key - The key to check
*/
doesKeyExist(key: string): boolean {
for (let i = 0; i < this.availableSelections.length; i++) {
let rowToCheck = this.availableSelections[i];
if (rowToCheck.key === key) {
return true;
}
}
return false
}
/**
* Get the key from the lookup values if possible.
* @param val - the value to match on, so we can find a predefined key.
*/
getKeyFromLookupValues(val: string): string {
let key = '';
for (let i = 0; i < this.availableSelections.length; i++) {
let rowToCheck = this.availableSelections[i];
if (rowToCheck.value === val) {
key = rowToCheck.key;
break;
}
}
return key;
}
/*
* Hides the list of predefined options.
* */
closeOptionsDialog() {
this.ngZone.run(() => {
this.showOptions = false;
});
}
}
import { KeyValueStringPair } from "./key-value-string-pair";
export class ComboboxHelper {
/**
* Takes a set of keys and a list of translations and converts the ones we want to and array of keyvalue pairs
* @param translationKeys - a set of keys we want back as a keyvalue pair e.g. ['LastName', .......]
* @param translations - a whole set of translations, where the property name will be the key and the property value will be the value
* e.g. (obj.Name = 'First Name'; obj.LastName = 'Surname', ....)
* RETURNS: an array of keyvaluepairs e.g. [ {key: LastName, value: 'Surname' }, ..... ]
*/
public static convertKeysToTranslationKVPs(translationKeys: string[], translations: object): KeyValueStringPair[] {
let returnVal = new Array<KeyValueStringPair>();
for (let i = 0; i < translationKeys.length; i++) {
let key1 = translationKeys[i];
let value1 = translations[key1];
let row: KeyValueStringPair = { key: key1, value: value1 };
returnVal.push(row);
}
return returnVal;
}
}
export class ComboboxState {
displayValue: string;
key: string;
isAPredefinedValue: boolean;
}
export interface KeyValueStringPair {
key: string;
value: string;
}
<div id="combobox_example">
<!--<div *ngFor="let row of titleLabelOptions">{{row.key}} | {{row.value}}</div>-->
<form [formGroup]="testForm" #thisForm="ngForm" (submit)="onSubmit()">
<div formGroupName="profile">
<app-combobox #titleLabelComponent
[parentNgForm]="thisForm"
[frmControl]="titleLabel"
[availableSelections]="titleLabelOptions"
[minLength]="2"
style="width:300px;"></app-combobox>
</div>
<p>Outside</p>
<div>
<button type="button" (click)="pushValue()">Push A Value</button>
</div>
<button type="submit">Submit</button>
</form>
</div>
import { Component, NgZone, OnInit, ViewChild } from '@angular/core';
import { faAtom } from '@fortawesome/free-solid-svg-icons'
import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
import { AuthTestService } from '../../../shared/services/auth-test.service';
import { StringResultTest } from '../../../modules/shared/models/string-result-test';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { KeyValueStringPair } from '../../../modules/shared/combobox/combobox/key-value-string-pair';
import { ComboboxComponent } from '../../shared/combobox/combobox/combobox.component';
import { ComboboxHelper } from '../../shared/combobox/combobox/combobox-helpers';
@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
@ViewChild(ComboboxComponent) titleLabelComponent;
testForm: FormGroup;
formTranslations: object;
titleLabelOptions: KeyValueStringPair[];
/**
*
* @param translate - MUST BE PUBLIC!!!
*/
constructor(public translate: TranslateService,
public ngZone: NgZone,
public fb: FormBuilder) {
// Setting from a component in code behind (better to go straight to UI where possible).
this.translate.stream('SHARED').subscribe((val: any) => {
this.formTranslations = val;
this.goodByeTranslated = val.GOODBYE;
});
this.testForm = this.fb.group({
profile: this.fb.group({
titleLabel: [''],
firstNameLabel: ['', Validators.required]
})
});
// Do something when the language changes
this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
// do something if needed (lang strings handle automagically)
this.setComboboxOptions();
setTimeout(() => {
this.titleLabelComponent.setStateInitial();
}, 100);
});
}
ngOnInit() {
this.setComboboxOptions();
// Add some initialization values.
this.testForm.setValue({
profile: {
titleLabel: 'PREFIX',
firstNameLabel: ''
}
});
}
setComboboxOptions() {
const titleLabelKeys = ['TITLE', 'PREFIX', 'FIRST_PART'];
this.titleLabelOptions = ComboboxHelper.convertKeysToTranslationKVPs(titleLabelKeys, this.formTranslations);
}
get titleLabel(): FormControl {
var profile = this.testForm.get('profile') as FormGroup;
var titleLabel = profile.get('titleLabel') as FormControl;
return titleLabel;
}
lastVal = 'CANCEL';
pushValue() {
var newVal = this.lastVal;
if (newVal !== 'CANCEL') {
this.lastVal = 'CANCEL';
} else {
this.lastVal = 'TESTEROO';
}
this.testForm.setValue({
profile: {
titleLabel: newVal,
firstNameLabel: ''
}
});
}
onSubmit() {
let wholeState = this.titleLabelComponent.state;
var x = this.titleLabel.value;
alert(x);
}
}
@ukcoderj
Copy link
Author

This is not a typeahead -it's a combobox (made in Angular/Typescript) like you might see in WPF/ Windows Forms apps. The advantage of this control is that a user can select something without having an idea up front of what they might want to select.

If you are copying this, you won't need the test-component stuff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment