Режим редактирования профиля

1. Создание компоненты для режима редактирования и передача данных из нее на сервер

  1. Выносим в отдельную компоненту ProfileData код, который выводил уже готовые данные из сервера, просто был внутри теперь уже родительской компоненты – ProfileInfo в том же файле. В пропсах сюда приходит profile, в котором и лежит объект из сервера, элементы которого мы и выводим в профиле. Также приходит isOwner, и согласно которому, если мы в режиме “владельца” (т.е. это наша страница), то появляется кнопочка для перехода в режим редактирования. При клике на нее запускается функция goToEditMode, также приходящая в пропсах извне.
    Выглядит эта компонента вот так (вместе с компонентой Contact, выводящей):

    const ProfileData = ({ profile, isOwner, goToEditMode }) => {
      return <div>
        {isOwner && <div><button onClick={goToEditMode}>Edit</button></div>}
        <div>
          <b>Full name</b>: {profile.fullName}
        </div>
        {profile.lookingForAJob &&
          <div>
            <b>Looking for a job</b>: {profile.lookingForAJob ? "yes" : "no"}
          </div>
        }
        <div>
          <b>My professional skills</b>: {profile.lookingForAJobDescription}
        </div>
    
        <div>
          <b>About me</b>: {profile.aboutMe}
        </div>
        <div>
          <b>Contacts</b>: {Object.keys(profile.contacts).map(key => {
            return <Contact key={key} contactTitle={key} contactValue={profile.contacts[key]} />
          })}
        </div>
      </div>
    }
    
    const Contact = ({ contactTitle, contactValue }) => {
      return <div className={s.contact}>
        <b>{contactTitle}</b>: {contactValue}
      </div>
    }

     

  2. Внутри родительской компоненте ProfileInfo в месте вывода вынесенной только что компоненты ProfileData создаем условие через тернарный оператор, при котором зависимо от приходящего из локального “хуковского” стейта editMode показывается либо обычный режим отображения информации (ProfileData), либо режим редактирования (ProfileDataForm).
    const ProfileInfo = ({ profile, status, updateStatus, isOwner, savePhoto }) => {
    
      let [editMode, setEditMode] = useState(false);
    
      if (!profile) {
        return <Preloader />
      }
    
      const mainPhotoSelected = (e) => {
        if (e.target.files.length) {
          savePhoto(e.target.files[0])
        }
      }
    
      return (
        <div>
          <div className={s.bigAvatar}>
            <img src="https://atlantis.nyc3.digitaloceanspaces.com/styled/72025f140f22a3eb32950bbb9d76e68d" />
          </div>
          <div className={s.descriptionBlock}>
            <img src={profile.photos.large || userPhoto} className={s.main} />
            {isOwner && <input type={"file"} onChange={mainPhotoSelected} />}
    
            {editMode //вот наш выбор, какую компоненту отображать
              ? <ProfileDataForm profile={profile} />
              : <ProfileData goToEditMode={() => { setEditMode(true) }} profile={profile} isOwner={isOwner} />}
    
    
            <ProfileStatusWithHooks status={status} updateStatus={updateStatus} />
          </div>
        </div>
      )
    }

    И как видно, прямо в разметке мы вставили коротенькую функцию goToEditMode, которую ранее вешали на кнопку. Она просто меняет editMode в локальном стейте на true.

  3. Создаем теперь отдельную компоненту  ProfileDataForm с полями для режима редактирования, которая похожа на ProfileData, которая выводит данные, но вместо полей вывода имеет инпуты. Выносим ее в в отдельный файл src\components\Profile\ProfileInfo\profileDataForm.jsx.
    Оборачивающие дивки заменяем на тег <form>. Инпуты создаем, вызывая

    createField (src\components\common\FormsControls\FormsControls.js) – функции, которая мы создавали ранее на базе Redux Form для создания различных полей, типы которых задаются в параметрах функции.
    Также создаем для ProfileDataForm контейнерную компоненту ProfileDataFormReduxForm с помощью хока reduxForm, чтобы заработали наши формочки, и экспортируем уже ее.
    Onclick мы не вешаем, так как само наличие кнопки внутри формы по умолчанию засабмитит форму. Вешаем через обработчик события onSubmit  на саму форму вызов функции handleSubmit, который придет в пропсах.
    Вот весь файл ProfileDataFormReduxForm:

    import React from "react";
    import s from './ProfileInfo.module.css';
    import style from '../../common/FormsControls/FormsControls.module.css';
    import { createField, Input, Textarea } from "../../common/FormsControls/FormsControls"
    import { reduxForm } from "redux-form";
    
    const ProfileDataForm = ({ handleSubmit }) => {
      return <form onSubmit={handleSubmit}>
        <div><button>Save</button></div>
        <div>
          <b>Full name</b>: {createField("Full name", "fullName", [], Input)}
        </div>
        <div>
          <b>Looking for a job</b>: {createField("", "lookingForAJob", [], Input, { type: "checkbox" })}
        </div>
    
        <div>
          <b>My professional skills</b>:
            {createField("My professional skills", "lookingForAJobDescription", [], Textarea)}
        </div>
    
        <div>
          <b>About me</b>:
          {createField("About me", "aboutMe", [], Textarea)}
        </div>
        <div>
          <b>Contacts</b>: {Object.keys(profile.contacts).map(key => {
            return <div key={key} className={s.contact}>
              <b>{key}: {createField(key, "contacts." + key, [], Input)}</b>
            </div >
          })}
        </div>
      </form>
    }
    
    const ProfileDataFormReduxForm = reduxForm({ form: 'edit-profile' })(ProfileDataForm);
    
    export default ProfileDataFormReduxForm;

    Обратите также внимание, что здесь мы через props получили profile и использовали его, чтобы перебрать и вывести контакты с полями для редактирования, что напоминает то, как мы это делали в компоненте для просмотра, но там мы создавали еще одну компоненту, которая много раз вызывалас.

  4. Теперь там где мы рисуем эту компоненту (внутри компоненты ProfileInfo) мы должны определить этот метод onSubmit, который вешаем на форму. Вот так мы в консоли можем вывести данные, которые передает форма в файле src\components\Profile\ProfileInfo\ProfileInfo.jsx:
    const ProfileInfo = ({ profile, status, updateStatus, isOwner, savePhoto }) => {
    
      let [editMode, setEditMode] = useState(false);
    
    ...
    const onSubmit = (formData) => {
      console.log(formData);
    }
    ...
    

    Вот что попадает в консоль в таком случае:
    formData из формы в консоли

    Redux Form нам собрал и упаковал объект. Его нужно теперь отправить на сервер.

  5. Для передачи на сервер заменяем вывод formData в консоль на передачу в санку saveProfile, которая будет приходить в ProfileInfo через пропсы.
    const ProfileInfo = ({ profile, status, updateStatus, isOwner, savePhoto, saveProfile }) => {
    
      let [editMode, setEditMode] = useState(false);
    
    ...
    
      const onSubmit = (formData) => {
        saveProfile(formData);
      }
    
      return (
        <div>
    ...
    
            {editMode
              ? <ProfileDataForm initialValues={profile} profile={profile} onSubmit={onSubmit} />
              : <ProfileData goToEditMode={() => { setEditMode(true) }} profile={profile} isOwner={isOwner} />}

     

  6. Этот же saveProfile передаем в пропсы через родительскую компоненту Profile:
    const Profile = (props) => {
      return  (
          <div>
            <ProfileInfo savePhoto={props.savePhoto}
            isOwner={props.isOwner} 
            profile={props.profile} 
            status={props.status} 
            updateStatus={props.updateStatus}
            saveProfile={props.saveProfile} /> //вот
            <MyPostsContainer />
          </div>
        )
    }

     

  7. А вот ProfileContainer, которая родительская для Profile, это уже контейнерная компонента и она конектится к бизнесу и наш санкриэйтер saveProfile мы уже в ней конектим внизу файла:
    ...
    import {getUserProfile, getStatus, updateStatus, savePhoto, saveProfile} from '../../redux/profile-reducer';
    ...
    class ProfileContainer extends React.Component {...}
    
    ...
    
    export default compose(
        connect(mapStateToProps, {getUserProfile, getStatus, updateStatus, savePhoto, saveProfile}),
        withRouter
    )(ProfileContainer)
  8. В редьюсере (profile-reducer.js) добавляем в конце перед экспортом код нашей санки (точнее сакнккриэйтера) saveProfile для вызова запроса из API:
    export const saveProfile  = (profile) => async () => {
        const response = await profileAPI.saveProfile(profile);  
    }

    А в API отправляем put-запрос на нужный url:

    export const profileAPI = {
       
    ...,
        saveProfile(profile) {
            return instance.put('profile', profile);
        }
    }

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

2. Передаем value по умолчанию в локальный стейт Redux Form

Описание передачи значения по умолчанию в Redux Form вынесен в одтельную статью.

3. Дорабатываем редьюсер, чтобы возвращаться в режим просмотра после сохранения

  1. Внутри нашего санккриэйтера saveProfile задиспатчим другой санккриэйтер из этого же файла – getUserProfile, чтобы он возвращал обновленный профайл текущего пользователя, – и передадим наш ID залогиненого пользователя. ID мы берем из стейта через getState, ведь нам не запрещено в рамках одного редьюсера обращаться к другому.
    export const saveProfile  = (profile) => async (dispatch, getState) => {
        const userId = getState().auth.userId;
        const response = await profileAPI.saveProfile(profile);  
                if (response.data.resultCode === 0) {
                    dispatch(getUserProfile(userId));
                } 
    }

    Это мы слали для того, чтобы при возврате на страницу просмотра профила видеть обновленные данные. Но пока возврат (выход из режима редактирования) автоматически не происходит.

  2. Для автоматического выхода из режима редактирования нужно в наше событие, висящее на кнопке “Save” в ProfileInfo добавить еще setEditMode(false)

     const ProfileInfo = ({ profile, status, updateStatus, isOwner, savePhoto, saveProfile }) => {
     ...
      const onSubmit = (formData) => {
        const promise = saveProfile(formData);
        setEditMode(false);
      }
    
    ...

     

4. Вывод ошибки при некорректном заполнении поля формы

  1. Получаем для нашей формы под названием “edit-profile” (такое имя мы указали при “оборачивании” ProfileDataForm и создании редаксовской формы из нее) в редьюсере текст ошибки, используя условие else, когда ответ сервера не 0:
    export const saveProfile  = (profile) => async (dispatch, getState) => {
        const userId = getState().auth.userId;
        const response = await profileAPI.saveProfile(profile);  
                if (response.data.resultCode === 0) {
                    dispatch(getUserProfile(userId));
                } else {
                    dispatch(stopSubmit("edit-profile", {_error: response.data.messages[0]})); //общая ошибка формы
                }
    }
  2. Эта ошибка попадет в редакс-форму под словом error. И мы, если erorr есть, отображаем соответствующую дивку (аналогично тому, как делали в LoginForm) в profileDataForm:
    const ProfileDataForm = ({ handleSubmit, profile, error }) => {
      return <form onSubmit={handleSubmit}>
        <div><button>Save</button></div>
        {error && <div className={style.formSummaryError}>
          {error}
        </div>
        }
        <div>
          <b>Full name</b>: {createField("Full name", "fullName", [], Input)}
        </div>
    ....

    Не забываем создать стили для этой ошибки либо импортировать их уже существующие в другой компоненте , что не очень запрещается.

  3. Теперь мы столкнулись с тем, что сразу после сохранения новых данных автоматически перешли в режим просмотра, так как ранее повесили setEditMode(false) на кнопку Save. И в итоге не успеем увидеть никакой ошибки, просто данные на сервер не пойдут. Для решения этой проблемы применяем промисы и setEditMode будем запускать уже в then:
    const onSubmit = (formData) => {
      const promise = saveProfile(formData);
      promise.then( 
        () => {
          setEditMode(false);
        }
      )
    }

    А в редьюсере добавим возвращение Promise.reject(“текст ошибки”) в условие else, когда ответ сервера не 0. Этот метод возвращает объект Promise, который был отклонен, по указанной в скобках причине.

    export const saveProfile  = (profile) => async (dispatch, getState) => {
        const userId = getState().auth.userId;
        const response = await profileAPI.saveProfile(profile);  
                if (response.data.resultCode === 0) {
                    dispatch(getUserProfile(userId));
                } else {
                    dispatch(stopSubmit("edit-profile", {_error: response.data.messages[0]}));
                    return Promise.reject(response.data.messages[0]);
                }
    }

    Обратите внимание, что async...await ламает в этом случае логику работы редакс формы – сохранение некорректных данных и возврат в режим просмотра не происходит, но ошибка не показывается. Поэтому мы и применили именно then.

5. Ошибка для конкретного поля, а не для всей формы

Мы можем выводить ошибку не общую для формы, а для поля, где введены некорректные данные. Просто заменив одну строчку. Вот сравнение строк, выводящих общую ошибку и для отдельного поля:

dispatch(stopSubmit("edit-profile", {_error: response.data.messages[0]})); //общая ошибка формы

dispatch(stopSubmit("edit-profile", {"contacts": {"facebook": response.data.messages[0]} })); //ошибка для поля фейсбук, но нужно править, чтобы была универсальной для остальных полей

 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *