/**
* DS Input - Web Component
*
* Usage:
*
*
*
*
* Attributes:
* - type: text | password | email | number | search | tel | url
* - placeholder: string
* - value: string
* - label: string
* - error: string
* - disabled: boolean
* - required: boolean
* - icon: string (SVG content or icon name)
*/
class DsInput extends HTMLElement {
static get observedAttributes() {
return ['type', 'placeholder', 'value', 'label', 'error', 'disabled', 'required', 'icon', 'tabindex', 'aria-label', 'aria-invalid', 'aria-describedby'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
disconnectedCallback() {
this.cleanupEventListeners();
}
attributeChangedCallback(name, oldValue, newValue) {
if (this.shadowRoot.innerHTML && oldValue !== newValue) {
if (name === 'value') {
const input = this.shadowRoot.querySelector('input');
if (input && input.value !== newValue) {
input.value = newValue || '';
}
} else {
this.cleanupEventListeners();
this.render();
this.setupEventListeners();
}
}
}
get type() {
return this.getAttribute('type') || 'text';
}
get placeholder() {
return this.getAttribute('placeholder') || '';
}
get value() {
const input = this.shadowRoot?.querySelector('input');
return input ? input.value : (this.getAttribute('value') || '');
}
set value(val) {
this.setAttribute('value', val);
const input = this.shadowRoot?.querySelector('input');
if (input) input.value = val;
}
get label() {
return this.getAttribute('label');
}
get error() {
return this.getAttribute('error');
}
get disabled() {
return this.hasAttribute('disabled');
}
get required() {
return this.hasAttribute('required');
}
get icon() {
return this.getAttribute('icon');
}
setupEventListeners() {
const input = this.shadowRoot.querySelector('input');
if (!input) return;
// Store handler references for cleanup
this.inputHandler = (e) => {
this.dispatchEvent(new CustomEvent('ds-input', {
bubbles: true,
composed: true,
detail: { value: e.target.value }
}));
};
this.changeHandler = (e) => {
this.dispatchEvent(new CustomEvent('ds-change', {
bubbles: true,
composed: true,
detail: { value: e.target.value }
}));
};
this.focusHandler = () => {
this.dispatchEvent(new CustomEvent('ds-focus', {
bubbles: true,
composed: true
}));
};
this.blurHandler = () => {
this.dispatchEvent(new CustomEvent('ds-blur', {
bubbles: true,
composed: true
}));
};
input.addEventListener('input', this.inputHandler);
input.addEventListener('change', this.changeHandler);
input.addEventListener('focus', this.focusHandler);
input.addEventListener('blur', this.blurHandler);
}
cleanupEventListeners() {
const input = this.shadowRoot?.querySelector('input');
if (!input) return;
// Remove all event listeners
if (this.inputHandler) {
input.removeEventListener('input', this.inputHandler);
delete this.inputHandler;
}
if (this.changeHandler) {
input.removeEventListener('change', this.changeHandler);
delete this.changeHandler;
}
if (this.focusHandler) {
input.removeEventListener('focus', this.focusHandler);
delete this.focusHandler;
}
if (this.blurHandler) {
input.removeEventListener('blur', this.blurHandler);
delete this.blurHandler;
}
}
focus() {
this.shadowRoot.querySelector('input')?.focus();
}
blur() {
this.shadowRoot.querySelector('input')?.blur();
}
render() {
const hasIcon = !!this.icon;
const hasError = !!this.error;
const errorClass = hasError ? 'ds-input--error' : '';
const tabindex = this.disabled ? '-1' : (this.getAttribute('tabindex') || '0');
const errorId = hasError ? 'error-' + Math.random().toString(36).substr(2, 9) : '';
// ARIA attributes
const ariaLabel = this.getAttribute('aria-label') || this.label || '';
const ariaInvalid = hasError ? 'aria-invalid="true"' : '';
const ariaDescribedBy = hasError ? `aria-describedby="${errorId}"` : '';
this.shadowRoot.innerHTML = `
${this.label ? `
` : ''}
${hasIcon ? `${this.getIconSVG()}` : ''}
${hasError ? `${this.error}
` : ''}
`;
}
getIconSVG() {
const icons = {
search: ``,
email: ``,
lock: ``,
user: ``,
};
return icons[this.icon] || this.icon || '';
}
}
customElements.define('ds-input', DsInput);
export default DsInput;