Obtain selection start, end, and selection value from Material UI's TextField

Viewed 829

I need to obtain text selection data from Material-ui's TextField. Text selection data must include selection start, selection end, and the selected text.

I managed to get as far as the following using createRefs

import React, { createRef } from "react";
import { TextField, Button, Box, Grid } from "@material-ui/core";

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selStart: undefined,
      selEnd: undefined,
      selValue: undefined
    };
    this.onSaveSelectionClicked = this.onSaveSelectionClicked.bind(this);
    this.textFieldRef = createRef();
  }

  onSaveSelectionClicked(ev) {
    // Somehow obtain selection start, end, and value
    // then update state
    const { selectionStart, selectionEnd, value } = this.textFieldRef.current;
    this.setState({
      selStart: selectionStart,
      selEnd: selectionEnd,
      selValue: value.substring(selectionStart, selectionEnd)
    });
  }

  render() {
    return (
      <Box p="2em">
        <Grid container spacing={2}>
          <Grid item xs={12}>
            <TextField
              name="essay1"
              label="Essay 1"
              multiline
              rows={3}
              fullWidth
              inputRef={this.textFieldRef}
            />
          </Grid>
          <Grid item xs={12}>
            <TextField
              name="essay2"
              label="Essay 2"
              multiline
              rows={3}
              fullWidth
              inputRef={this.textFieldRef}
            />
          </Grid>
          <Grid item xs={12}>
            <Button variant="contained" onClick={this.onSaveSelectionClicked}>
              Save selection
            </Button>
          </Grid>
          <Grid item xs={12}>
            <pre>{JSON.stringify(this.state)}</pre>
          </Grid>
        </Grid>
      </Box>
    );
  }
}

Which gives me the following

enter image description here

The problem is, this doesn't seem to work with multiple TextField. The current property of the refs will always refer to 'Essay 2' text field.

Are there better way to approach this problem?

Here's the link to codesandbox for the above code

2 Answers

No need for refs at all, in this way selection changes dynamically.
Change constructor as follow:

constructor(props) {
    super(props);
    this.state = {
      selStart: undefined,
      selEnd: undefined,
      selValue: undefined
    };
    this.onSelectionChange = this.onSelectionChange.bind(this);
  }

Add handler:

 onSelectionChange(e) {
    const { selectionStart, selectionEnd, value } = e.target;

    this.setState({
      selStart: selectionStart,
      selEnd: selectionEnd,
      selValue: value.substring(selectionStart, selectionEnd)
    });
  }

And then in your textfields Just set event onMouseUp (or onBlur if you want update on lostFocus):

onMouseUp={this.onSelectionChange}

Doing this your source code will be as follow:

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selStart: undefined,
      selEnd: undefined,
      selValue: undefined
    };
    this.onSelectionChange = this.onSelectionChange.bind(this);
  }

  onSelectionChange(e) {
    const { selectionStart, selectionEnd, value } = e.target;

    this.setState({
      selStart: selectionStart,
      selEnd: selectionEnd,
      selValue: value.substring(selectionStart, selectionEnd)
    });
  }

  render() {
    return (
      <Box p="2em">
        <Grid container spacing={2}>
          <Grid item xs={12}>
            <TextField
              name="essay1"
              label="Essay 1"
              multiline
              rows={3}
              fullWidth
              onMouseUp={this.onSelectionChange}
            />
          </Grid>
          <Grid item xs={12}>
            <TextField
              name="essay2"
              label="Essay 2"
              multiline
              rows={3}
              fullWidth
              onMouseUp={this.onSelectionChange}
            />
          </Grid>
        </Grid>
      </Box>
    );
  }
}

You need to create a ref for each dom node you want to access to.

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selStart: undefined,
      selEnd: undefined,
      selValue: undefined
    };
    this.onSaveSelectionClicked = this.onSaveSelectionClicked.bind(this);
    // Don't forget to attach refs to dom nodes accordingly!
    this.firstTextFieldRef = createRef();
    this.secondTextFieldRef = createRef();
  }
  // Rest of your component...
}

Then you can either listen to the focus event of each textArea to know which one was last selected (and therefore contains the selected text) or you can rely on the fact that you cannot select text in two text areas at the same time (which is not completely true).

The focus option is cleaner IMO:

// Using function composition to avoid logic duplication
handleTextFieldFocus = ref => () => {
  this.setState({ lastSelectedField: ref })
}

// Attach to your first text area
handleFirstFieldFocus = handleTextFieldFocus(this.firstTextFieldRef)

// Attach to your second text area
handleSecondFieldFocus = handleTextFieldFocus(this.secondTextFieldRef)

onSaveSelectionClicked() {
  // We are getting the values from the last focused input
  const {
    selectionStart,
    selectionEnd,
    value
  } = this.state.lastSelectedField.current;

  this.setState({
    selStart: selectionStart,
    selEnd: selectionEnd,
    selValue: value.substring(selectionStart, selectionEnd)
  });
}

The second option means checking the values of each ref before setting the state:

onSaveSelectionClicked() {
  let {
    selectionStart,
    selectionEnd,
    value
  } = this.firstTextFieldRef.current;

  // If there is no selected text in the first text field
  // we will assume that there must be on the second text area
  if (!firstSelectionStart && !firstSelectionEnd) {
    selectionStart = this.secondTextFieldRef.current.selectionStart
    selectionEnd = this.secondTextFieldRef.current.selectionEnd
    value = this.secondTextFieldRef.current.value
  }

  this.setState({
    selStart: selectionStart,
    selEnd: selectionEnd,
    selValue: value.substring(selectionStart, selectionEnd)
  });
}
Related