'Custom input element with inline chips and text
I am trying to create a custom input element that has 2 parts.
- Allows the user to input text
- Allows the user to embed or remove chips in between the text on selecting an option from a dropdown.
I am using Angular 8 and kendo-ui. I've attached the image of the custom input.
I have made use of a contenteditable div to achieve this but the issue I have is once I add the chip, I cannot place the cursor after the chip and continue typing.
The custom input element -
My attempt in achieving this -
My approach -
addChip(event: Event) {
this.selectedValue = event;
const inputEl = document.querySelector('#inputArea');
const closeButton = document.createElement('span');
closeButton.classList.add('chip-close-button', 'k-icon', 'k-i-close-circle');
closeButton.setAttribute('id', 'btnClose');
closeButton.addEventListener('click', () => this.remove());
const textNode = document.createTextNode(this.selectedValue?.text)
const chip = document.createElement('span')
KENDO_CHIP_CONFIG.attributes.forEach(HTMLattribute => {
chip.setAttribute(HTMLattribute.attribute, HTMLattribute.value);
})
chip.appendChild(textNode)
chip.appendChild(closeButton)
chip.classList.add('chip')
inputEl.appendChild(chip)
}
public remove(): void {
const chip = document.querySelector('.chip') as HTMLElement;
chip.remove()
this.selectedValue = null;
}
The HTML code-
<kendo-label [style.display]="'block'" [style]="'margin-bottom: 10px'" [text]="label" [for]="inputArea"></kendo-label>
<div class="textAreaContainer">
<span class="inputArea" [formControl]="control" placeholder="placeholder" id="inputArea" #inputArea contenteditable>
</span>
<kendo-dropdownbutton class="dropDownButton" (itemClick)="addChip($event)" [data]="data" [icon]="icon">
</kendo-dropdownbutton>
</div>
Solution 1:[1]
I found a way and created a separate component which handles this functionality.
chip-textbox.component.html
<div class="textAreaContainer">
<div #editableDiv class="inputArea" id="editableDiv" (focusout)="checkChange($event)" [contentEditable]="true"> </div>
<kendo-dropdownbutton class="dropDownButton" (itemClick)="divClickAdd($event)" [data]="data" [icon]="icon">
</kendo-dropdownbutton>
</div>
chip-textbox.component.ts (A little verbose)
export class ChipTextboxComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
private unSubscribe$: Subject<void> = new Subject<void>();
@ViewChild('editableDiv') public divEditable!: ElementRef;
@Input() public control: FormControl | AbstractControl;
@Input() public placeholder: string;
@Input() public icon: string = 'plus';
@Input() public data: dynamicInputDropDownInterface[];
@Input() public label: string;
@Input() public iconClass: string;
selectorID = `inputArea-${Math.floor(Math.random() * 1000)}`;
public $event: Event;
private clickListener: () => void;
constructor(
private renderer: Renderer2
) { }
ngOnInit(): void {
}
ngAfterViewInit(): void {
this.convertControlValueToHtml();
this.clickListener = this.renderer.listen(this.divEditable.nativeElement, 'click', this.removeChip);
}
ngOnChanges(changes: SimpleChanges): void {
this.control.valueChanges.pipe().subscribe(res => {
this.convertControlValueToHtml()
});
}
divClickAdd($event: any): void {
const oldValue =this.control?.value ?? ' ';
this.control.patchValue(oldValue + `{{product.${($event?.value).toLowerCase()}}}`);
this.convertControlValueToHtml();
}
checkChange(event: any) {
const keyCode = event.keyCode;
if ((keyCode < 48 || keyCode > 57) && (keyCode < 65 || keyCode > 90 ) && (keyCode < 186 || keyCode > 192 ) && (keyCode < 219 || keyCode > 222 ) && keyCode !== 32 && keyCode !== 8) {
return;
}
this.generateControlText();
}
generateControlText() {
const text = this.divEditable.nativeElement.innerHTML;
const rawValue = text.toLowerCase();
let filteredValue;
filteredValue = rawValue.replaceAll(' ', '');
filteredValue = filteredValue.replaceAll('<br>', ' ');
filteredValue = filteredValue.replaceAll('<span class="chip" contenteditable="false">', '{{product.');
filteredValue = filteredValue.replaceAll(`<i class="k-icon k-i-close-circle"></i></span> `, '}}');
this.control.patchValue(filteredValue);
return filteredValue;
}
public removeChip({target}): void {
if(target && target.nodeName ==='I') {
target.parentNode.remove();
}
return;
}
convertControlValueToHtml(): void {
const controlValue = this.control?.value;
if (!controlValue) {
return;
}
let filteredValue;
filteredValue = controlValue.replaceAll('{{product.', ' <span class="chip" contenteditable="false">');
filteredValue = filteredValue.replaceAll('}}',`<i class="k-icon k-i-close-circle"></i></span> `);
this.divEditable.nativeElement.innerHTML = filteredValue;
return;
}
ngOnDestroy(): void {
this.unSubscribe$.next();
this.unSubscribe$.complete();
this.clickListener();
}
}
chip-textbox.component.scss
.textAreaContainer {
display: flex;
flex-direction: row;
}
[contenteditable=true]:empty:before {
content: attr(placeholder);
pointer-events: none;
display: block;
/* For Firefox */
}
::ng-deep .inputArea {
margin: 0;
padding: 0.12rem 0.75rem;
line-height: 48px;
width: 100%;
min-width: 0;
border: 1px solid lightgray;
border-radius: 5px;
box-sizing: border-box;
box-shadow: none;
color: inherit;
background: none;
font: inherit;
display: flex;
flex-flow: row nowrap;
align-items: center;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
-webkit-appearance: none;
.chip{
margin: 0 !important;
display: inline-block;
line-height: 22px;
.k-i-close-circle{
margin: 0 2px;
cursor: pointer;
&:hover{
cursor: pointer;
}
}
}
&:focus-within {
background-color: white;
border-color: #80bdff;
}
}
.dropDownButton {
margin-left: 10px;
}
tada! Danke schon!
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Srinath Kamath |


