DEV Community

Discussion on: Typewriter Component for Vue.js

Collapse
 
theobittencourt profile image
Theo B

Just some fixes and two features:

  1. Blink cursor.
  2. Add interval between words/phrases cycle.
<template>
  <span>
    {{ displayText.join('') }}
    <span class="cursor">|</span>
  </span>
</template>

<script>
export default {
  props: {
    speed: {
      type: Number,
      default: 100,
    },
    deleteSpeed: {
      type: Number,
      default: 30,
    },
    nextWordInterval: {
      type: Number,
      default: 1200
    },
    words: {
      type: Array,
      default: [],
    },
  },
  data() {
    return {
      displayText: [],
      currentWord: '',
      wordIdx: 0,
      timeoutSpeed: null,
      isWaitingNextWord: false,
    }
  },
  mounted() {
    this.start()
  },
  methods: {
    start() {
      if (this.words && this.words.length > 0) {
        this.currentWord = this.words[this.wordIdx].split('')
        this.timeoutSpeed = this.speed
        this.animate = setTimeout(this.type, this.timeoutSpeed)
      }
    },
    type() {
      // if typing...
      if (this.currentWord.length > 0) {
        this.displayText.push(this.currentWord.shift())
        // if done typing, wait for a while
      } else if (!this.isWaitingNextWord && this.currentWord.length === 0 && this.displayText.length === this.words[this.wordIdx].length) {
        this.timeoutSpeed = this.nextWordInterval
        this.isWaitingNextWord = true
        // if done typing, then delete
      } else if (this.currentWord.length === 0 && this.displayText.length > 0) {
        this.timeoutSpeed = this.deleteSpeed
        this.displayText.pop()
        // if done typing & deleting
      } else if (this.currentWord.length === 0 && this.displayText.length === 0) {
        // change words
        if (this.wordIdx < (this.words.length - 1)) {
          this.wordIdx++
        } else {
          // reset
          this.wordIdx = 0
        }

        this.timeoutSpeed = this.speed
        this.isWaitingNextWord = false
        this.currentWord = this.words[this.wordIdx].split('')
        this.displayText.push(this.currentWord.shift())
      }

      setTimeout(this.type, this.timeoutSpeed)
    },
  },
}
</script>

<style lang="scss" scoped>
@keyframes blink-animation {
  to {
    visibility: hidden;
  }
}

.cursor {
  display: inline-block;
  margin-left: -3px;
  animation: blink-animation 1s steps(2, start) infinite;
}
</style>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rachel_cheuk profile image
Rachel

Thanks Theo! I've included your reply above as part of an update!

Collapse
 
imseaworld profile image
SeaWorld • Edited

This was a great addition, I'd also like to contribute some changes I've made.
I put all the main functionality of the loop into individual functions for better state management. I personally loved the addition of the cursor since it was exactly what I was looking for, but I had an issue with the look of the cursor. I wanted it to be exact to what's seen when you actually type. Ex: Typing, cursor is static, stop typing, cursor blinks.

<template>
    <span>
        {{ displayText.join('') }}
        <span :class="{cursor: true, 'cursor--blink': blinkCursor}">|</span>
    </span>
</template>

<script>
export default {
    name: 'TypeWriter',
    props: {
        speed: {
            type: Number,
            default: 100,
        },
        deleteSpeed: {
            type: Number,
            default: 30,
        },
        nextWordInterval: {
            type: Number,
            default: 1200,
        },
        words: {
            type: Array,
            default: [],
        },
    },
    data() {
        return {
            displayText: [],
            currentWord: '',
            wordIdx: 0,
            timeoutSpeed: null,
            isWaitingNextWord: false,
            blinkCursor: false,
        };
    },
    mounted() {
        this.start();
    },
    methods: {
        start() {
            if (this.words && this.words.length > 0) {
                this.currentWord = this.words[this.wordIdx].split('');
                this.timeoutSpeed = this.speed;
                this.animate = setTimeout(this.type, this.timeoutSpeed);
            }
        },
        type() {
            if (this.currentWord.length > 0) {
                this.displayText.push(this.currentWord.shift());
                this.timeoutSpeed = this.speed;
                this.animate = setTimeout(this.type, this.timeoutSpeed);
            } else {
                this.blinkCursor = true;
                this.isWaitingNextWord = true;
                this.timeoutSpeed = this.nextWordInterval;
                this.animate = setTimeout(this.delete, this.timeoutSpeed);
            }
        },
        nextWord() {
            this.isWaitingNextWord = false;
            this.displayText = [];
            if (++this.wordIdx >= this.words.length) {
                this.wordIdx = 0;
            }
            this.blinkCursor = false;
            this.currentWord = this.words[this.wordIdx].split('');
            this.timeoutSpeed = this.speed;
            this.animate = setTimeout(this.type, this.timeoutSpeed);
        },
        delete() {
            if (this.displayText.length > 0) {
                this.blinkCursor = false;
                this.displayText.pop();
                this.timeoutSpeed = this.deleteSpeed;
                this.animate = setTimeout(this.delete, this.timeoutSpeed);
            } else {
                this.blinkCursor = true;
                this.isWaitingNextWord = false;
                this.timeoutSpeed = this.nextWordInterval;
                this.animate = setTimeout(this.nextWord, this.timeoutSpeed);
            }
        },
    },
};
</script>

<style lang="scss" scoped>
@keyframes blink-animation {
    to {
        visibility: hidden;
    }
}

.cursor {
    display: inline-block;
    margin-left: -5px;

    &--blink {
        animation: blink-animation 1s steps(2, start) infinite;
    }
}
</style>
Enter fullscreen mode Exit fullscreen mode