DEV Community

Dhilip kumar
Dhilip kumar

Posted on

Cursor Imitator

So what are we going to build today?

We are going to Imitate the user typing effect using React in this project.

Our final output will look like the one below:

Project Outline:

  • Component takes an array of description as an input.
  • Component will have a Blinking text Cursor('|').
  • Text starts appearing in the screen with the specified delay.
  • Cursor moves along with the types text.
  • Cursor blinks a while at the end of the sentence.
  • No cursor Blink for last sentence's end.


import React from 'react';
import PropTypes from 'prop-types';

class CursorImitator extends React.Component {

  static defaultProps = {
    content: [],
    nextLetterTime: 50,
    waitAtEnd: 500,
    waitAtStart: 200,
    blinkSpeed: 130,
    letterWrapClass: ''
  }

  constructor(props) {
    super(props);
    this.loadIntro = this.loadIntro.bind(this);
    this.loadCursorBlink = this.loadCursorBlink.bind(this);
    this.clearAllTimeoutsAndIntervals = this.clearAllTimeoutsAndIntervals.bind(this);
    this.state = {
      mounted: false
    };
    this.ttw = 0;             // Time to Wait.
    this.timeoutList = [];    // List of all timeouts triggered.
  }

  componentDidMount() {
    this.setState({ mounted: true });
  }

  loadIntro(content) {
    const contentLast = content.length - 1;
    return(
      <div>
        {
          content.map((sentence, index) => {
            const refName = 'line_' + index;
            const isLast = contentLast === index;
            return (
              <div ref={refName} className={this.props.letterWrapClass}>
                {this.state.mounted && this.loadLetter(sentence, refName, isLast)}
              </div>
            )
          })
        }
      </div>
    )
  }

  render() {
    const {content, className, style} = this.props;
    return (
      <div className={className} style={style}>
        {this.loadIntro(content)}
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

So what have we done so far?

  • defaultProps has initial values for Properties if not provided.
    • content - The array of description. Each index stores a sentence to appear.
    • nextLetterTime - Time before next letter appears.
    • waitAtEnd - Time to wait at the end of each sentence.
    • waitAtStart - Time to wait before the start of each sentence.
    • blinkSpeed - Time for cursor to appear and reappear[blink].
    • letterWrapClass - class for the<div> wrapping the sentence.
  • constructor()
    • this.ttw - time to wait is an instance variable since it has to be persisted.
    • this.timeoutList - List of all timeouts triggered so that we can clear when required.
  • render() - loads loadIntro()
  • loadIntro() - For each sentence we iterate and load letter but only if the component has been mounted. This is because we need refs to each sentence and they will get created only after the component being mounted. We are making use of refs to each sentence's div to update it rather than having it as a state variable.

Up Next: Loading Letter by Letter

loadLetter(sentence, refName, isLastSentence) {
    /* To retain content when accessed from within setTimeout */
    let sentenceLength = sentence.length;
    sentence.split('').forEach((letter, index) => {
      let nextLetter = letter;
      let ttw = this.ttw++;
      let reference = refName;
      const {nextLetterTime, waitAtEnd, waitAtStart} = this.props;
      let self = this;
      let currIndex = index;
      /* To create a Blink at the start of every Sentence */
      if (index === 0) {
        this.loadCursorBlink(self.refs[reference], this.ttw);
        this.ttw = this.ttw + (waitAtStart / nextLetterTime);
        ttw = this.ttw;
      }
      const nextTimeout = setTimeout(() => {
        if (self.interval) {
          clearInterval(self.interval);       // Clear any previous Intervals and removing blink
        }
        if (currIndex === 0 && self.refs && self.refs[reference]) { // Adding '|' in the beginning of every sentence and  inserting incoming texts before that
          self.refs[reference].innerText = '|';
        }
        if (nextLetter === ' ' && self.refs && self.refs[reference]) {  // Handling space
          return self.refs[reference].innerHTML = self.refs[reference].innerHTML.substring(0, self.refs[reference].innerHTML.length - 1) + ' |';
        } else if (self.refs && self.refs[reference]) {                 // adding next digit
          return self.refs[reference].innerText = self.refs[reference].innerText.substring(0,self.refs[reference].innerText.length - 1) + nextLetter + '|';
        }
      }, ttw * nextLetterTime);  // incremented value for every sentence
      this.timeoutList.push(nextTimeout); // To clear it all at once if required
      if (index === sentenceLength - 1) {
        /* To create a Blink at the End of every Sentence */
        this.loadCursorBlink(this.refs[reference], this.ttw, true, isLastSentence);
        this.ttw = this.ttw + (waitAtEnd / nextLetterTime);
      }
    })
  }

Enter fullscreen mode Exit fullscreen mode

loadLetter() accepts 3 arguments.

  • sentence is the sentence that appears on a seperate line.
  • refName gets the reference to the div on which it should load the content.
  • islastSentence is used to avoid loading blinking cursor at the end.

Here we should be careful about Closures as we will be usingsettimeout for each and every letter the parent scope should not be missed. So we use let andconst to keep them bound tosetTimeout.

this.ttw = this.ttw + (waitAtStart / nextLetterTime);
Enter fullscreen mode Exit fullscreen mode
  • waitAtStart/nextLetterTime gives the number of iteration after which the next letter has to appear.
  • We increment this.ttw for every letter. Each letter's time to appear is a multiple of its position inthis.ttwandnextLetterTime
  • index === 0 check is done because whenever it is a new sentence we make,the cursor should blink at the beginning for a while. And we calculate the newthis.ttw and it should be after blink time expired.

  • nextTimeout holds the currently triggered timeout which triggers after the required time elapses.And it is pushed intothis.timeoutList to be able to clear later.

    • Here we clear any previousself.interval if present, this makes sure blinking is not happening.this.interval holds the blinking interval that is created.
    • currIndex === 0 check is done to add '|' at the beginning of every sentence and inserting incoming letter before that.
  • The Last check is done if we reached the end of the sentence.If yes we can blink cursor.

loadCursorBlink(ref, ttw, end, isLastSentence) {
  let reference = ref;
  let self = this;
  let isEnd = end;
  const {nextLetterTime, blinkSpeed} = this.props;
  const nextTimeOut = setTimeout(() => {
    if (self.interval) {
      clearInterval(self.interval);
      // 'self.lastReference' stores last shown sentence's reference, we remove the '|' symbol before creating a new interval
      if (self.lastReference && self.lastReference.innerText.substring(self.lastReference.innerText.length-1) === '|') {
        self.lastReference.innerText = self.lastReference.innerText.substring(0, self.lastReference.innerText.length - 1);
      }
    }
    if (!isLastSentence) {
      self.interval = setInterval(() => {
        self.lastReference = reference;
        if (isEnd) {
          if (reference.innerText.substring(reference.innerText.length - 1) === '|') {
            reference.innerText = reference.innerText.substring(0, reference.innerText.length - 1);
          } else if (reference.innerText.substring(reference.innerText.length - 1) !== '|') {
            reference.innerText = reference.innerText + '|';
          }
        } else {
          if (reference.innerText === '|') {
            reference.innerText = '';
          } else if (reference.innerText === '') {
            reference.innerText = '|';
          }
        }
      }, blinkSpeed);
    }
  }, ttw * nextLetterTime);
  this.timeoutList.push(nextTimeOut);
}

Enter fullscreen mode Exit fullscreen mode

loadCursorBlink() takes in 4 args. a reference to the div, time to wait , end of the sentence and if it is a last sentence.

  • setTimeout for this method hold the time after which the blink appears and disappears.
  • nextTimeout holds the currently triggered timeout which triggers after the required time elapses.And it is pushed intothis.timeoutList to be able to clear later.
    • Here we clear any previous interval if present &self.lastReference stores last shown sentence's reference, we remove the '|' symbol before creating a new interval.
  • If it is not the last sentence then we initiate the interval to create a blink as per givenblinkSpeed.
  • We handle blink at End of all letters in a sentence and the beginning of the first letter in the sentence.

componentWillUnmount() {
  this.clearAllTimeoutsAndIntervals()
}

clearAllTimeoutsAndIntervals() {
  if (this.timeoutList) {
    this.timeoutList.forEach((timeout) => {
      clearTimeout(timeout);
    });
  }
  clearInterval(this.interval);
}
Enter fullscreen mode Exit fullscreen mode

clearAllTimeoutsAndIntervals() helps in clearing all interval if the component if it is unmounted before all triggered timeouts get initiated.

View the entire code here

Thats All Folks!!!

Top comments (2)

Collapse
 
k2t0f12d profile image
Bryan Baldwin

Why stop there? Just reimplement the VT100 and call it done.

Collapse
 
dhilipkmr profile image
Dhilip kumar

😅😅😅