Google Places in React: иногда обработчик кликов не запускается - PullRequest
0 голосов
/ 04 февраля 2020

Я работаю над сайтом, закодированным в React, Redux и redux-form. Клиент хочет, чтобы одна из его форм приема использовала API автозаполнения / Адресов Google, чтобы автоматически заполнять адрес пользователя и помогать упростить процесс.

Основной c поток пользователя для этого взаимодействия выглядит следующим образом:

  • Пользователь начинает вводить адрес в поле * 1007. *.
  • Автозаполнение Google запрашивает новый текст и возвращает список предложений.
  • React отображает эти предложения в поле ниже поля address1.
  • Пользователь нажимает на предложение, а затем окно предложения исчезает.
  • Google Places API извлекает данные соответствующего места для обновления полей.
  • Поля address1, city, state и zip обновляются в соответствии с данными места.

Форма также содержит поля, которые не следует перезаписывать, когда поля адреса заполнены. Чтобы решить эту проблему, я FormPage отслеживаю эти auxFields и отправляю их обратно в FormComponent с результатом запроса Google Адресов.

Проблема, с которой я столкнулся вызывает то, что когда пользователь нажимает на одно из предложений, иногда обработчик щелчка не срабатывает. В результате поиск Google Адресов не происходит, и поля не заполняются. Я подозреваю, что эта проблема связана с React, а не с проблемой Google Адресов. Я предполагаю, что это своего рода состояние гонки с регистрацией обработчиков кликов на вновь созданных элементах предложения. Я не уверен, как решить эту проблему.

FormPage.js - компонент уровня страницы, отвечающий за поиск в Google и передачу результатов в FormComponent:

import Script from 'react-load-script';
class FormPage extends PureComponent {
  constructor(props) {
    super(props);

    this.states = {AL: 'AL', /* ... and so on */};
    this.auxFields = {
      address2: null,
      dob: null,
    };
    this.state = {
      placesAPILoaded: false,
      addressValues: {
        address1: null,
        city: null,
        state: null,
        zip: null,
        ...this.auxFields,
      },
      suggestions: [
        {
          label: '',
          place: null,
        },
      ],
    };

    this.googleUrl = 'https://maps.googleapis.com/maps/api/place/autocomplete/json?';
    this.googleApiKey = 'This--is--where--your--API--key--goes--';
    this.placesService = null;
    this.autocompleteService = null;
  };

  handlePlacesLoaded() {
    const {placesAPILoaded} = this.state;
    if (!placesAPILoaded) {
      this.autocompleteService = new google.maps.places.AutocompleteService();
      this.placesService = new google.maps.places.PlacesService(document.createElement('div'));
      this.setState({placesAPILoaded: true});
    }
  };

  queryPlacesAPI(input) {
    if (this.autocompleteService) {
      const request = {input: input};
      this.autocompleteService.getPlacePredictions(request, (predictions, status)=>{
        if (status === 'OK' && predictions.length) {
          const suggestions = predictions.map((p, i)=>{
            return {label: p.description, place: p.place_id};
          });
          this.setState({suggestions});
          return;
        }
      });
    }
  };

  handleAuxFieldChange(valueMap, newVal, prevVal, fieldName) {
    this.auxFields[fieldName] = newVal;
  };

  updateAddressValues(placeId) {
    if (this.placesService) {
      const request = {placeId: placeId, fields: ['address_components']};
      this.placesService.getDetails(request, (place, status)=>{
        if (status === 'OK') {
          const {addressValues} = this.state;
          document.getElementById('Faddress1').blur(); // blur address1
          const address = {...addressValues, address1:[], ...this.auxFields};
          place.address_components.forEach((item, i) => {
            switch (item.types[0]) {
              case 'street_number': address.address1[0] = item.short_name; break;
              case 'route': address.address1[1] = item.short_name; break;
              case 'locality': address.city = item.short_name; break;
              case 'administrative_area_level_1': address.state = this.states[item.short_name]; break;
              case 'postal_code': address.zip = item.short_name; break;
              default: break;
            }
          });
          address.address1 = address.address1.join(' ');
          this.setState((prevState)=>({addressValues: address}));
        }
      });
    }
  };

  render() {
    const { addressValues, suggestions} = this.state;
    const content = [{
      title: 'More About You',
      component: (
        <FormComponent
          addressValues={addressValues}
          onAuxFieldChange={this.handleAuxFieldChange}
          doPlacesQuery={this.queryPlacesAPI}
          suggestedPlaces={suggestions}
          choosePlace={this.updateAddressValues}
        />
      ),
    }];
    return (
      <div>
        <PageTemplate content={content} />
        <Script
          url={`https://maps.googleapis.com/maps/api/js?key=${this.googleApiKey}&libraries=places`}
          onLoad={this.handlePlacesLoaded}
        />
      </div>
    );
  };
};

FormComponent.js обрабатывает взаимодействия на уровне формы и вызывает методы поиска родителя FormPage:

class FormComponent extends PureComponent {
  constructor(props, context) {
    super(props, context);
    this.fields = {
      address1: {...},
      address2: {...},
      // ...
    };
    this.suggestionsBox = null;
    this.suggestionsRef = (element) => {
      if (element) this.suggestionsBox = element;
    };
  }

  onAddressChange(chars, newValue, oldValue, fieldName) {
    const {doPlacesQuery, suggestedPlaces} = this.props;
    if (newValue !== '') {
      doPlacesQuery(newValue);
      if (!suggestedPlaces || suggestedPlaces.length === 0) console.error('Places Error.');
      if (this.suggestionsBox) this.suggestionsBox.style.display = 'block';
    } else {
      if (this.suggestionsBox) this.suggestionsBox.style.display = 'none';
    }
  };

  onAddressBlur() {
    setTimeout(()=>{
      if (this.suggestionsBox) this.suggestionsBox.style.display = 'none';
    }, 100);
  };

  chooseSuggestion(place) {
    const {choosePlace} = this.props;
    choosePlace(place);
  };

  componentDidUpdate(prevProps) {
    const {updateAddress, addressValues, change} = this.props;
    console.log('component->componentDidUpdate.');
    if (prevProps.addressValues.address1 !== addressValues.address1 ||
        prevProps.addressValues.city !== addressValues.city ||
        prevProps.addressValues.state !== addressValues.state ||
        prevProps.addressValues.zip !== addressValues.zip) {
      console.log('component`s new props had a new address.');
      updateAddress(addressValues, change);
    }
  };

  submitForm(formData) {
    // ...
    // postToApi(url, data).then(onSuccess, onError);
  };

  renderInput(field) {
    const {onAuxFieldChange, suggestedPlaces} = this.props;
    switch(field.type) {
      // cases 'select', 'date', 'ssn': return (jsx);
      case 'text': default:
        if (field.slug === 'address1') return (
          <div>
            <Input className={s.FormComponent__field__input}
              label={field.label} type="text"
              id={`F${field.slug}`} name={field.slug}
              onChange={this.onAddressChange}
              onBlur={this.onAddressBlur}
            />
            <div
              className={s.FormComponent__suggest}
              style={{display: 'none'}}
              ref={(element)=>{ this.suggestionsRef(element) }}>
              <p className={s.FormComponent__suggest__title}>Suggestions</p>
              <ul>
                { suggestedPlaces && suggestedPlaces.map((sp, i)=>(
                  <li key={sp.label} onClick={()=>this.chooseSuggestion(sp.place)}><span>{sp.label}</span></li>
                )) }
              </ul>
              <div className={s.FormComponent__suggest__poweredby}><img src="/assets/images/shared/powered_by_google_on_white.png" /></div>
            </div>
          </div>
        );
        else if (field.slug === 'address2') return (
          <Input /* ... */ onChange={onAuxFieldChange} />
        );
        return null;
    }
  };

  render() {
    const {
      handleSubmit,
      error,
      invalid,
      submitting,
      pristine,
    } = this.props;

    return (
      <div className={s.FormComponent__wrapper}>
        <form onSubmit={handleSubmit(this.submitForm)}>
          {error && <strong>{error}</strong>}
          <div className={s.FormComponent__fields}>
            {Object.values(this.fields).map((field)=>(
              <div
                key={field.slug}
                className={FormComponent__field}
                style={{width:`${field.size}%`}}
              >
                { this.renderInput(field) }
              </div>
            ))}
          </div>
          <button type="submit">Continue</button>
        </form>
      </div>
    );
  };
};

PageTemplate.js:

const PageTemplate = ({content}) => {
  return (
    <div className={s.templateWrapper}>
      <div className={s.templateContainer}>
        {content.map((node, index) => {
          const {component, title} = node;
          return <ContentContainer header={title} key={`content-${index}`}>{component}</ContentContainer>;
        })}
      </div>
    </div>
  );
};

ContentContainer.js:

const ContentContainer = ({header, children}) => {
  return (
    <div className={s.contentContainer}>
      {header && <div className={s.contentContainer__header}>{header}</div>}
      <div>{children}</div>
    </div>
  );
};
...