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.