From d73c509864e8e605c1786bfab67474f2067700bb Mon Sep 17 00:00:00 2001 From: andrew setterfield Date: Fri, 18 Oct 2024 12:01:17 +0100 Subject: [PATCH 1/4] update character-count.directive.ts to count byte lengh rather then characters --- src/directives/character-count.directive.ts | 37 +++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/directives/character-count.directive.ts b/src/directives/character-count.directive.ts index d594e74b5..8cba0043e 100644 --- a/src/directives/character-count.directive.ts +++ b/src/directives/character-count.directive.ts @@ -51,19 +51,44 @@ export class CharacterCountDirective implements AfterViewInit { } ngAfterViewInit() { - const valueLength = this.el.nativeElement.value ? this.el.nativeElement.value.length : 0; + const valueLength = this.el.nativeElement.value || ''; + const byteLength = this.getUtf8ByteLength(valueLength); if (this.charLimit) { - this.onCharacterCountChanged.emit(this.charLimit - valueLength); + this.onCharacterCountChanged.emit(this.charLimit - byteLength); } } onInput(e: any) { - if (!this.charLimit || e.target.value === undefined) return; - this.onCharacterCountChanged.emit(this.charLimit - e.target.value.length); + const value = e.target.value; + const byteLength = this.getUtf8ByteLength(value); + + if (!this.charLimit || value === undefined) return; + + // Emit the remaining character count minus the byte count of the character entered + this.onCharacterCountChanged.emit(this.charLimit - byteLength); } onIonChange(e: any) { - if (!this.charLimit || e.value === undefined) return; - this.onCharacterCountChanged.emit(this.charLimit - e.value.length); + const value = e.value || ''; + const byteLength = this.getUtf8ByteLength(value); + + if (!this.charLimit || value === undefined) return; + + // Emit the remaining character count minus the byte count of the character entered + this.onCharacterCountChanged.emit(this.charLimit - byteLength); + } + + /** + * Get the byte length of a string + * + * @param input - The string to calculate the byte length of + * @returns The byte length of the input + * + */ + getUtf8ByteLength(input: string): number { + // Using TextEncoder to get the byte length + const encoder = new TextEncoder(); + const encoded = encoder.encode(input); + return encoded.length; } } From 7ba1c7a47a967b217d8415af7f9c4cadf2a6476b Mon Sep 17 00:00:00 2001 From: andrew setterfield Date: Wed, 23 Oct 2024 08:59:52 +0100 Subject: [PATCH 2/4] WIP --- .../__tests__/character-count.directive.spec.ts | 6 +++--- src/directives/character-count.directive.ts | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/directives/__tests__/character-count.directive.spec.ts b/src/directives/__tests__/character-count.directive.spec.ts index 15a93363a..f6734f83d 100644 --- a/src/directives/__tests__/character-count.directive.spec.ts +++ b/src/directives/__tests__/character-count.directive.spec.ts @@ -18,7 +18,7 @@ class ElementRefMock extends ElementRef { }; } -describe('Directive: CharacterCountDirective', () => { +fdescribe('Directive: CharacterCountDirective', () => { let fixture: ComponentFixture; let directiveEl: DebugElement; let directiveInstance: CharacterCountDirective; @@ -37,8 +37,8 @@ describe('Directive: CharacterCountDirective', () => { expect(directiveEl).not.toBeNull(); }); - describe('onIonChange', () => { - it('should emit onCharacterCountChanged if ' + 'charLimit is true and e.value is not undefined', () => { + fdescribe('onIonChange', () => { + it('should emit onCharacterCountChanged if charLimit is true and e.value is not undefined', () => { directiveInstance['charLimit'] = 3; directiveInstance.onIonChange({ value: '11' }); expect(directiveInstance.onCharacterCountChanged.emit).toHaveBeenCalledWith(1); diff --git a/src/directives/character-count.directive.ts b/src/directives/character-count.directive.ts index 8cba0043e..3c448cbf9 100644 --- a/src/directives/character-count.directive.ts +++ b/src/directives/character-count.directive.ts @@ -51,7 +51,7 @@ export class CharacterCountDirective implements AfterViewInit { } ngAfterViewInit() { - const valueLength = this.el.nativeElement.value || ''; + const valueLength = this.el.nativeElement.value || 0; const byteLength = this.getUtf8ByteLength(valueLength); if (this.charLimit) { this.onCharacterCountChanged.emit(this.charLimit - byteLength); @@ -69,10 +69,14 @@ export class CharacterCountDirective implements AfterViewInit { } onIonChange(e: any) { - const value = e.value || ''; - const byteLength = this.getUtf8ByteLength(value); + console.log('e', e); + const valueLength = this.el.nativeElement.value ? this.el.nativeElement.value.length : 0; + const byteLength = this.getUtf8ByteLength(valueLength); - if (!this.charLimit || value === undefined) return; + console.log('valueLength', valueLength); + console.log('byteLength', byteLength); + + if (!this.charLimit || valueLength === undefined) return; // Emit the remaining character count minus the byte count of the character entered this.onCharacterCountChanged.emit(this.charLimit - byteLength); From 7697b96f0058893bdad002fef5f292f2b0001e3b Mon Sep 17 00:00:00 2001 From: andrew setterfield Date: Fri, 25 Oct 2024 14:45:22 +0100 Subject: [PATCH 3/4] re-write directive to take account of byte length for input --- .../character-count.directive.spec.ts | 97 +++++++++++++------ src/directives/character-count.directive.ts | 67 ++++++++----- 2 files changed, 111 insertions(+), 53 deletions(-) diff --git a/src/directives/__tests__/character-count.directive.spec.ts b/src/directives/__tests__/character-count.directive.spec.ts index f6734f83d..34ebcc16b 100644 --- a/src/directives/__tests__/character-count.directive.spec.ts +++ b/src/directives/__tests__/character-count.directive.spec.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line max-classes-per-file import { Component, DebugElement, ElementRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -11,14 +10,13 @@ class TestCharCountComponent {} class ElementRefMock extends ElementRef { nativeElement = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars getAttribute(qualifiedName: string): string | null { return '10'; }, }; } -fdescribe('Directive: CharacterCountDirective', () => { +describe('Directive: CharacterCountDirective', () => { let fixture: ComponentFixture; let directiveEl: DebugElement; let directiveInstance: CharacterCountDirective; @@ -37,48 +35,87 @@ fdescribe('Directive: CharacterCountDirective', () => { expect(directiveEl).not.toBeNull(); }); - fdescribe('onIonChange', () => { - it('should emit onCharacterCountChanged if charLimit is true and e.value is not undefined', () => { + describe('ngAfterViewInit', () => { + it('should emit onCharacterCountChanged if charLimit is true', () => { directiveInstance['charLimit'] = 3; - directiveInstance.onIonChange({ value: '11' }); + directiveInstance.el.nativeElement.value = '11'; + directiveInstance.ngAfterViewInit(); expect(directiveInstance.onCharacterCountChanged.emit).toHaveBeenCalledWith(1); }); - it('should return undefined if charLimit is false and e.value is not undefined', () => { - directiveInstance['charLimit'] = null; - directiveInstance.onIonChange({ value: '11' }); - expect(directiveInstance.onCharacterCountChanged.emit).not.toHaveBeenCalled(); + }); + + describe('onInput', () => { + it('should call handleChange with the correct value', () => { + spyOn(directiveInstance, 'handleChange'); + directiveInstance.onInput({ target: { value: 'hello' } }); + expect(directiveInstance.handleChange).toHaveBeenCalledWith('hello'); }); - it('should return undefined if charLimit is true and e.value is undefined', () => { - directiveInstance['charLimit'] = 3; - directiveInstance.onIonChange({ value: undefined }); - expect(directiveInstance.onCharacterCountChanged.emit).not.toHaveBeenCalled(); + }); + + describe('onIonChange', () => { + it('should call handleChange with the correct value', () => { + spyOn(directiveInstance, 'handleChange'); + directiveInstance.onIonChange({ target: { value: 'hello' } }); + expect(directiveInstance.handleChange).toHaveBeenCalledWith('hello'); }); }); - describe('onInput', () => { - it('should emit onCharacterCountChanged if ' + 'charLimit is true and e.target.value is not undefined', () => { - directiveInstance['charLimit'] = 3; - directiveInstance.onInput({ target: { value: '11' } }); - expect(directiveInstance.onCharacterCountChanged.emit).toHaveBeenCalledWith(1); + describe('handleChange', () => { + it('should emit remaining character count when charLimit is set and value is provided', () => { + directiveInstance['charLimit'] = 10; + directiveInstance.handleChange('hello'); + expect(directiveInstance.onCharacterCountChanged.emit).toHaveBeenCalledWith(5); }); - it('should return undefined if charLimit is false and e.target.value is not undefined', () => { + + it('should not emit when charLimit is not set', () => { directiveInstance['charLimit'] = null; - directiveInstance.onInput({ target: { value: '11' } }); + directiveInstance.handleChange('hello'); expect(directiveInstance.onCharacterCountChanged.emit).not.toHaveBeenCalled(); }); - it('should return undefined if charLimit is true and e.target.value is undefined', () => { - directiveInstance['charLimit'] = 3; - directiveInstance.onInput({ target: { value: undefined } }); + + it('should not emit when value is undefined', () => { + directiveInstance['charLimit'] = 10; + directiveInstance.handleChange(undefined); expect(directiveInstance.onCharacterCountChanged.emit).not.toHaveBeenCalled(); }); + + it('should emit remaining character count when input is empty string', () => { + directiveInstance['charLimit'] = 10; + directiveInstance.handleChange(''); + expect(directiveInstance.onCharacterCountChanged.emit).toHaveBeenCalledWith(10); + }); + + it('should emit remaining character count when input is null', () => { + directiveInstance['charLimit'] = 10; + directiveInstance.handleChange(null); + expect(directiveInstance.onCharacterCountChanged.emit).toHaveBeenCalledWith(10); + }); }); - describe('ngAfterViewInit', () => { - it('should emit onCharacterCountChanged if charLimit is true', () => { - directiveInstance['charLimit'] = 3; - directiveInstance.el.nativeElement.value = '11'; - directiveInstance.ngAfterViewInit(); - expect(directiveInstance.onCharacterCountChanged.emit).toHaveBeenCalledWith(1); + describe('getUtf8ByteLength', () => { + it('should return correct byte length for ASCII characters', () => { + const result = directiveInstance.getUtf8ByteLength('hello'); + expect(result).toBe(5); + }); + + it('should return correct byte length for multi-byte characters', () => { + const result = directiveInstance.getUtf8ByteLength('你好'); + expect(result).toBe(6); + }); + + it('should return 0 for an empty string', () => { + const result = directiveInstance.getUtf8ByteLength(''); + expect(result).toBe(0); + }); + + it('should return correct byte length for mixed characters', () => { + const result = directiveInstance.getUtf8ByteLength('hello你好'); + expect(result).toBe(11); + }); + + it('returns correct byte length for special characters', () => { + const result = directiveInstance.getUtf8ByteLength('!@#$%^&*()'); + expect(result).toBe(10); }); }); }); diff --git a/src/directives/character-count.directive.ts b/src/directives/character-count.directive.ts index 3c448cbf9..a025abc52 100644 --- a/src/directives/character-count.directive.ts +++ b/src/directives/character-count.directive.ts @@ -50,36 +50,58 @@ export class CharacterCountDirective implements AfterViewInit { this.charLimit = this.el.nativeElement.getAttribute('charLimit'); } + + /** + * Lifecycle hook that is called after a component's view has been fully initialized. + * + * This method calculates the remaining character count based on the initial value + * of the input field and emits the result through the `onCharacterCountChanged` event. + */ ngAfterViewInit() { - const valueLength = this.el.nativeElement.value || 0; + const valueLength = this.el.nativeElement.value || ''; const byteLength = this.getUtf8ByteLength(valueLength); if (this.charLimit) { this.onCharacterCountChanged.emit(this.charLimit - byteLength); } } - onInput(e: any) { - const value = e.target.value; - const byteLength = this.getUtf8ByteLength(value); - - if (!this.charLimit || value === undefined) return; - - // Emit the remaining character count minus the byte count of the character entered - this.onCharacterCountChanged.emit(this.charLimit - byteLength); + /** + * Handles the input event for the input field. + * + * This method is triggered when the input event occurs on the input field. + * It delegates the handling of the event to the handleChange method. + * + * @param event - The event object containing the new value of the input field. + */ + onInput(event: any) { + this.handleChange(event.target.value); } - onIonChange(e: any) { - console.log('e', e); - const valueLength = this.el.nativeElement.value ? this.el.nativeElement.value.length : 0; - const byteLength = this.getUtf8ByteLength(valueLength); - - console.log('valueLength', valueLength); - console.log('byteLength', byteLength); - - if (!this.charLimit || valueLength === undefined) return; + /** + * Handles the ionChange event for the input field. + * + * This method is triggered when the ionChange event occurs on the input field. + * It delegates the handling of the event to the handleChange method. + * + * @param event - The event object containing the new value of the input field. + */ + onIonChange(event: any) { + this.handleChange(event.target.value); + } - // Emit the remaining character count minus the byte count of the character entered - this.onCharacterCountChanged.emit(this.charLimit - byteLength); + /** + * Handles the change event for the input field. + * + * This method calculates the remaining character count based on the UTF-8 byte length + * of the input value and emits the result through the `onCharacterCountChanged` event. + * + * @param value - The current value of the input field. + */ + handleChange(value: string) { + if (this.charLimit !== null && value !== undefined) { + const byteLength = this.getUtf8ByteLength(value); + this.onCharacterCountChanged.emit(this.charLimit - byteLength); + } } /** @@ -90,9 +112,8 @@ export class CharacterCountDirective implements AfterViewInit { * */ getUtf8ByteLength(input: string): number { + if (input === null) return 0; // Using TextEncoder to get the byte length - const encoder = new TextEncoder(); - const encoded = encoder.encode(input); - return encoded.length; + return new TextEncoder().encode(input).length; } } From 27746c6cac2e8f6308a847db895139d29140501b Mon Sep 17 00:00:00 2001 From: andrew setterfield Date: Fri, 25 Oct 2024 14:47:18 +0100 Subject: [PATCH 4/4] re-write directive to take account of byte length for input --- src/directives/character-count.directive.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/directives/character-count.directive.ts b/src/directives/character-count.directive.ts index a025abc52..7f8d48144 100644 --- a/src/directives/character-count.directive.ts +++ b/src/directives/character-count.directive.ts @@ -50,7 +50,6 @@ export class CharacterCountDirective implements AfterViewInit { this.charLimit = this.el.nativeElement.getAttribute('charLimit'); } - /** * Lifecycle hook that is called after a component's view has been fully initialized. *