Русский
Русский
English
Статистика
Реклама

Forms

React.js формошлепство или работа с формами при помощи пользовательских хуков

06.01.2021 20:05:30 | Автор: admin


В этом посте будет реализован простой пользовательский хук для работы с состояниями формы и будут разобраны способы работы с объектами и массивами. В завершение разберем валидацию форм и реализуем соответствующие функции.


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


К слову, в React для удобной работы с формами уже набрали популярность 3 отличных библиотеки. Это Formik, Redux Form и React Hook Form. На сайте последнего представлены плюсы перед конкурентами.


Для начала


Для начала нам нужно создать React приложение. Сделаем это через Create React App. Если информации по ссылке будет недостаточно, то github.


Так как я являюсь поклонником TypeScript, я использовал готовый шаблон для работы с ним используя команду:


npx create-react-app react-custom-forms-article  --template typescript

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


Приступая к реализации хука


Перед созданием хука надо определиться, как мы будем работать с формой и ее полями. Для удобства можно использовать один объект инициализации формы, в котором изначально будут описываться необходимые поля и их настройки. Объявление этого объекта вынесено за код компонента и выглядит так.


// App.tsxconst formInputs = {  firstName: {},  lastName: '',}

В компоненте инициализируем форму.


// App.tsxconst App = () => {  const { fields, handleSubmit } = useForm(formInputs);  const { firstName } = fields;  return <></>;}

handleSubmit метод, принимающий callback-функцию и возвращающий значение всех полей.
fields структура вида "ключ-значение", в ней будут храниться все описанные ранее поля формы с дополнительной информацией, например: свойство с текстом ошибки, значение поля, флаг валидности поля и те свойства, что будут переданы в объекте при создании формы.


Компонент вернет примерно такую верстку.


// App.tsx// Метод выполнения формы при срабатывание onSubmitconst onSubmit = ({ values }: { values: IValues }) => {  console.log(values, 'submit');}return (  <div className="App">    <form onSubmit={handleSubmit(onSubmit)}>      <input type="text" value={firstName.value} onChange={firstName.setState}/>    </form>  </div>);

useForm


Приступим к реализации хука. Для начала необходимо пройтись по объекту инициализации формы. Во время этого процесса будет происходить проверка значений и объявление необходимых далее свойств.


// hooks/useForm.tsexport const useForm = (initialFields: any = {}) => {  const form = Object.entries(initialFields).reduce((fields, [name, value]: any[]) => {    const isString = typeof value === 'string';    const field = {      [name]: {        value: (isString && value) || ((!isString && value.value) || ''),        setState: (value: ChangeEvent<HTMLInputElement>) => handleInput(value, name),        ...(!isString && value),      }    }    return { ...fields, ...field };  }, {});

В этом примере кода для удобства итераций используется метод объектов entries, который возвращает массив вида [[propName, propValue], [propName, propValue]], и метод для работы с массивами reduce, который помогает собрать объект заново. В целом все выглядит неплохо, но не хватает методов для обновления значения полей. Добавим состояний с использованием React Hook.


// hooks/useForm.ts...const [fields, setState] = useState<any>(form);const handleInput = (element: ChangeEvent<HTMLInputElement>, name: string) => {  const input = fields[name];  const value = element.target.value;  const field = { ...input,  value };  setState(prevState => ({ ...prevState, [name]: field });}

Здесь заводится состояние для полей формы, и в качестве начального значения используется готовая структура формы. Функция handleInput будет необходима для редактирования данных. Как видно из кода, стейт будет обновляться полностью. Это специфика хука useState и текущей реализации. Если бы для работы с состояниями использовалась библиотека RxJs вместо хука useState, то была бы возможность обновлять состояние частично, не провоцируя повторный рендер компонента. В setState в данном примере состояние обновляется также через функцию обратного вызова. При использовании записи вида setState({ ...fields, [name]: field }) изменение другого поля провоцировало бы возврат остальных полей к исходным значениям.


Следующий пример кода проиллюстрирует применение формы.


// hooks/useForm.ts...const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const values = Object.entries(fields).reduce(((prev: any, [name, { value }]: any) => ({ ...prev, [name]: value })), {});    onSubmit({ values });  }

При помощи [каррирования] (https://learn.javascript.ru/currying-partials) принимается переданная из компонента функция и далее при сабмите вызывается с аргументами из хука. Каррирование в примере выше используется для того, чтобы иметь возможность вызвать метод в верстке, не выполняя его при рендере компонента.


Таким образом у нас получился минимальный хук для обычных форм.


Весь код хука
import { ChangeEvent, FormEvent,, useState } from 'react';export const useForm = (initialFields = {}) => {  const form = Object.entries(initialFields).reduce((fields, [name, value]: any[]) => {    const isString = typeof value === 'string';    const field = {      [name]: {        value: (isString && value) || ((!isString && value.value) || ''),        setState: (value: ChangeEvent<HTMLInputElement>) => handleInput(value, name),        ...(!isString && value),      },    };    return { ...fields, ...field };  }, {});  const [fields, setState] = useState<any>(form);  const handleInput = useCallback(    (element: ChangeEvent<HTMLInputElement>, name: string) => {      const input = fields[name];      const value = element.target.value;      const field = { ...input, value };      setState(prevState => ({ ...prevState, [name]: field });    }, [fields, setState]);  const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const values = Object.entries(fields).reduce(((prev: any, [name, {value}]: any) => ({ ...prev, [name]: value })), {});    onSubmit({ values });  }  return {    handleSubmit,    fields,  }}

Типизация


Я не хочу останавливаться на этой теме, так как данный код можно спокойно использовать и без TypeScript, поэтому в коде могут внезапно появляться какие-то типы, чтобы все корректно собрать, в конце статьи будет предоставлена ссылка на github.


Валидация


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


Итак, немного перепишем схему. Здесь указан массив validators для обоих полей и добавлен флаг обязательности поля.


// App.tsx...const formInputs = {  firstName: {    required: true,    validators: [      (s: string) => !s.length && 'Поле обязательно для заполнения',      (s: string) => s.length < 2 && 'Минимальная длина строки 2',      (s: string) => s.length <= 2 && 'А теперь 3',      (s: string) => parseInt(s) < 2 && 'Должна быть цифра больше 1',    ]  },  datetime: {    validators: [      (s: string) => new Date(s).getUTCFullYear() > new Date().getUTCFullYear() && 'Год рождения не может быть больше текущего',    ],  },}

Помимо обновленной схемы добавим в хук переменную isValid, которая будет запрещать/разрешать отправку формы по кнопке. Рядом с полями также будет выводиться ошибка. К слову, ошибку будем выводить только для тронутых полей.


// App.tsx...const { fields, isValid, handleSubmit } = useForm(formInputs);const { firstName, datetime }: Form = fields;return (    <div className="App">      <form onSubmit={handleSubmit(onSubmit)}>        <input type="text" value={firstName.value} onChange={firstName.setState}/>        <span>{firstName.touched && firstName.error}</span>        <input type="date" value={datetime.value} onChange={datetime.setState}/>        <span>{datetime.touched && datetime.error}</span>        <button disabled={!isValid}>Send form</button>      </form>    </div>);

Разберем этот код. В показанной функции ожидается 2 аргумента, field поле инпута, второй опциональный, в нем будут храниться дополнительные свойства для поля. Далее при помощи деструктуризации объекта заводятся переменные value, required и массив валидаций. Чтобы не менять свойства аргумента, заводятся новые переменные error и valid. Я объявил их через let, так как меняю их в процессе. В коде до обхода массива валидаторов стоит проверка на required, в теле условия проверяется значение поля, и там же прокидывается ошибка.


Мы подошли к условию, где проверяем переменную validators. Она должна быть массивом. Далее по коду создаем массив результатов выполнения функций валидации при помощи метода массива map. validateFn здесь хранится функция, в которую передается значение поля из свойства value. Результат выполнения функции валидации должен быть строкой, так как выводить мы будем именно текст ошибки. Если результат не строка, то возвращаться будет что-то другое. Конкретно здесь пустая строка, но там может быть и другое значение, например false. В любом случае далее происходит фильтрация массива результатов для удаления пустых значений. Таким образом, поле ошибки могло бы быть и массивом ошибок. Но здесь я решил выводить лишь одну ошибку, поэтому далее стоит условие проверки массива result, где присваивается ошибка и меняется состояние valid. В конце выполнения функция fieldValidation возвращает новый объект поля, где записаны все переданные ранее значения + новые, модифицированные.


Далее этот метод будет использоваться в функции handleInput и handleSubmit. В обоих случаях будет прогоняться валидация.


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


// hooks/useForm.tsconst [isValid, setFormValid] = useState<boolean>(true);const getFormValidationState = (fields) => Object.entries(fields).reduce((isValid: any, [_, value]: any) => Boolean(isValid * value.isValid), true);const handleInput = (element: ChangeEvent<HTMLInputElement>, name: string) => {    const input = fields[name];    const value = element.target.value;    const field = {      ...input,      value,      touched: true,      isValid: true,    };    const validatedField = fieldValidation(field);    setState((prevState) => {      const items = {...prevState, [name]: validatedField};      setFormValid(getFormValidationState(items));      return items;    });  }  const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const fieldsArray = Object.entries(fields);    // Забираем только значения    const values = fieldsArray.reduce(((prev: any, [name, {value}]: any) => ({ ...prev, [name]: value })), {});    // Валидируем каждый инпут    const validatedInputs = fieldsArray.reduce((prev: any, [name, value]: any) => ({ ...prev, [name]: fieldValidation(value, { touched: true }) }), {});    // Изменяем значение стейта isValid    setFormValid(getFormValidationState(validatedInputs));    setState(validatedInputs);    onSubmit({ values });  }

Теперь у нас полностью готов кастомный хук для простых форм с возможностью валидировать значения ввод пользователя. После некоторых манипуляций на выходе получим такой код:


useForm.ts
import { ChangeEvent, FormEvent, useState } from 'react';type IValidatorFN = (s: string) => {};export interface IField {  value?: string;  type?: string;  label?: string;  error?: string;  isValid?: boolean;  required?: boolean;  touched?: boolean;  setState?: (event: ChangeEvent<HTMLInputElement>) => {};  validators?: IValidatorFN[];}export type ICustomField<T = {}> = IField & T;export type ICustomObject<T = {}> = {  [key: string]: ICustomField & T;}export type IValues = {  [key: string]: string | number;}export type IForm = {  fields: ICustomObject;  isValid: boolean;  handleSubmit: (onSubmit: Function) => (e: FormEvent) => void;}type IOptions = {  [key: string]: any;}export const useForm = (initialFields: ICustomObject): IForm => {  const form = Object.entries(initialFields).reduce((fields, [name, value]: any[]) => {    const isString = typeof value === 'string';    const field = {      [name]: {        type: 'text',        value: (isString && value) || ((!isString && value.value) || ''),        error: (!isString && value.error) || null,        validators: (!isString && value.validators) || null,        isValid: (!isString && value.isValid) || true,        required: (!isString && value.required) || false,        touched: false,        setState: (value: ChangeEvent<HTMLInputElement>) => handleInput(value, name),        ...(!isString && value),      },    };    return {...fields, ...field};  }, {});  const [fields, setState] = useState<ICustomObject>(form);  const [isValid, setFormValid] = useState<boolean>(true);  const getFormValidationState = (fields: ICustomObject): boolean =>    Object.entries(fields).reduce((isValid: boolean, [_, value]: any) => Boolean(Number(isValid) * Number(value.isValid)), true);  const fieldValidation = (field: ICustomField, options: IOptions = {}) => {    const { value, required, validators } = field;    let isValid = true, error;    if (required) {      isValid = !!value;      error = isValid ? '' : 'field is required';    }    if (validators && Array.isArray(validators)) {      const results = validators.map(validateFn => {        if (typeof validateFn === 'string') return validateFn;        const validationResult = validateFn(value || '');        return typeof validationResult === 'string' ? validationResult : '';      }).filter(message => message !== '');      if (results.length) {        isValid = false;        error = results[0];      }    }    return { ...field, isValid, error, ...options };  };  const handleInput = (element: ChangeEvent<HTMLInputElement>, name: string) => {    const input = fields[name];    const value = element.target.value;    const field = {      ...input,      value,      touched: true,      isValid: true,    };    const validatedField = fieldValidation(field);    setState((prevState: ICustomObject) => {      const items = {...prevState, [name]: validatedField};      setFormValid(getFormValidationState(items));      return items;    });  }  const handleSubmit = (onSubmit: Function) => (e: FormEvent) => {    if (e && e.preventDefault) {      e.preventDefault();    }    const fieldsArray = Object.entries(fields);    const values = fieldsArray.reduce(((prev: ICustomObject, [name, { value }]: any) => ({ ...prev, [name]: value })), {});    const validatedInputs = fieldsArray.reduce((prev: ICustomObject, [name, value]: any) => ({ ...prev, [name]: fieldValidation(value, { touched: true }) }), {});    setFormValid(getFormValidationState(validatedInputs));    setState(validatedInputs);    onSubmit({ values });  }  return {    fields,    isValid,    handleSubmit,  }}

App.tsx
import React from 'react';import { useForm, IValues } from './hooks/useForm';const formInputs = {  firstName: {    required: true,    validators: [      (s: string) => !s.length && 'Поле обязательно для заполнения',      (s: string) => s.length < 2 && 'Минимальная длина строки 2',      (s: string) => s.length <= 2 && 'А теперь 3',      (s: string) => parseInt(s) < 2 && 'Должна быть цифра, больше 1',    ],    label: 'First Name',  },  datetime: {    type: 'date',    label: 'Birth Date',    validators: [      (s: string) => new Date(s).getUTCFullYear() > new Date().getUTCFullYear() && 'Год рождения не может быть больше текущего',    ],  },  lastName: {    label: 'Last Name',  },}const App = () => {  const { fields, isValid, handleSubmit } = useForm(formInputs);  const { firstName, datetime, lastName } = fields;  const onSubmit = ({ values }: { values: IValues }) => {    console.log(values, 'submit');  }  const formFields = [firstName, lastName, datetime];  return (    <div className="App">      <form onSubmit={handleSubmit(onSubmit)}>        {formFields.map((field, index) => (          <div key={index}>            <input              type={field.type}              placeholder={field.label}              value={field.value}              onChange={field.setState}            />            <span>{field.touched && field.error}</span>          </div>        ))}        <div>          <button disabled={!isValid}>Send form</button>        </div>      </form>    </div>  );}export default App;

Итог


В данной статье мне хотелось привести пример создания собственного хука для работы с формами. Необязательно для работы с ними ограничиваться локальными состояниями или реакт-хуками. Как я упоминал выше, есть возможности и способы оптимизации текущего примера. Основная проблема в этом коде в том, что он вызывает рендер всего компонента при взаимодействии с полями. От этого можно избавиться, если написать реализацию без контроля состояния полей. Однако не всегда неуправляемый вариант подходит для решения той или иной задачи. На помощь может прийти библиотека rxjs или другие, использующие ее возможности, например, такие как focal или rixio. Но это тема для другой статьи.
Спасибо, что дочитали до конца. Я надеюсь, что после ознакомления с этими примерами у вас улучшилось понимание темы кастомных хуков, в частности, работа с формами.
Проект на github

Подробнее..

Основы Flutter для начинающих (Часть VI)

05.06.2021 14:06:41 | Автор: admin

Когда вы создаете различные формы (например: регистрации или входа) на Flutter, вы не заморачиваетесь с кастомизацией компонентов, потому что вы можете изменить любое поле формы под свой стиль.

Помимо кастомизации, Flutter предоставляет возможность обработки ошибок и валидации полей формы.

И сегодня мы постараемся разобраться с этой темой на небольшом примере.

Ну что ж, погнали!

Наш план
  • Часть 1- введение в разработку, первое приложение, понятие состояния;

  • Часть 2- файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3- BottomNavigationBar и Navigator;

  • Часть 4- MVC. Мы будем использовать именно этот паттерн, как один из самых простых;

  • Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 (текущая статья) - работа с формами, текстовые поля и создание поста.

  • Часть 7 - работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 9 - немного о тестировании;

Создание формы: добавление поста

Для начала добавим на нашу страницу HomePage кнопку по которой мы будем добавлять новый пост:

@overrideWidget build(BuildContext context) {  return Scaffold(    appBar: AppBar(      title: Text("Post List Page"),    ),    body: _buildContent(),    // в первой части мы уже рассматривали FloatingActionButton    floatingActionButton: FloatingActionButton(      child: Icon(Icons.add),      onPressed: () {      },    ),  );}

Далее создадим новую страницу в файле post_add_page.dart:

import 'package:flutter/material.dart';class PostDetailPage extends StatefulWidget {  @override  _PostDetailPageState createState() => _PostDetailPageState();}class _PostDetailPageState extends State<PostDetailPage> {    // TextEditingController'ы позволят нам получить текст из полей формы  final TextEditingController titleController = TextEditingController();  final TextEditingController contentController = TextEditingController();    // _formKey пригодится нам для валидации  final _formKey = GlobalKey<FormState>();    @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post Add Page"),        actions: [          // пункт меню в AppBar          IconButton(            icon: Icon(Icons.check),            onPressed: () {              // сначала запускаем валидацию формы              if (_formKey.currentState!.validate()) {                // здесь мы будем делать запроc на сервер              }            },          )        ],      ),      body: Padding(        padding: EdgeInsets.all(15),        child: _buildContent(),      ),    );  }  Widget _buildContent() {    // построение формы    return Form(      key: _formKey,      // у нас будет два поля      child: Column(        children: [          // поля для ввода заголовка          TextFormField(            // указываем для поля границу,            // иконку и подсказку (hint)            decoration: InputDecoration(                border: OutlineInputBorder(),                prefixIcon: Icon(Icons.face),                hintText: "Заголовок"            ),            // не забываем указать TextEditingController            controller: titleController,            // параметр validator - функция которая,            // должна возвращать null при успешной проверки            // или строку при неудачной            validator: (value) {              // здесь мы для наглядности добавили 2 проверки              if (value == null || value.isEmpty) {                return "Заголовок пустой";              }              if (value.length < 3) {                return "Заголовок должен быть не короче 3 символов";              }              return null;            },          ),          // небольшой отступ между полями          SizedBox(height: 10),          // Expanded означает, что мы должны          // расширить наше поле на все доступное пространство          Expanded(            child: TextFormField(              // maxLines: null и expands: true               // указаны для расширения поля на все доступное пространство              maxLines: null,              expands: true,              textAlignVertical: TextAlignVertical.top,              decoration: InputDecoration(                  border: OutlineInputBorder(),                  hintText: "Содержание",              ),              // не забываем указать TextEditingController              controller: contentController,              // также добавляем проверку поля              validator: (value) {                if (value == null || value.isEmpty) {                  return "Содержание пустое";                }                return null;              },            ),          )        ],      ),    );  }}

Не забудьте добавить переход на страницу формы:

floatingActionButton: FloatingActionButton(   child: Icon(Icons.add),   onPressed: () {      Navigator.push(context, MaterialPageRoute(         builder: (context) => PostDetailPage()      ));   },),

Запускаем и нажимаем на кнопку:

Вуаля! Форма работает.

Небольшая заметка

У новичков могут возникнуть проблемы даже с готовым кодом. И это не издевательство, такое бывает.

Поэтому для 100%-ной работы коды постарайтесь использовать схожие версии Flutter и Dart с моими:

  • Flutter 2.0.6

  • Dart SDK version: 2.12.3

Также в комментах я обратил внимание на null safety. Это очень важно, я позабыл об этом и это мой косяк.

Я уже добавил в приложение поддержку null safety. Вы наверно обратили внимание на восклицательный знак:

// ! указывает на то, что мы 100% уверены// что currentState не содержит null значение_formKey.currentState!.validate()

О null safety и о её поддержи в Dart можно сделать целый цикл статей, а возможно и написать целую книгу.

Мы задерживаться не будем и переходим к созданию POST запроса.

POST запрос для добавления данных на сервер

POST, как уже было отмечено, является одним из HTTP методов и служит для добавления новых данных на сервер.

Для начала добавим модель для нашего результата и изменим немного класс Post:

class Post {  // все поля являются private  // это сделано для инкапсуляции данных  final int? _userId;  final int? _id;  final String? _title;  final String? _body;  // создаем getters для наших полей  // дабы только мы могли читать их  int? get userId => _userId;  int? get id => _id;  String? get title => _title;  String? get body => _body;  // добавим новый конструктор для поста  Post(this._userId, this._id, this._title, this._body);  // toJson() превращает Post в строку JSON  String toJson() {    return json.encode({      "title": _title,      "content": _body    });  }  // Dart позволяет создавать конструкторы с разными именами  // В данном случае Post.fromJson(json) - это конструктор  // здесь мы принимаем объект поста и получаем его поля  // обратите внимание, что dynamic переменная  // может иметь разные типы: String, int, double и т.д.  Post.fromJson(Map<String, dynamic> json) :    this._userId = json["userId"],    this._id = json["id"],    this._title = json["title"],    this._body = json["body"];}// у нас будут только два состоянияabstract class PostAdd {}// успешное добавлениеclass PostAddSuccess extends PostAdd {}// ошибкаclass PostAddFailure extends PostAdd {}

Затем создадим новый метод в нашем Repository:

// добавление поста на серверFuture<PostAdd> addPost(Post post) async {  final url = Uri.parse("$SERVER/posts");  // делаем POST запрос, в качестве тела  // указываем JSON строку нового поста  final response = await http.post(url, body: post.toJson());  // если пост был успешно добавлен  if (response.statusCode == 201) {    // говорим, что все ок    return PostAddSuccess();  } else {    // иначе ошибка    return PostAddFailure();  }}

Далее добавим немного кода в PostController:

// добавление поста// функция addPost будет принимать callback,// через который мы будет получать результатvoid addPost(Post post, void Function(PostAdd) callback) async {  try {    final result = await repo.addPost(post);    // сервер вернул результат    callback(result);  } catch (error) {    // произошла ошибка    callback(PostAddFailure());  }}

Ну что ж пора нам вернуться к нашему представлению PostAddPage:

class PostDetailPage extends StatefulWidget {  @override  _PostDetailPageState createState() => _PostDetailPageState();}// не забываем поменять на StateMVCclass _PostDetailPageState extends StateMVC {  // _controller может быть null  PostController? _controller;  // получаем PostController  _PostDetailPageState() : super(PostController()) {    _controller = controller as PostController;  }  // TextEditingController'ы позволят нам получить текст из полей формы  final TextEditingController titleController = TextEditingController();  final TextEditingController contentController = TextEditingController();  // _formKey нужен для валидации формы  final _formKey = GlobalKey<FormState>();  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: Text("Post Add Page"),        actions: [          // пункт меню в AppBar          IconButton(            icon: Icon(Icons.check),            onPressed: () {              // сначала запускаем валидацию формы              if (_formKey.currentState!.validate()) {                // создаем пост                // получаем текст через TextEditingController'ы                final post = Post(                  -1, -1, titleController.text, contentController.text                );                // добавляем пост                _controller!.addPost(post, (status) {                  if (status is PostAddSuccess) {                    // если все успешно то возвращаемя                    // на предыдущую страницу и возвращаем                    // результат                    Navigator.pop(context, status);                  } else {                    // в противном случае сообщаем об ошибке                    // SnackBar - всплывающее сообщение                    ScaffoldMessenger.of(context).showSnackBar(                      SnackBar(content: Text("Произошла ошибка при добавлении поста"))                    );                  }                });              }            },          )        ],      ),      body: Padding(        padding: EdgeInsets.all(15),        child: _buildContent(),      ),    );  }  Widget _buildContent() {    // построение формы    return Form(      key: _formKey,      // у нас будет два поля      child: Column(        children: [          // поля для ввода заголовка          TextFormField(            // указываем для поля границу,            // иконку и подсказку (hint)            decoration: InputDecoration(                border: OutlineInputBorder(),                prefixIcon: Icon(Icons.face),                hintText: "Заголовок"            ),            // указываем TextEditingController            controller: titleController,            // параметр validator - функция которая,            // должна возвращать null при успешной проверки            // и строку при неудачной            validator: (value) {              // здесь мы для наглядности добавили 2 проверки              if (value == null || value.isEmpty) {                return "Заголовок пустой";              }              if (value.length < 3) {                return "Заголовок должен быть не короче 3 символов";              }              return null;            },          ),          // небольшой отступ между полями          SizedBox(height: 10),          // Expanded означает, что мы должны          // расширить наше поле на все доступное пространство          Expanded(            child: TextFormField(              // maxLines: null и expands: true              // указаны для расширения поля              maxLines: null,              expands: true,              textAlignVertical: TextAlignVertical.top,              decoration: InputDecoration(                  border: OutlineInputBorder(),                  hintText: "Содержание",              ),              // указываем TextEditingController              controller: contentController,              // также добавляем проверку поля              validator: (value) {                if (value == null || value.isEmpty) {                  return "Содержание пустое";                }                return null;              },            ),          )        ],      ),    );  }}

Логика работы следующая:

  1. мы нажаем добавить новый пост

  2. открывается окно с формой, вводим данные

  3. если все ок, то возвращаемся на предыдущую страницу и сообщаем об этом иначе выводим сообщение об ошибке.

Заключительный момент, добавим обработку результата в PostListPage:

floatingActionButton: FloatingActionButton(  child: Icon(Icons.add),  onPressed: () {    // then возвращает объект Future    // на который мы подписываемся и ждем результата    Navigator.push(context, MaterialPageRoute(      builder: (context) => PostDetailPage()    )).then((value) {      if (value is PostAddSuccess) {        // SnackBar - всплывающее сообщение        ScaffoldMessenger.of(context).showSnackBar(         SnackBar(content: Text("Пост был успешно добавлен"))        );      }    });  },),

Теперь тестируем:

К сожалению JSONPlaceholder на самом деле не добавляет пост и поэтому мы не сможем его увидеть среди прочих постов.

Заключение

Я надеюсь, что убедил вас в том, что работа с формами на Flutter очень проста и не требует почти никаких усилий.

Большая часть кода - это создание POST запроса на сервер и обработка ошибок.

Полезные ссылки

Всем хорошего кода)

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru