Skip to content

IBX-10307: Form Control - Input Text #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/bundle/Resources/public/ts/components/HelperText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Base from '../shared/Base';

type IconsTypes = 'default' | 'error';

export default class HelperText extends Base {
private _iconWrapper: HTMLDivElement;
private _contentWrapper: HTMLDivElement;
private _iconsTemplates: Record<IconsTypes, Node | null> = {
default: null,
error: null,
};

private _error = false;
private _message = '';
private _defaultMessage: string;

constructor(container: HTMLElement) {
super(container);

const iconWrapper = container.querySelector<HTMLDivElement>('.ids-helper-text__icon-wrapper');
const contentWrapper = container.querySelector<HTMLDivElement>('.ids-helper-text__content-wrapper');

if (!iconWrapper || !contentWrapper) {
throw new Error('HelperText: Required elements are missing in the container.');
}

this._iconWrapper = iconWrapper;
this._contentWrapper = contentWrapper;
this._defaultMessage = contentWrapper.textContent ?? '';

const defaultIconTemplate = iconWrapper.querySelector<HTMLTemplateElement>(
'.ids-helper-text__icon-template[data-ids-type="default"]',
);
const errorIconTemplate = iconWrapper.querySelector<HTMLTemplateElement>('.ids-helper-text__icon-template[data-ids-type="error"]');

this._iconsTemplates = {
default: defaultIconTemplate?.content.cloneNode(true) ?? null,
error: errorIconTemplate?.content.cloneNode(true) ?? null,
};
}

set defaultMessage(value: string) {
this._defaultMessage = value;
}

set error(value: boolean) {
if (this._error === value) {
return;
}

this._error = value;

this.container.classList.toggle('ids-helper-text--error', value);

const iconElement = this._iconWrapper.querySelector('.ids-helper-text__icon');

if (!iconElement) {
return;
}

const replacementIcon = value ? this._iconsTemplates.error : this._iconsTemplates.default;

if (!replacementIcon) {
throw new Error(`HelperText: Icon template for type "${value ? 'error' : 'default'}" is missing.`);
}

iconElement.replaceWith(replacementIcon.cloneNode(true));
}

set message(value: string) {
if (this._message === value) {
return;
}

this._message = value;

this._contentWrapper.textContent = value;
}

changeToDefaultMessage() {
this.message = this._defaultMessage;
}
}

export type HelperTextType = InstanceType<typeof HelperText>;
16 changes: 16 additions & 0 deletions src/bundle/Resources/public/ts/components/Label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Base from '../shared/Base';

export default class Label extends Base {
private _error = false;

set error(value: boolean) {
this._error = value;
this.container.classList.toggle('ids-label--error', value);
}

get error(): boolean {
return this._error;
}
}

export type LabelType = InstanceType<typeof Label>;
110 changes: 110 additions & 0 deletions src/bundle/Resources/public/ts/components/formControls/InputText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import HelperText, { HelperTextType } from '../HelperText';
import InputText, { InputTextType } from '../inputs/InputText';
import Label, { LabelType } from '../Label';
import Base from '../../shared/Base';

import ValidatorManager, { ValidatorManagerType } from '../../validators/ValidatorManager';
import IsEmptyStringValidator from '../../validators/IsEmptyStringValidator';

export default class FormControlInputText extends Base {
private _labelInstance: LabelType | null = null;
private _inputTextInstance: InputTextType;
private _helperTextInstance: HelperTextType | null = null;
private _validatorManager: ValidatorManagerType;
private _error = false;
private _errorMessage = '';

constructor(container: HTMLDivElement) {
super(container);

const inputTextContainer = container.querySelector<HTMLDivElement>('.ids-input-text');

if (!inputTextContainer) {
throw new Error('FormControlInputText: Required elements are missing in the container.');
}

const labelContainer = container.querySelector<HTMLDivElement>('.ids-label');

if (labelContainer) {
this._labelInstance = new Label(labelContainer);
}

const helperTextContainer = container.querySelector<HTMLDivElement>('.ids-helper-text');

if (helperTextContainer) {
this._helperTextInstance = new HelperText(helperTextContainer);
}

this._inputTextInstance = new InputText(inputTextContainer);
this._validatorManager = new ValidatorManager();

if (this._inputTextInstance.required) {
const isEmptyStringValidator = new IsEmptyStringValidator();

this._validatorManager.addValidator(isEmptyStringValidator);
}
}

set error(value) {
if (this._error === value) {
return;
}

this._error = value;
this._inputTextInstance.error = value;

if (this._labelInstance) {
this._labelInstance.error = value;
}

if (this._helperTextInstance) {
this._helperTextInstance.error = value;
}
}

get error(): boolean {
return this._error;
}

set errorMessage(value: string) {
if (this._errorMessage === value) {
return;
}

this._errorMessage = value;

if (this._error && this._helperTextInstance) {
this._helperTextInstance.message = value;
}

if (!this._error && this._helperTextInstance) {
this._helperTextInstance.changeToDefaultMessage();
}
}

initChildren() {
this._labelInstance?.init();
this._inputTextInstance.init();
this._helperTextInstance?.init();
}

initInputListeners() {
this._inputTextInstance.inputElement.addEventListener('input', ({ currentTarget }) => {
if (!(currentTarget instanceof HTMLInputElement)) {
throw new Error('FormControlInputText: Current target is not an HTMLInputElement.');
}

const validatorData = this._validatorManager.validate(currentTarget.value);

this.error = !validatorData.isValid;
this.errorMessage = validatorData.messages.join(', ');
});
}

init() {
super.init();

this.initChildren();
this.initInputListeners();
}
}
29 changes: 28 additions & 1 deletion src/bundle/Resources/public/ts/components/inputs/InputText.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import Base from '../../shared/Base';

export default class InpuText extends Base {
export { BASE_EVENTS } from '../../shared/Base';

export enum INPUT_TEXT_EVENTS {
CLEARED = 'ids:component:input-text:cleared',
}
export default class InputText extends Base {
private _inputElement: HTMLInputElement;
private _actionsElement: HTMLDivElement;
private _clearBtnElement: HTMLButtonElement;
private _error = false;

constructor(container: HTMLDivElement) {
super(container);
Expand All @@ -21,6 +27,24 @@ export default class InpuText extends Base {
this._clearBtnElement = clearBtnElement;
}

get inputElement(): HTMLInputElement {
return this._inputElement;
}

get required(): boolean {
return this._inputElement.required;
}

set error(value) {
this._inputElement.classList.toggle('ids-input--error', value);

this._error = value;
}

get error(): boolean {
return this._error;
}

private _updateInputPadding() {
const actionsWidth = this._actionsElement.offsetWidth;

Expand Down Expand Up @@ -56,6 +80,7 @@ export default class InpuText extends Base {
event.stopPropagation();

this.changeValue('');
this.container.dispatchEvent(new Event(INPUT_TEXT_EVENTS.CLEARED));
});
}

Expand All @@ -67,3 +92,5 @@ export default class InpuText extends Base {
this._updateInputPadding();
}
}

export type InputTextType = InstanceType<typeof InputText>;
13 changes: 12 additions & 1 deletion src/bundle/Resources/public/ts/init_components.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Accordion from './components/accordion';
import FormControlInputText from './components/formControls/InputText';
import InputText from './components/inputs/InputText';

const accordionContainers = document.querySelectorAll<HTMLDivElement>('.ids-accordion:not([custom-init])');
Expand All @@ -9,10 +10,20 @@ accordionContainers.forEach((accordionContainer: HTMLDivElement) => {
accordionInstance.init();
});

const inputTextContainers = document.querySelectorAll<HTMLDivElement>('.ids-input-text:not([custom-init])');
const inputTextContainers = document.querySelectorAll<HTMLDivElement>('.ids-input-text:not([data-ids-custom-init])');

inputTextContainers.forEach((inputTextContainer: HTMLDivElement) => {
const inputTextInstance = new InputText(inputTextContainer);

inputTextInstance.init();
});

const formControlInputTextContainers = document.querySelectorAll<HTMLDivElement>(
'.ids-form-control--input-text:not([data-ids-custom-init])',
);

formControlInputTextContainers.forEach((inputTextContainer: HTMLDivElement) => {
const inputTextInstance = new FormControlInputText(inputTextContainer);

inputTextInstance.init();
});
14 changes: 11 additions & 3 deletions src/bundle/Resources/public/ts/shared/Base.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { setInstance } from '../helpers/object.instances';

export enum BASE_EVENTS {
INITIALIZED = 'ids:component:initialized',
}

export default abstract class Base {
container: HTMLElement;
private _container: HTMLElement;

constructor(container: HTMLElement) {
this.container = container;
this._container = container;

setInstance(container, this);
}

get container(): HTMLElement {
return this._container;
}

init() {
this.container.dispatchEvent(new CustomEvent('ids:component:initialized', { detail: { component: this } }));
this._container.dispatchEvent(new CustomEvent('ids:component:initialized', { detail: { component: this } }));
}
}
7 changes: 7 additions & 0 deletions src/bundle/Resources/public/ts/validators/BaseValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default abstract class BaseValidator {
abstract getErrorMessage(): string;

abstract validate(_value: unknown): boolean;
}

export type BaseValidatorType = typeof BaseValidator;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import BaseValidator from './BaseValidator';

export default class IsEmptyStringValidator extends BaseValidator {
getErrorMessage(): string {
return /*@Desc("This field cannot be empty.")*/ 'ibexa.validators.is_empty_string'; // TODO: Use translation service when available
}

validate(value: string): boolean {
return value.trim() !== '';
}
}
27 changes: 27 additions & 0 deletions src/bundle/Resources/public/ts/validators/ValidatorManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import BaseValidator from './BaseValidator';

export default class ValidatorManager {
private _validators: BaseValidator[];

constructor(validators: BaseValidator[] = []) {
this._validators = validators;
}

addValidator(validator: BaseValidator): void {
this._validators.push(validator);
}

removeValidator(validator: BaseValidator): void {
this._validators = this._validators.filter((savedValidator) => savedValidator !== validator);
}

validate(value: unknown) {
const errors = this._validators
.filter((validator: BaseValidator) => !validator.validate(value))
.map((validator: BaseValidator) => validator.getErrorMessage());

return { isValid: !errors.length, messages: errors };
}
}

export type ValidatorManagerType = InstanceType<typeof ValidatorManager>;
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
<div class="ids-helper-text__icon-wrapper">
{% block icon_inner %}
<twig:ibexa:Icon :name="icon_name" size="small" class="ids-helper-text__icon" />
<template class="ids-helper-text__icon-template" data-ids-type="default">
<twig:ibexa:Icon name="info-circle" size="small" class="ids-helper-text__icon" />
</template>
<template class="ids-helper-text__icon-template" data-ids-type="error">
<twig:ibexa:Icon name="alert-error" size="small" class="ids-helper-text__icon" />
</template>
{{ icon_text|default('') }}
{% endblock icon_inner %}
</div>
{% endblock icon_outer %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends '@IbexaDesignSystemTwig/themes/standard/design_system/partials/base_form_control.html.twig' %}

{% block content %}
<twig:ibexa:inputs:InputText {{ ...input }} />
{% endblock content %}
Loading
Loading