I once saw an article on “What can a One-liner agent Do? Answer:

At that time tried really very fun, so every time can show a wave of operation in front of the sister, in their amazed eyes, MY heart happily smiled — well, and let a person who does not understand the technology found the United States 🐶, cough.

For a long time, I felt that this attribute existed just for the sake of existence, but after receiving the request today, I found that this attribute, which felt useless, was the perfect solution to my need.

demand

The requirements are simple, just add a button to the input field. This function is usually used for mass mail, where the button “name” is actually a variable, and the backend should automatically fill in the name of the real user, and then send the email to the user.

The problem

At first glance, this requirement seems to be accomplished using position: relative + position: Absolute. But think about it, it’s unlikely: the button will surely overwrite the input, and a single delete “name” button would be hard to do.

It is also impossible to do this with

I think another option would be to start with

and add a 1px
at the end of

, then use bidirectional binding to add buttons and modify text, and use the 1px
for focus and blur. But feelings are also particularly hard to achieve.

Button Inside TextArea in HTML

Then I searched again and found this library: React-Contenteditable

The solution

It was a bit of a shock to see contentEditable, a property I had been using to show off for a long time, but it was a day of relief for me.

The library is also interesting to use, because when you use function components, instead of having a value and an onChange, you need to pass an innerRef to control the text inside.

function App() {
  const innerRef = useRef<HTMLElement>(null);
  const value = useRef<string> (' ');

  const onChange = (event: ContentEditableEvent) = > {
    value.current = event.target.value;
  }

  const onAddButton = () = > {
    if(! innerRef.current) {return;
    }
    innerRef.current.innerHTML += & NBSP; '
  }

  return (
    <div className="App">
      <ContentEditable 
        style={{ border: '1px solid black', height: 100 }} 
        innerRef={innerRef} 
        html={value.current} 
        onChange={onChange} 
      />
      <button onClick={onAddButton}>Add the name</button>
    </div>
  );
}
Copy the code

Take a look at the react-Contenteditable source code

The above mentioned use of ref to control text changes made me curious about how it was implemented, so I cloned his Github and found that the implementation was not very simple. Github is here.

Render function

Since we use contentEditable for input and output, almost any element is ok, so this component allows us to pass in a tagName to specify which element to base on.

render() {
  const{ tagName, html, innerRef, ... props } =this.props;

  return React.createElement(
    tagName || 'div',
    {
      ...props,
      ref: typeof innerRef === 'function' ? (current: HTMLElement) = > {
        innerRef(current)
        this.el.current = current
      } : innerRef || this.el,
      onInput: this.emitChange,
      onBlur: this.props.onBlur || this.emitChange,
      onKeyUp: this.props.onKeyUp || this.emitChange,
      onKeyDown: this.props.onKeyDown || this.emitChange,
      contentEditable:!this.props.disabled,
      dangerouslySetInnerHTML: { __html: html }
    },
    this.props.children);
}
Copy the code

The render function here is a function that specifies which function to render, binds events, whether to enable the contentEditable property, and passes props.

value

We also observe that the value here is actually displayed by dangerouslySetInnerHTML: {__html: HTML}.

We also had to think about preventing script injection when we were dangerously close, so the source code normalize values as well:

function normalizeHtml(str: string) :string {
  return str && str.replace(/  |\u202F|\u00A0/g.' ');
}
Copy the code

The event

It’s also worth noting that in addition to and

can also trigger an onInput event.

For example, I tried to implement this myself:

const VarInput: FC<IProps> = (props) = > {
  const{ value, tag, disabled, onInput, ref, ... restProps } = props;const innerRef = useRef(null);

  const curtRef = ref || innerRef;

  const emitChange = () = > {
    const callbackValue: string = curtRef.current ? curtRef.current.innerHTML : ' '; onInput! (callbackValue); }constvarInputProps = { ... restProps,ref: curtRef,
    contentEditable: !disabled,
    onInput: emitChange,
    dangerouslySetInnerHTML: { __html: value }
  }

  return createElement(tag || 'div', varInputProps);
}
Copy the code

When used

function App() {
  const [value] = useState(' ');

  const onChange = (value: string) = > {
    console.log(value); / / print the value
  }

  return (
    <div className="App">
      <VarInput value={value} onInput={onChange} />
    </div>
  );
Copy the code

However, when I only bindonChangeWill not trigger the event! So, there is a difference between onInput and onChange!

emitChange

The onChange, onInput, and other callbacks call the emitChange function:

emitChange = (originalEvt: React.SyntheticEvent<any>) => { const el = this.getEl(); if (! el) return; const html = el.innerHTML; if (this.props.onChange && html ! == this.lastHtml) { // Clone event with Object.assign to avoid // "Cannot assign to read only property 'target' of object" const evt = Object.assign({}, originalEvt, { target: { value: html } }); this.props.onChange(evt); } this.lastHtml = html; }Copy the code

It makes a lot of sense, after all, to just get the innerHTML, construct an event, and put it in onChange. Simple.

componentDidUpdate

To be honest, I have done all the above things myself, but there is one problem I can’t do, that is, every time I type, the cursor moves to the front!! For example, IF I type “hello”, it will say: “Olleh”, what the hell is that? !

Usage:

function App() {
  const [value, setValue] = useState(' ');

  const onChange = (value: string) = > {
    console.log(value);
    setValue(value)
  }

  return (
    <div className="App">
      <VarInput value={value} onInput={onChange} />
    </div>
  );
}
Copy the code

And that’s because when I setValue, the cursor moves to the front, back to the source code, and it takes that into account. He used a function in componentDidUpdate to handle this:

componentDidUpdate() {
  const el = this.getEl();
  if(! el)return;

  // Perhaps React (whose VDOM gets outdated because we often prevent
  // rerendering) did not update the DOM. So we update it manually now.
  if (this.props.html ! == el.innerHTML) { el.innerHTML =this.props.html;
  }
  this.lastHtml = this.props.html;
  replaceCaret(el);
}

function replaceCaret(el: HTMLElement) {
  // Place the caret at the end of the element
  const target = document.createTextNode(' ');
  el.appendChild(target);
  // do not move caret if element was not focused
  const isTargetFocused = document.activeElement === el;
  if(target ! = =null&& target.nodeValue ! = =null && isTargetFocused) {
    var sel = window.getSelection();
    if(sel ! = =null) {
      var range = document.createRange();
      range.setStart(target, target.nodeValue.length);
      range.collapse(true);
      sel.removeAllRanges();
      sel.addRange(range);
    }
    if (el instanceofHTMLElement) el.focus(); }}Copy the code

This function ensures that the cursor is moved to the last position after each update, which is naive.

shouldComponentUpdate

ShouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldProps

The last

Next time show this attribute when you can also give this article to sister to see, learn 🐶 (escape

(after)