DevGang
Авторизоваться

Секреты стилизации форм с использованием AWS Amplify Studio

Формы обманчиво сложны. Когда вы создаете форму для своего сайта, вам необходимо принять множество решений. Необходимо позаботиться о валидации, структуре и способе передачи данных в бэкэнд. Требуется решить, как обрабатывать ошибки, как стилизовать и настраивать форму. Важно обеспечить доступность форм. В этом может помочь AWS Amplify Studio (https://docs.amplify.aws/console/formbuilder/overview/)  с ее конструктором форм!

В этой статье я расскажу о том, как с помощью Amplify Studio добавить новую форму, подключенную к источнику данных AWS Appsync (https://docs.amplify.aws/console/data/data-model/). Мы изучим, как настроить эту форму, добавив темный/светлый режим. Также рассмотрим, как можно дополнительно настроить правила валидации и отредактировать сообщения об ошибках! Затем мы протестируем наши отправления, отобразив результаты с помощью JavaScript GraphQL API-библиотеки Amplify.

Если вы хотите посмотреть готовый код, пожалуйста, посмотрите этот репозиторий

Начало работы

Мы создадим базовую форму Todo с помощью студии Amplify.

Для начала войдите в свою консоль AWS, найдите AWS Amplify и щелкните по ней. Если вы впервые используете Amplify, выберите Get Started и еще раз нажмите кнопку Get started под Amplify Studio. Если вы уже создали приложение Amplify в этом регионе, то можно щелкнуть New appBuild an app. Выберите имя приложения, нажмите Confirm deployment (Подтвердить развертывание). После этого нажмите кнопку Launch Studio, чтобы начать работу!

Если вы уже являетесь пользователем Amplify Studio, запустите Studio из имеющегося приложения. Эта функция доступна для всех.

Данные

Давайте создадим в Amplify Studio источник данных, который мы сможем использовать с нашей формой. Щелкните в меню пункт Data.

Нажмите кнопку Add model и создайте Todo. Добавьте поля completed и title.

После добавления этих моделей обязательно нажмите кнопку Save and Deploy (Сохранить и развернуть) в правом верхнем углу. Развертывание может занять несколько минут.

После развертывания модели вы увидите страницу с инструкциями по извлечению последней клиентской конфигурации в ваше React-приложение. Обязательно скопируйте команду pull.

Настройка приложения Next.js

Теперь, когда у нас есть данные и формы, мы можем перенести информацию в существующее приложение. Я предполагаю, что вы уже создали новое приложение Next.js. Если нет, и вы запускаете новое приложение Next.js, убедитесь, что маршрутизатор App выключен.

Откройте свое приложение и установите эти библиотеки с помощью вашего любимого менеджера пакетов.

$ npm install @aws-amplify/ui-react aws-amplify react-icons

Если он еще не установлен, убедитесь, что вы также выполнили глобальную установку Amplify CLI.

$ npm install @aws-amplify/cli -g

Если вы впервые используете Amplify, обязательно обновите CLI до последней версии.

$ amplify upgrade

Далее возьмите команду, скопированную из предыдущего раздела, и вставьте в терминал, запустив её в руте своего проекта Next.js.

$ amplify pull --appId <replace-this-with-your-id> --envName <replace-with-env-name>

Теперь в проекте должно появиться несколько новых файлов и каталогов.

Поскольку мы работаем с AppSync, вам потребуется создать несколько файлов запросов GraphQL.

$ amplify codegen add

В результате будут сгенерированы некоторые полезные файлы запросов/подписок/мутаций GraphQL, которые мы будем использовать далее в этом руководстве.

Давайте настроим Amplify, чтобы мы могли использовать наши новые формы!

Примечание: если вы используете Next.js с маршрутизатором приложений или используете Vite, вам может потребоваться небольшая дополнительная настройка. Пожалуйста, следуйте руководству по использованию Next.js или Vite.

Добавление нашей формы и её настройка

Давайте начнем настраивать наше приложение Next, чтобы оно могло взаимодействовать с нашим бэкендом AppSync.

В главном файле точки входа приложения добавьте эти строки, чтобы настроить Amplify на нужные стили.

import { Amplify } from "aws-amplify";
import awsExports from "../aws-exports";
Amplify.configure(awsExports);
import "@aws-amplify/ui-react/styles.css";

Далее мы добавим специальный файл темы. Этот файл используется для настройки глобальных тем во всем приложении. В нашем случае мы хотим переопределить стандартную тему наших форм для поддержки dark режима. Для этого будет использован параметр defaultDarkModeOverride.

Создайте новый файл theme.tsx в папке src.

import { Theme, defaultDarkModeOverride } from "@aws-amplify/ui-react";

export const theme: Theme = {
  name: "theme",
  overrides: [defaultDarkModeOverride],
};

Далее мы добавим этот провайдер Theme в новый файл Layout. В папке src/components создадим новый файл Layout.tsx. Внутри этого файла мы добавим новую переменную colorMode useState, которую будем использовать для изменения цвета темы.

ThemeProvider обрамляет наше приложение и предоставляет ему возможности создания тем с помощью маркеров оформления. Эти маркеры могут полностью переопределить внешний вид нашего приложения.

import { theme } from "@/theme";
import { ColorMode, ThemeProvider } from "@aws-amplify/ui-react";
import React from "react";


export default function Layout({ children }: React.PropsWithChildren) {
  const [colorMode, setColorMode] = React.useState<ColorMode>("light");
  return (
    <ThemeProvider theme={theme} colorMode={colorMode}>
        {children}
    </ThemeProvider>
  )
}

Добавим кнопку-переключатель, чтобы пользователи могли выбирать между темным и светлым режимом. Для этих значков мы будем использовать набор react-icons/md.

import { theme } from "@/theme";
import { MdOutlineDarkMode, MdOutlineLightMode } from "react-icons/md";
import { ColorMode, Flex, ThemeProvider, ToggleButton, ToggleButtonGroup } from "@aws-amplify/ui-react";
import React from "react";


export default function Layout({ children }: React.PropsWithChildren) {
  const [colorMode, setColorMode] = React.useState<ColorMode>("light");
  return (
    <ThemeProvider theme={theme} colorMode={colorMode}>
      <Flex direction="column" gap="0" width="100vw" height="100vh" backgroundColor="background.primary">
          <ToggleButtonGroup
            value={colorMode}
            size="small"
            isExclusive
            onChange={(value) => setColorMode(value as ColorMode)}
          >
            <ToggleButton value="light">
              <MdOutlineLightMode />
            </ToggleButton>
            <ToggleButton value="dark">
              <MdOutlineDarkMode />
            </ToggleButton>
          </ToggleButtonGroup>
        {children}
      </Flex>
    </ThemeProvider>
  )
}

Группа ToggleButtonGroup входит в набор примитивов пользовательского интерфейса, предлагаемых библиотекой @aws-amplify/ui-react. Она создает приятную на вид кнопку переключения. При каждом нажатии на кнопку переключения устанавливается режим цвета (colorMode), который обновляется в ThemeProvider. В результате экран будет меняться между темным и светлым режимами.

Наконец, мы добавим компонент AppHeader, который поможет прикрепить макет к левому верхнему углу. Мы также добавим красивую иконку, используя примитив компонента Icon.

import { theme } from "@/theme";
import { MdOutlineDarkMode, MdOutlineLightMode } from "react-icons/md";
import {
  ColorMode,
  Flex,
  Icon,
  ThemeProvider,
  ToggleButton,
  ToggleButtonGroup,
} from "@aws-amplify/ui-react";
import React from "react";

function AppHeader({ children }: React.PropsWithChildren) {
  return (
    <Flex
      as="header"
      direction="row"
      justifyContent="space-between"
      alignItems="center"
      padding="1rem"
      boxShadow="small"
      position="sticky"
      top="0"
      left="0"
      width="100%"
      backgroundColor="background.primary"
    >
      {children}
    </Flex>
  );
}

export default function Layout({ children }: React.PropsWithChildren) {
  const [colorMode, setColorMode] = React.useState<ColorMode>("light");
  return (
    <ThemeProvider theme={theme} colorMode={colorMode}>
      <Flex
        direction="column"
        gap="0"
        width="100vw"
        height="100vh"
        backgroundColor="background.primary"
      >
        <AppHeader>
          <Icon
            color="brand.primary.60"
            fontSize="xl"
            paths={[
              {
                d: "M10.8484 4.19838C10.7939 4.2926 10.7939 4.40867 10.8484 4.50288L21.3585 22.6711C21.413 22.7653 21.5138 22.8233 21.6228 22.8233H23.9901C24.225 22.8233 24.3718 22.5696 24.2543 22.3666L12.5605 2.15225C12.4431 1.94925 12.1495 1.94925 12.0321 2.15225L10.8484 4.19838Z",
              },
              {
                d: "M15.2084 22.6711C15.2629 22.7653 15.3636 22.8233 15.4726 22.8233H17.8461C18.081 22.8233 18.2278 22.5696 18.1104 22.3666L9.48857 7.46259C9.37113 7.25959 9.07755 7.25959 8.96011 7.46259C6.09213 12.4203 3.21732 17.4003 0.336955 22.3816C0.219574 22.5846 0.366371 22.8383 0.601212 22.8383H11.7185C11.9533 22.8383 12.1001 22.5846 11.9827 22.3816L10.8455 20.4158C10.791 20.3216 10.6903 20.2635 10.5813 20.2635H4.8952C4.77776 20.2635 4.70437 20.1367 4.76308 20.0352L9.0912 12.5534C9.14991 12.4519 9.29671 12.4519 9.35542 12.5534L15.2084 22.6711Z",
              },
            ]}
          />
          <ToggleButtonGroup
            value={colorMode}
            size="small"
            isExclusive
            onChange={(value) => setColorMode(value as ColorMode)}
          >
            <ToggleButton value="light">
              <MdOutlineLightMode />
            </ToggleButton>
            <ToggleButton value="dark">
              <MdOutlineDarkMode />
            </ToggleButton>
          </ToggleButtonGroup>
        </AppHeader>
        {children}
      </Flex>
    </ThemeProvider>
  );
}

Нам необходимо обновить файл _app.tsx, чтобы наш новый макет обрамлял приложение.

...
export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

Включите темный режим и посмотрите на него в действии.

До:

После:

Добавление наших форм и их настройка

Теперь добавим нашу форму в файл index.tsx основного маршрута в нашем приложении Next.js.

Импортируйте форму на страницу.

import TodoForm from "@/ui-components/TodoCreateForm";

export default function Home() {

return <TodoForm/>

}

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

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

 <TodoForm
    padding="0"
    onValidate={{
       title: (value, validateResponse) => {
         if (value.trim().length === 0) {
             return {
             hasError: true,
               errorMessage: "Please enter a value!",
             };
          }
          return validateResponse;
       },
     }}
  />

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

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

const [showSuccess, setShowSuccess] = useState(false);
  ...
        <TodoForm
          padding="0"
          onValidate={{
            title: (value, validateResponse) => {
              if (value.trim().length === 0) {
                return {
                  hasError: true,
                  errorMessage: "Please enter a value!",
                };
              }
              return validateResponse;
            },
          }}
          onSuccess={() => {
            setShowSuccess(true);
            setTimeout(() => {
              setShowSuccess(false);
            }, 2000);
          }}
        />
        {showSuccess && <Alert variation="success">Todo added!</Alert>}

Обработчик onSuccess сработает, как только форма будет успешно отправлена. После этого мы можем показать оповещение! Alert - это еще один примитив пользовательского интерфейса @aws-amplify/ui-react.

Теперь, когда у нас есть способ добавления todo, нам нужен способ их отображения.

Давайте добавим метод, который будет захватывать все todo. Для этого будет использоваться GraphQL API библиотеки Amplify JS.

import { API, graphqlOperation } from "aws-amplify";
import { GraphQLQuery, GraphQLSubscription } from "@aws-amplify/api";
import { ListTodosQuery, Todo, OnCreateTodoSubscription } from "@/API";
import { listTodos } from "@/graphql/queries";
...
const [todos, setTodo] = useState<Todo[]>([]);


const getTodos = async () => {
    const allTodos = await API.graphql<GraphQLQuery<ListTodosQuery>>({
      query: listTodos,
    });

    const filteredTodos = allTodos.data?.listTodos?.items
      .filter((todo) => !todo?._deleted)
      .sort(
        (a, b) =>
          new Date(a?.createdAt!).getTime() - new Date(b?.createdAt!).getTime()
      );

    setTodo(filteredTodos as Todo[]);
  };

Поскольку мы создавали наше приложение с помощью Studio и конструктора форм, то по условию наши формы используют DataStore и conflict resolution, включенное в фоновом режиме. Разрешение конфликтов создает версионный источник данных, который расширяет объектную модель данных с помощью метаданных. На данный момент отключить эту функцию при использовании Form Builder и Studio невозможно.

Это означает, что наша модель данных имеет несколько дополнительных метаполей, включая _deleted и _version. Они были автоматически добавлены в нашу модель данных при ее создании. При удалении записи они не удаляются, как можно было бы ожидать. Вместо этого поле _deleted устанавливается в true, и запись остается. Помните об этом, когда работаете с данными с включенным conflict resolution. Именно поэтому в коде необходимо отфильтровать записи, для которых значение _deleted установлено в true, чтобы случайно не показать удаленные данные.

Полезно знать, что если вы решите обновить или удалить какую-либо запись с помощью Amplify GraphQL API, то, поскольку разрешение конфликтов включено, вы также должны включить в GraphQL-ввод значение _version. Поле _version будет увеличиваться при каждом изменении записи. Вам необходимо отслеживать поле _version при обновлении или удалении элемента.

Чтобы гарантировать правильный порядок, мы сортируем по времени createdAt.

Теперь, когда у нас есть способ захвата данных, мы создадим подписку GraphQL, которая будет загружаться сразу после загрузки приложения с помощью UseEffect.

import { API, graphqlOperation } from "aws-amplify";
import { GraphQLQuery, GraphQLSubscription } from "@aws-amplify/api";
import { ListTodosQuery, Todo, OnCreateTodoSubscription } from "@/API";
import { onCreateTodo } from "@/graphql/subscriptions";
import { useEffect, useState } from "react";
...

const [todos, setTodo] = useState<Todo[]>([]);

useEffect(() => {
    getTodos();
    const sub = API.graphql<GraphQLSubscription<OnCreateTodoSubscription>>(
      graphqlOperation(onCreateTodo)
    ).subscribe({
      next: ({ provider, value }) => {
        setTodo((prevValue) => [
          ...prevValue,
          value.data?.onCreateTodo as Todo,
        ]);
      },
      error: (error) => console.warn(error),
    });

    return () => sub.unsubscribe();
  }, []);

Сначала мы запускаем getTodos, а затем запускаем нашу подписку. Как только создается новый Todo, подписка срабатывает. Затем мы можем использовать setTodo для сохранения данных в массив. В качестве альтернативы мы могли бы снова вызывать getTodos при каждом изменении подписки, однако в рамках данной статьи мы будем манипулировать массивом todos и добавлять в него новый Todo.

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

import {
  Alert,
  Card,
  Collection,
  Flex,
  Heading,
  Text,
  useTheme,
} from "@aws-amplify/ui-react";
import TodoForm from "@/ui-components/TodoCreateForm";
import { API, graphqlOperation } from "aws-amplify";
import { GraphQLQuery, GraphQLSubscription } from "@aws-amplify/api";
import { ListTodosQuery, Todo, OnCreateTodoSubscription } from "@/API";
import { listTodos } from "@/graphql/queries";
import { onCreateTodo } from "@/graphql/subscriptions";
import { useEffect, useState } from "react";

export default function Home() {
  const [showSuccess, setShowSuccess] = useState(false);
  const [todos, setTodo] = useState<Todo[]>([]);

  const getTodos = async () => {
    const allTodos = await API.graphql<GraphQLQuery<ListTodosQuery>>({
      query: listTodos,
    });

    const filteredTodos = allTodos.data?.listTodos?.items
      .filter((todo) => !todo?._deleted)
      .sort(
        (a, b) =>
          new Date(a?.createdAt!).getTime() - new Date(b?.createdAt!).getTime()
      );

    setTodo(filteredTodos as Todo[]);
  };

  useEffect(() => {
    getTodos();
    const sub = API.graphql<GraphQLSubscription<OnCreateTodoSubscription>>(
      graphqlOperation(onCreateTodo)
    ).subscribe({
      next: ({ provider, value }) => {
        setTodo((prevValue) => [
          ...prevValue,
          value.data?.onCreateTodo as Todo,
        ]);
      },
      error: (error) => console.warn(error),
    });

    return () => sub.unsubscribe();
  }, []);

  const { tokens } = useTheme();

  return (
    <Flex
      direction="row"
      height="100%"
      width="100%"
      justifyContent="stretch"
      gap="0"
    >
      <Flex
        direction="column"
        gap="medium"
        padding="xxl"
        backgroundColor="background.primary"
      >
        <Text>New ToDo</Text>
        <TodoForm
          padding="0"
          onValidate={{
            title: (value, validateResponse) => {
              if (value.trim().length === 0) {
                return {
                  hasError: true,
                  errorMessage: "Please enter a value!",
                };
              }
              return validateResponse;
            },
          }}
          onSuccess={() => {
            setShowSuccess(true);
            setTimeout(() => {
              setShowSuccess(false);
            }, 2000);
          }}
        />
        {showSuccess && <Alert variation="success">Todo added!</Alert>}
      </Flex>
      <Flex
        direction="column"
        flex="1"
        padding="xxl"
        backgroundColor="background.secondary"
      >
        <Heading level={3}>List of Todos</Heading>

        <Collection gap="small" type="list" items={todos}>
          {(todo) => (
            <Card
              variation="elevated"
              color={tokens.colors.brand.primary[100]}
              marginTop="1rem"
              padding="1rem"
              key={todo.id}
              textDecoration={todo?.completed ? "line-through" : ""}
            >
              {todo?.title}
            </Card>
          )}
        </Collection>
      </Flex>
    </Flex>
  );
}

Давайте посмотрим на него в действии:

И с темным режимом, после нажатия на значок темного режима!

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

Удаление

После завершения работы с приложением можно удалить все ресурсы, выполнив команду amplify delete.

Заключение

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

Источник:

#Начинающим #AWS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу