Appearance
TextArea Component Code
Dependencies
This component requires:
- Vue 3 with Composition API
- BasicInput component for the input wrapper
Full Component Code
vue
<script setup>
import BasicInput from './BasicInput.vue'; // Import the basic input component
// Import necessary Vue features
import { ref, useAttrs, onMounted, watch, nextTick, computed } from 'vue';
// Define emitted events for the component
const emit = defineEmits([
'prepend:click', // Event emitted when the prepend button is clicked
'prependInner:click', // Event emitted when the inner prepend button is clicked
'clear:click', // Event emitted when the clear button is clicked
'append:click', // Event emitted when the append button is clicked
'appendInner:click', // Event emitted when the inner append button is clicked
'update:modelValue', // Event for v-model binding
'validate', // Event emitted for validation results
// Form events
'focus', // Event emitted on input focus
'blur', // Event emitted on input blur
'input', // Event emitted on input change
'change', // Event emitted on input change
// Keyboard events
'keydown', // Event emitted on key down
'keyup', // Event emitted on key up
'keypress', // Event emitted on key press
// Mouse events
'click', // Event emitted on mouse click
'dblclick', // Event emitted on double click
'mousedown', // Event emitted on mouse down
'mouseup', // Event emitted on mouse up
'mouseenter', // Event emitted on mouse enter
'mouseleave', // Event emitted on mouse leave
// Clipboard events
'copy', // Event emitted on copy action
'cut', // Event emitted on cut action
'paste', // Event emitted on paste action
// Composition events for languages with complex input
'compositionstart', // Event emitted when composition starts
'compositionupdate', // Event emitted during composition
'compositionend', // Event emitted when composition ends
// Drag events
'dragenter', // Event emitted when dragging enters the component
'dragover', // Event emitted when dragging over the component
'dragleave', // Event emitted when dragging leaves the component
'drop', // Event emitted when an item is dropped
]);
// Define component props
const props = defineProps({
rows: { type: Number, required: false, default: 3 },
noResize: { type: Boolean, required: false },
autoGrow: { type: Boolean, required: false, default: false },
maxlength: { type: Number, required: false },
minRows: { type: Number, required: false, default: 1 },
counter: { type: Boolean, required: false, default: false },
});
// Use attributes passed to the component
const attrs = useAttrs();
const textareaRef = ref(null);
function autoResize() {
if (props.autoGrow && textareaRef.value) {
textareaRef.value.style.height = 'auto';
textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px';
}
}
</script>
<template>
<BasicInput
class="default-text-area-wrapper"
v-bind="attrs"
@input="(e) => emit('input', e)"
@change="(e) => emit('change', e)"
@focus="(e) => emit('focus', e)"
@blur="(e) => emit('blur', e)"
@keydown="(e) => emit('keydown', e)"
@keyup="(e) => emit('keyup', e)"
@keypress="(e) => emit('keypress', e)"
@click="(e) => emit('click', e)"
@dblclick="(e) => emit('dblclick', e)"
@mousedown="(e) => emit('mousedown', e)"
@mouseup="(e) => emit('mouseup', e)"
@mouseenter="(e) => emit('mouseenter', e)"
@mouseleave="(e) => emit('mouseleave', e)"
@copy="(e) => emit('copy', e)"
@cut="(e) => emit('cut', e)"
@paste="(e) => emit('paste', e)"
@compositionstart="(e) => emit('compositionstart', e)"
@compositionupdate="(e) => emit('compositionupdate', e)"
@compositionend="(e) => emit('compositionend', e)"
@dragenter="(e) => emit('dragenter', e)"
@dragover="(e) => emit('dragover', e)"
@dragleave="(e) => emit('dragleave', e)"
@drop="(e) => emit('drop', e)"
@prepend:click="(e) => emit('prepend:click', e)"
@prependInner:click="(e) => emit('prependInner:click', e)"
@clear:click="(e) => emit('clear:click', e)"
@append:click="(e) => emit('append:click', e)"
@appendInner:click="(e) => emit('appendInner:click', e)"
@update:modelValue="(e) => emit('update:modelValue', e)"
@validate="(e) => emit('validate', e)"
>
<!-- Forward all other slots -->
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps" />
</template>
<template
#input-field="{ props: { internalType, readonly, disabled, placeholder, attrs, internalValue, triggerEvent } }"
>
<textarea
ref="textareaRef"
class="text-area-field"
:class="{ 'no-resize': noResize }"
name=""
id=""
:type="internalType"
:readonly="readonly"
:disabled="disabled"
:placeholder="placeholder"
:rows="rows || minRows"
:maxlength="maxlength"
v-bind="attrs"
:value="internalValue"
@input="
(e) => {
triggerEvent('input', e);
autoResize();
}
"
@change="triggerEvent('change', $event)"
@focus="triggerEvent('focus', $event)"
@blur="triggerEvent('blur', $event)"
@keydown="triggerEvent('keydown', $event)"
@click="triggerEvent('click', $event)"
@dblclick="triggerEvent('dblclick', $event)"
@mousedown="triggerEvent('mousedown', $event)"
@mouseup="triggerEvent('mouseup', $event)"
@mouseenter="triggerEvent('mouseenter', $event)"
@mouseleave="triggerEvent('mouseleave', $event)"
@keyup="triggerEvent('keyup', $event)"
@keypress="triggerEvent('keypress', $event)"
@copy="triggerEvent('copy', $event)"
@cut="triggerEvent('cut', $event)"
@paste="triggerEvent('paste', $event)"
@compositionstart="triggerEvent('compositionstart', $event)"
@compositionupdate="triggerEvent('compositionupdate', $event)"
@compositionend="triggerEvent('compositionend', $event)"
@dragenter="triggerEvent('dragenter', $event)"
@dragover="triggerEvent('dragover', $event)"
@dragleave="triggerEvent('dragleave', $event)"
@drop="triggerEvent('drop', $event)"
></textarea>
</template>
<template
#details-right-content="{ props: { internalValue, hint, error, errorMessage, persistentDetails, focused } }"
>
<span v-show="counter" class="char-counter" :class="{ persistentDetails, focused }">
{{ internalValue?.length }}
<template v-if="maxlength"> / {{ maxlength }} </template>
</span>
</template>
</BasicInput>
</template>
<style lang="scss" scoped>
.default-text-area-wrapper {
textarea {
position: relative; /* Relative positioning */
z-index: 1; /* Layering */
background: transparent; /* Transparent background */
margin: 0.875rem 0 0 0; /* No margin */
border: none; /* No border */
outline: 0; /* No outline */
padding: 0 0; /* Padding */
width: 100%; /* Full width */
resize: vertical; /* Allow vertical resizing */
min-height: 1.5rem;
&::-webkit-scrollbar {
width: initial;
height: initial;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
&::-webkit-scrollbar-thumb {
background: #888;
cursor: default;
&:hover {
background: #555;
}
}
&.no-resize {
resize: none; /* Only disable resize if noResize is true */
}
&:focus {
outline: none; /* No outline on focus */
}
}
.char-counter {
opacity: 0; /* Hidden by default */
visibility: hidden; /* Hidden by default */
font-size: 0.75rem; /* Font size */
min-height: 0.875rem; /* Minimum height */
min-width: 0.0625rem; /* Minimum width */
position: relative; /* Relative positioning */
transform: translateY(-100%); /* Move up */
transition: all 0.2s ease-in-out; /* Smooth transition */
&.focused,
&.persistentDetails {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
}
}
</style>Implementation Details
Component Structure
The OTextArea component is built on top of the BasicInput component, which provides the base input functionality, validation, and styling. The OTextArea component adds textarea-specific features:
Auto-Growing Functionality: The
autoResize()function adjusts the height of the textarea based on its content when theautoGrowprop is enabled.Character Counter: A character counter is displayed when the
counterprop is enabled, showing the current character count and optional maximum length.Customizable Resizing: The
noResizeprop allows disabling the manual resizing handle of the textarea.Event Forwarding: All events from the BasicInput component are forwarded to the parent component.
Slot Forwarding: All slots from the BasicInput component are forwarded to allow customization.
Key Features
Auto-Growing Textarea
The auto-growing functionality is implemented in the autoResize() function:
js
function autoResize() {
if (props.autoGrow && textareaRef.value) {
textareaRef.value.style.height = 'auto';
textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px';
}
}This function is called whenever the user types in the textarea, adjusting its height to fit the content.
Character Counter
The character counter is implemented as a slot in the BasicInput component:
html
<template #details-right-content="{ props: { internalValue, hint, error, errorMessage, persistentDetails, focused } }">
<span v-show="counter" class="char-counter" :class="{ persistentDetails, focused }">
{{ internalValue?.length }}
<template v-if="maxlength"> / {{ maxlength }} </template>
</span>
</template>The counter shows the current character count and, if maxlength is set, the maximum allowed characters.
Customizable Styling
The component includes detailed styling for the textarea and character counter, with support for custom scrollbars, focus states, and animations.