Skip to content

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:

  1. Auto-Growing Functionality: The autoResize() function adjusts the height of the textarea based on its content when the autoGrow prop is enabled.

  2. Character Counter: A character counter is displayed when the counter prop is enabled, showing the current character count and optional maximum length.

  3. Customizable Resizing: The noResize prop allows disabling the manual resizing handle of the textarea.

  4. Event Forwarding: All events from the BasicInput component are forwarded to the parent component.

  5. 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.