Продолжаем создавать собственный смарт-калькулятор на базе Реакта. В этой части сделаем базовый интерфейс для подсчета простых выражений при помощи экранной клавиатуры. Разберем основы фреймворка и примитивные возможности визуальной бибилиотеки Chakra UI.
Предыдущая статья: Калькулятор на базе React. Часть 1: Настройка окружения
Создаем первый собственный React-компонент
В Реакте все элементы интерфейса отрисовываются при помощи компонентов в формате JSX (это гибрид форматов, позволяющий прописывать логику и базовую верстку в одном блоке кода). И мы сейчас создадим свой компонент, а точнее отредактируем уже существующий, заменив весь код на собственный.
Работать мы будем в файле App.jsx – это точка доступа. То есть ключевой файл, который отображается в HTML-верстке нашего приложения. Все дополнительные компоненты вкладываются в App и только после этого становятся доступны пользователю. Сам App представляет собой JavaScript-функцию.
Так что наше приложение в первую очередь будет описано в файле App.jsx. Оттуда и начнем.
-
Открываем файл App.jsx (он находится в директории src проекта create-react-app).
-
Удаляем весь существующий код внутри функции App кроме div'a с классом App.
-
Импортируем в нашу программу тег Box из библиотеки Chakra UI:
import { Box } from '@chakra-ui/react'
Теперь мы можем использовать объект Box для отрисовки интерфейса. Это абстрактный тег, использующийся в качестве альтернативы для div. Зачем стандартным тегам вообще нужна альтернатива, вы скоро поймете.
-
Затем для примера создаем обертку Box:
<Box> </Box>
Ее внутренности заменим на еще один блок из Чакры. На этот раз Text. Только его сначала тоже нужно импортировать. И здесь становится ясно, в чем преимущество компонентов Чакры над стандартными тегами HTML. Мы можем управлять их визуальной составляющей прямо в разметке интерфейса, не переходя к другим файлам.
В классическом варианте Реакта стили прописываются в отдельном файле, так же, как в случае с простым стеком HTML, CSS и JS. Chakra UI позволяет все важные составляющие разработки объединить в одной среде.
На скриншоте ниже видно, что я поменял фон у Box и размер текста у Text. Значения можно использовать как стандартные (доступные в CSS), так и специальные, доступные в Чакре (они создавались на основе фреймворка Tailwind).
Это пример элементарного Реакт-приложения. Оно пока ничего не делает, кроме отображения текста, но мы только начинаем.
Добавляем цифры
Очевидно, чтобы от калькулятора был прок, нужны цифры. Для их добавления импортируем еще один элемент из Чакры. На этот раз Button. Все импортированные элементы на текущий момент будут выглядеть следующим образом:
import { Box, Text, Button } from '@chakra-ui/react'
Следующий шаг – создание функции, которая будет непосредственно рисовать интерфейс с цифрами (напоминаю, что в Реакте все элементы интерфейса это либо классы, либо функции – мы используем функции в угоду удобства).
Вот как она может выглядеть:
-
Начинаем с объявления функции (что такое props, скоро разберем).
function Numbers (props) { }
-
Затем перебираем массив с цифрами от 0 до 9, чтобы нарисовать их в интерфейсе. Можно было бы прописать каждую букву вручную, но это трудозатратно, да еще и помешает дальнейшему улучшению нашей программы или изменению какой-либо логики. Поэтому мы создаем массив, перебираем его и каждый элемент массива возвращаем как кнопку.
const nums = Array.from(Array(10).keys()).map( number => { return <Button> </Button> }
Пока это пустые блоки без контента. Чтобы в них появился контент, надо прописать внутри значение number, являющееся переменной в выражении map.
<Button key={number}> {number} </Button>
key – это атрибут, необходимый для корректной перерисовки компонентов в интерфейсе Реакта. У каждого элемента в списках должен быть уникальный ключ.
Так как в функции map мы возвращаем Button, то получаем в ответ 10 кнопок, который можно отобразить в интерфейсе, сделав возврат из функции Numbers. Из нее мы вернем Box со всеми числами, описанными выше.
Получится:
return <Box> {nums} </Box>
Функция является компонентом, и этот компонент можно добавить в другую функцию, чтобы отрисовать его в интерфейсе программы. Например, в нашем случае пользователь видит только содержимое App, а Numbers для него скрыт.
Это легко исправляется добавлением специального тега в код App. Просто пишем в div <Numbers />. Такой вид тегов обозначает, что в функцию добавлен компонент. Теперь в App отображается вся логика, описанная в Numbers.
Теперь можно потратить немного времени на базовую стилизацию.
-
Нам нужно указать точный размер кнопок с цифрами. Поэтому указываем атрибуты w и h для элемента Button внутри Numbers.
-
Также стоит сократить размер всего блока с цифрами. Поэтому укажем точное значение ширины для элемента Box внутри Numbers.
-
Компонент <Numbers /> стоит обернуть в Box, чтобы центрировать его и не дать остальным элементам интерфейса разъехаться по экрану.
На текущем этапе получится подобный набор кнопок. Пока они неактивны и не выполняют никакой функции, но понажимать их уже можно. Chakra UI даже стилизовала их для нас, сделав меняющийся фон при наведении.
Добавляем логику подсчета выражений
Теперь надо научить калькулятор считать хотя бы базовые выражения. Поделим эту работу на несколько частей.
-
Создадим внутри App логику для отображения введенных значений и результата вычислений.
-
Добавим в Numbers функцию передачи цифр в поле для подсчета результатов.
-
Добавим клавиши для сложения, умножения, деления и вычитания чисел.
-
Автоматизируем процесс подсчета (создадим систему моментального вывода результата).
-
Реализуем кнопку «Равно» для ручного вывода результата выражения.
Начнем с первой задачи и сразу же разберем одну из важнейших концепций Реакта. Каждый компонент в интерфейсе на базе React имеет состояние. Это какое-то значение, которое при изменении влияет на визуальную составляющую программы. Грубо говоря, если у вас есть программа для подсчета выпитых стаканов воды, то как раз поглощенный литраж и будет состоянием интерфейса.
В нашем случае будет два состояния. Одно для значений в поле подсчета результатов и одно для самого результата вычислений.
-
Импортируем в наше приложение функцию useState:
import { useState } from 'react'
-
Затем прописываем первое состояние:
const [counts, setCounts] = useState('0')
-
И второе:
const [result, setResult = useState('')
Логика тут довольно простая. В массиве хранится переменная со значением состояния и функция set для изменения переменной. А в useState хранится значение по умолчанию. Пока этого знать достаточно.
Далее сделаем интерфейс для отображения поля с подсчетом и поля с результатом работы калькулятора. Можете стилизовать их как угодно, но я предпочитаю создавать имитацию единого поля, как в обычных калькуляторах, поэтому объединяю переменные count и result в один Box, а потом разбрасываю их по краям с помощью свойства justifyContent="space-between". Сами переменные рекомендую обернуть в блоки Text и задать им динамическую ширину w="fit-content". Тогда они точно займут нужное пространство в интерфейсе.
Теперь поговорим о пропсах (аргумент props, что мы видели ранее в функции Numbers). Это атрибут, позволяющий передать информацию из одного компонента в другой. Мы словно говорим одной функции из другой, что у нас есть определенные данные и методы, которые ты тоже можешь использовать. Так в Реакте и других фреймворках выстраиваются взаимоотношения между различными элементами интерфейса (даже далекими и не особо зависящими друг от друга).
Мы будем использовать пропсы для двух вещей: для передачи в Numbers информации о состоянии переменной counts (это наше поле для вычислений), а также для передачи в Numbers метода setCounts, позволяющего добавить новые значения в counts.
Для этого допишем пропсы в том месте, где мы добавляем компонент Numbers в App.
<Numbers data={counts} onClick={setCounts}
Пропсы можно называть любыми именами. Я предпочитаю называть практически любую передаваемую информацию data, но это работает только на небольших компонентах. Есть определенные конвенции – например, функции, запускаемые по клику, передаются в компоненты через пропсы с названием onClick.
Теперь мы можем использовать пропсы внутри компонента Numbers, обращаясь к ним через объект props.
Мы создадим функцию для добавления новых цифр. Пропишем ее в атрибуте onClick, отвечающем за событие «щелчок». Добавим туда логику для обновления состояния data из компонента App, чтобы в нем отображались все нажатые цифры.
onClick = { (e) => {props.onClick(props.data + e.target.innerHTML)} }
Теперь каждая кнопка, генерируемая в Numbers, добавляет в data свое значение.
Заметьте, что onClick фигурирует дважды, но один из них является атрибутом React, а другой добавлен через props и является неким мостом между функцией в App и кнопкой-активатором в Numbers.
Но у нас есть проблема. Значение по умолчанию равняется 0. Так как мы работаем не с цифрами, а со строками, то при сложении 0 с другими значениями он никуда не удаляется, и мы получаем значение, начинающееся с нуля, что приведет к ошибке при попытке провести расчет.
Это можно исправить, если добавить небольшую проверку на наличие нуля в строке вычислений. Если 0 есть, то значение в поле попросту заменяется на новое, а если 0 нет, то число добавляется к уже существующим.
Код выглядит так:
-
Сначала пишем логику для работы без нуля:
if (props.data != '0') props.onClick(props.data + e.target.innerHTML)
-
А потом и для обратной ситуации:
else props.onClick(e.target.innerHTML)
Мы научились добавлять изображения и можем избавляться от ненужных нулей. Следующий этап – базовые расчеты. Начнем со сложения.
Добавим в App компонент с названием CountButton (его пока не существует, но скоро мы его создадим). У этого компонента будет сразу три пропса:
-
data – это наше поле для расчетов.
-
expression – выражение, используемое для подсчетов.
-
onClick – функция, активируемая в App при клике на CountButton.
Содержимое CountButton выглядит следующим образом:
-
Создаем функцию CountButton:
function CountButton (props) { }
-
В теле пока что оставляем только возврат кнопки:
return ( <Button onClick = {() => {props.onClick(props.data + props.expression)}}> </Button> )
В этой CountButton происходит то же, что и в Numbers, но грядет несколько нюансов.
Мы создадим дополнительный метод, высчитывающий результат работы калькулятора при вводе новых значений. То есть еще до нажатия на кнопку «Равно», чтобы у пользователя всегда была актуальная информация о проводимых расчетах.
-
Для этого сделаем функцию applyExpressions:
function applyExpression
-
Аргументом функции будет countedNumber. Будем брать его значение извне.
-
В теле функции переназначаем состояние counts на аргумент applyExpression:
setCounts(countedNumber)
-
И меняем состояние переменной result на результат вычисления примера, прописанного в counts.
setResult(eval(counts))
Теперь при вводе знака плюс будет автоматически проводиться расчет примера в counts. На скриншоте ниже видно, как это работает.
Остается продублировать вновь созданный компонент, заменив атрибут expression на другие выражения.
Но есть проблема. Она связана с тем, что функция eval – проблемная сама по себе. Часто приводит к ошибкам и ломает приложения, если не создать идеальные условия для ее работы. Одной из причин поломок может стать ввод некорректного выражения в поле для подсчета. Например, указав два плюса подряд, вы убьете React-приложение. Поэтому нужно либо отказаться от eval, либо предотвратить шанс на появление двух плюсов или минусов (или любых других выражений) подряд.
-
Внутри CountButton создадим переменную expressions с регулярным выражением, содержащим +, - и другие «опасные» символы.
const expressions = /\+|\-|\/|\*| /
-
Затем объявляем переменную lastNumber, указывающую на последний знак в counts:
const lastNumber = props.data[props.data.length - 1]
-
Создаем метод для проверки выражения, чтобы оно не попадало в список запрещенных. Так удастся предотвратить повторный ввод неподходящих знаков:
if (expressions.test(lastNumber)) return
Пока что имеем следующий интерфейс. В нем плохо лишь то, что кнопки с +, - и т.п. уползли вниз.
Я рекомендую объединить цифры и элементы CountButton в единый Box и добавить к нему свойство display="flex", чтобы выставить оба блока вровень, а набор CountButton закинуть в другой Box и сделать из него еще один flex, но уже с дополнительным атрибутом flexDirection="column", чтобы кнопки выстроились в ряд.
Получится вот такой, на мой взгляд, более лаконичный интерфейс, хоть и далекий от идеала.
Нам тут не хватает только кнопки =.
У нее несложный код. Надо добавить в интерфейс кнопку, а к этой кнопке прикрепить метод, идентичный тому, что используется при подсчете итогового результата при вводе выражения. То есть надо скопировать тот же код, что в applyExpression, но использовать его в кнопке =.
onClick={() => {setResult(eval(counts))}}
Вместо заключения
Что ж, на этом можно было бы закончить весь цикл, потому что калькулятор уже готов. С помощью этого интерфейса уже можно считать. Но в нашем случае это только начало. Предстоит внедрить немало умных функций и сделать калькулятор достойным инструментом. К тому же не забываем, что наш калькулятор будет еще и конвертером.
Продолжение: Калькулятор-конвертер на базе React. Часть 3: Автоматический подсчет результатов
Комментарии