reactjs - redux forma e estrutura de interface do usuário material - com tipo automático - e funcionalidade de campo de limpeza

Oct 07 2020

Estou construindo um framework de formulário aninhado que usa o formulário redux e o framework ui material - eu construí os componentes aqui até agora - https://codesandbox.io/s/heuristic-hopper-lzekw

o que eu gostaria de fazer - é acrescentar alguma "animação" aos campos - para imitar a digitação - consegui isso por meio de uma pequena função que pegará o texto inicial e percorrerá os caracteres - atualizando o prop do inicial valores para esse campo.

Agora, o problema que tenho - é que preciso criar um onClick no textField - e se for um campo de texto de tipo automatizado - redefina o valor para uma string vazia - passe esse onclick de volta para os shells pais - e até mesmo de volta até a função typetext para quebrar o tempo limite --- então, se o usuário carregar a página, ele verá a digitação do texto - mas com melhorias na funcionalidade da IU - se eu clicar no campo durante o meio da animação - quero que a animação pare / interrompa , Quero que o campo fique limpo.

Desejo ter controle também sobre quais campos devem ser limpos - então, neste caso - ter um parâmetro - que indica onClickClear: true - para não quebrar o perfil de edição do usuário em formulários pré-preenchidos.

=== sandbox sem texto de tipo - mas uma boa base para colar essas duas estruturas juntas https://codesandbox.io/s/heuristic-hopper-lzekw?file=/src/Home.js

== esta é a última sandbox com digitação automática, como é atualmente https://codesandbox.io/s/amazing-bell-z8nhf

var self = this;
typeAnimation(this.state.initial_search_term.search_term, 100, function(msg){
  self.setState({
    initial_search_term: {"search_term": msg}
  });
});

Respostas

1 lissettdm Oct 12 2020 at 17:54

Acho que atualizar a propriedade do espaço reservado usando a referência de entrada é uma boa solução, dessa forma você não precisa atualizar o valor de entrada (evite re-renderizações do componente), e você pode limpar o texto do espaço reservado no evento de clique:

Home.js

class Home extends Component {
  constructor(props, context) {
    super(props, context);
    this.searchInputRef = React.createRef(null);
    this.state = { initial_search_term: { search_term: "" } };
  }

  componentDidMount() {
    var self = this;
    typeAnimation("Start typing...", 100, function (msg) {
      if (document.activeElement !== self.searchInputRef.current) {
        self.searchInputRef.current.setAttribute("placeholder", msg);
      } else {
        return true; // stop typings
      }
    });
  }


  render() {
    //...

    let fieldsSearchForm = [
      {
        id: "search-field",
        type: "text",
        label: "Search Term",
        name: ["search_term"],
        options: [],
        fieldRef: this.searchInputRef,
        onClick: () => (this.searchInputRef.current.placeholder = "")
      }
    ];
   //...
  }
}

FieldMaker.js

class FieldMaker extends Component {
  //...

  
  render() {
   
    return (
      <>
        {this.state.fields.map((item, j) => {
          if (item.visibility) {
            if (item.type !== "checkbox") {
              return (
                <Field
                  id={item.id}
                  //...other props
                  fieldRef={item.fieldRef}
                  onClick={item.onClick}
                />
              );
            } else {
              //...
            }
          } else {
            //...
          }
        })}
      </>
    );
  }
}

renderTextField.js

const renderTextField = ({
  id,
  input,
  rows,
  multiline,
  label,
  type,
  meta: { touched, error, warning },
  onClick,
  fieldRef
}) => (
  <FormControl
    component="fieldset"
    fullWidth={true}
    className={multiline === true ? "has-multiline" : null}
  >
    <TextField
      id={id}
      inputRef={fieldRef}
      onClick={onClick}
      // other props
    />
  </FormControl>
);

Utility.js

export async function typeAnimation(text, timing, callback) {
  let concatStr = "";
  for (const char of text) {
    concatStr += char;
    await sleep(timing);
    const shouldStop = callback(concatStr);
    if (shouldStop) break; // stop the loop
  }
}

styles.css // para manter o marcador de posição visível

#search-field-label {
  transform: translate(0, 1.5px) scale(0.75);
  transform-origin: top left;
}

#search-field::-webkit-input-placeholder {
  opacity: 1 !important;
}
1 ZacharyHaber Oct 12 2020 at 03:26

Sei que essa não é a resposta que você está procurando, mas o caminho mais fácil seria animar o texto do espaço reservado em vez do texto de entrada principal. Então você não precisa se preocupar com nada e pode simplesmente deixar a animação tocar independentemente das ações do usuário.

Agora, o problema que tenho - é que preciso criar um onClick no textField - e se for um campo de texto de tipo automatizado - redefina o valor para uma string vazia - passe esse onclick de volta para os shells pais - e até mesmo de volta até a função typetext para quebrar o tempo limite --- então, se o usuário carregar a página, ele verá a digitação do texto - mas com melhorias na funcionalidade da IU - se eu clicar no campo durante o meio da animação - quero que a animação pare / interrompa , Quero que o campo fique limpo.

Desejo ter controle também sobre quais campos devem ser limpos - então, neste caso - ter um parâmetro - que indica onClickClear: true - para não quebrar o perfil de edição do usuário em formulários pré-preenchidos.

Tudo isso é satisfeito com o uso do espaço reservado do campo (embora o texto do tipo não pare porque não há necessidade, pois o texto do usuário / texto pré-preenchido ocultará os espaços reservados). A única coisa que não conectei é interromper o texto do tipo Home, componentWillUnmountpois, sem isso, ele lançaria mensagens de aviso de que setState está sendo chamado em um componente não montado.

Tive que fazer uma refatoração, pois havia alguns problemas com coisas como alterar o estado React ( toggleFieldVisibilityin FieldMaker.js) e não atualizar this.state.fieldsquando novos props são passados, já que o estado estava apenas sendo definido no construtor . Também mudei o nome de algumas coisas internas FieldMaker.jsenquanto estava nisso (principalmente devido à preferência pessoal neste caso).

Definitivamente, há problemas em tentar derivar o estado dos adereços, independentemente de como você faz isso: Provavelmente, você não precisa do estado derivado

Código de execução:

https://codesandbox.io/s/busy-davinci-mk0dq?file=/src/Home.js

Home.js

  state = {
    initial_search_term: { search_term: "" },
    searchPlaceholder: "",
    textPlaceholder: "",
    valPlaceholder: ""
  };

  componentDidMount() {
    typeAnimation("Search Text...", 100, (msg) => {
      this.setState({
        searchPlaceholder: msg
      });
    });
    typeAnimation("Just some super long text you used to know", 100, (msg) => {
      this.setState({
        textPlaceholder: msg
      });
    });
    typeAnimation("I'm a value, but am I valuable??", 100, (msg) => {
      this.setState({
        valPlaceholder: msg
      });
    });
  }

// Render funct:
  let fieldsSearchForm = [
      {
        type: "text",
        label: "Search Term",
        name: ["search_term"],
        props: { placeholder: this.state.searchPlaceholder },
        options: []
      },
      {
        type: "text",
        label: "Text",
        name: ["test"],
        props: { placeholder: this.state.textPlaceholder },
        options: []
      },
      {
        type: "text",
        label: "Value",
        name: ["test2"],
        props: { placeholder: this.state.valPlaceholder }
      }
    ];

FieldMaker.js

O getDerivedStateFromProps é a diferença principal real aqui, isso é para preencher a matriz subs com base nos campos sempre que os campos mudam (e para definir a visibilidade). Eu não tenho nenhuma idéia de quanto do que é realmente necessário porque não há nenhuma noção do que qualquer de que realmente é suposto fazer neste. Portanto, é provável que seja necessário mais trabalho para que funcione totalmente.

A outra diferença é um pouco uma refatoração para ter um visiblityobjeto separado no estado em vez de modificar o fieldsestado.

O principal motivo para modificar este arquivo é certificar-se de que as atualizações para o fieldsobjeto se traduzam em atualizações para as crianças, de Fieldsmodo que o marcador possa ser passado por meio de objetos para o Fielde, assim, orenderTextField

  state = {
    visibility: {}
  };

  static getDerivedStateFromProps(props, state) {
    let newState = { prevFields: props.fields };
    if (props.fields !== state.prevFields) {
      let visibility = state.visibility;
      let subs = props.fields.reduce((subs, field) => {
        if (field.sub) {
          subs.push(field.sub);
          visibility[field.name] = false;
        } else {
          visibility[field.name] = true;
        }
        return subs;
      }, []);
      newState.subs = subs;
    }
    return newState;
  }

  toggleFieldVisibility(pos, isVisibile) {
    let field = this.props.fields[pos].name;
    this.setState((prev) => {
      return { ...prev, [field]: isVisibile };
    });
    // This directly manipulates state, and is likely problematic in React
    // let fields = { ...this.state.fields };
    // fields[pos]["visibility"] = isVisibile;
  }

  componentDidMount() {
    this.hideSubs();
  }

// In render:
    return (
      <>
        {this.props.fields.map((item, j) => {
          if (this.state.visibility[item.name]) {
            if (item.type !== "checkbox") {
              return (
                <Field
                  key={j}                  
                  name={item.name[0]}
                  props={item.props}
                  label={item.label}
// ...

renderTextField.js

Nesse sentido, o objetivo da mudança é apenas passar o espaço reservado para o MUI TextField e fazer com que o rótulo do MUI TextField diminua com a configuração InputLabelProps = {shrink: true}

const renderTextField = ({
  input,
  rows,
  multiline,
  label,
  type,
  meta: { touched, error, warning },
  placeholder,
  InputLabelProps
}) => {
  // Ensure that the label is shrunk to the top of the input
  // whenever there's a placeholder set
  InputLabelProps = placeholder
    ? { ...(InputLabelProps ?? {}), shrink: true }
    : InputLabelProps;
  return (
    <FormControl
      component="fieldset"
      fullWidth={true}
      className={multiline === true ? "has-multiline" : null}
    >
      <TextField
        InputLabelProps={InputLabelProps}
        placeholder={placeholder}
        label={label}
        multiline={multiline}
        rows={rows}
        type={type}
        error={touched && (error && error.length > 0 ? true : false)}
        helperText={
          touched &&
          ((error && error.length > 0 ? error : null) ||
            (warning && warning.length > 0 ? warning : null))
        }
        {...input}
      />
    </FormControl>
  );
};


Refiz a solução de uma maneira muito rápida e suja para evitar as armadilhas que existem no arquivo FieldMaker que inicialmente causaram problemas na solução original:

https://codesandbox.io/s/fervent-moser-0qtvu?file=/src/Home.js

Modifiquei typeAnimation para oferecer suporte a uma aparência de cancelamento, retornando uma função de cancelamento que interrompe o loop e define usa o retorno de chamada para definir o valor para o estado final.

export function typeAnimation(text, timing, callback) {
  let concatStr = "";
  let canceled = false;
  function cancel() {
    canceled = true;
  }
  async function runAnimation() {
    for (const char of text) {
      concatStr += char;
      await sleep(timing);
      if (canceled) {
        break;
      }
      callback(concatStr);
    }
    if (canceled) {
      callback(text);
    }
  }
  runAnimation();
  return cancel;
}

Em seguida Home.js, modifiquei o estado inicial e o componentDidMount para trabalhar com os espaços reservados e me fornecer um local para armazenar as funções de cancelamento.

  constructor(props, context) {
    super(props, context);
    this.state = {
      initial_search_term: { search_term: "" },
      placeholders: { search_term: "" }
    };
  }
  cancelAnimations = {};
  componentDidMount() {
    var self = this;
    this.cancelAnimations.search_term = typeAnimation(
      "Start typing...",
      100,
      function (msg) {
        self.setState((state) => ({
          placeholders: { ...state.placeholders, search_term: msg }
        }));
      }
    );
  }

Eu também adiciono fieldsExtrase passo isso todo o caminho para o componente FieldMaker para adicionar adereços extras ao campo naquele componente por meio do índice que corresponde ao fieldsSearchFormarray.

    let fieldsExtras = [
      {
        placeholder: this.state.placeholders.search_term,
        onClick: this.cancelAnimations.search_term
      }
    ];

Então, uma vez que os adereços extras foram passados ​​para o campo, em renderTextField, eu faço o mesmo tipo de coisa que antes, mas também adicionei o onClick para chamar a onClickfunção passada

const renderTextField = ({
  input,
  rows,
  multiline,
  label,
  type,
  meta: { touched, error, warning },
  placeholder,
  onClick,
  InputLabelProps
}) => {
  InputLabelProps = placeholder
    ? { ...(InputLabelProps ?? {}), shrink: true }
    : InputLabelProps;
  return (
    <FormControl
      component="fieldset"
      fullWidth={true}
      className={multiline === true ? "has-multiline" : null}
    >
      <TextField
        placeholder={placeholder}
        InputLabelProps={InputLabelProps}
        onClick={(e, value) => {
          onClick && onClick(e);
        }}
        label={label}
        multiline={multiline}
        rows={rows}
        type={type}
        error={touched && (error && error.length > 0 ? true : false)}
        helperText={
          touched &&
          ((error && error.length > 0 ? error : null) ||
            (warning && warning.length > 0 ? warning : null))
        }

        {...input}
      />
    </FormControl>
  );
};