Продолжаем цикл по разработке калькулятора-конвертера. Большую часть нужных опций в калькулятор мы уже добавили, пришло время разобраться с тем, как мог бы работать конвертер. Начнем с простейшей конвертации расстояния и постепенно будем добавлять другие виды метрик и формулы.
Предыдущий материал: Калькулятор-конвертер на базе React. Часть 5: Добавляем историю результатов
Создаем меню для выбора типа интерфейса
Мы не можем просто встроить конвертер в уже существующий интерфейс калькулятора. Нам необходимо сделать отдельный интерфейс и специальное меню, чтобы можно было переключиться в конвертер и обратно. Поэтому начнем с реализации такого меню и декомпозиции компонента App на новые составные части.
Сначала загрузим коллекцию иконок в стиле Чакры, чтобы добавить значок «Бургер» меню в качестве кнопки для переключения в другие режимы. Для этого откроем терминал в корневой директории проекта (если вы работаете в VS Code, то достаточно нажать на сочетание клавиш Ctrl + `). В командную строку вводим команду для установки иконок Chakra.
npm install @chakra-ui/icons
Теперь мы можем экспортировать иконки в Реакт-компоненты.
-
В файле App.jsx создаем импорт иконки «Бургера».
import { HamburgerIcon } from '@chakra-ui/icons'
-
Затем вставляем эту иконку в функцию App, чтобы она отобразилась в интерфейсе приложения (стили можете не применять, я увеличил размер иконки, потому что по умолчанию она слишком мелкая).
<HamburgerIcon />
Иконка есть, и теперь нам нужно меню для переключения между конвертером и калькулятором. Для этого создадим функцию Menu в файле App.jsx, а внутрь поместим примитивный блок Box, в котором будет размещаться интерфейс меню.
Меню для выбора режима работы программы может представлять собой простой список с переключателями. Чтобы с ним работать, импортируем еще два типа элементов из Чакры:
import { ListItem, UnorderedList } from '@chakra-ui/react'
Из вновь добавленных компонентов соберем неупорядоченный список из трех пунктов меню: Calculator, Converter, Settings.
Первый у нас уже готов, второй будет изготовлен в ближайшее время, а третьим займемся в конце. Получится следующий шаблон:
<UnorderedList> <ListItem>Calculator</ListItem> <ListItem>Converter</ListItem> <ListItem>Settings</ListItem> </UnorderedList>
Следующий этап – добавление анимации появления меню. По умолчанию оно отображается на постоянной основе, но нам нужно скрывать дополнительные опции до тех пор, пока они не понадобятся пользователю. В этом нам помогут компоненты SlideFade и useDisclosure из библиотеки Chakra. Импортируем их в свое приложение:
import { SlideFade, useDisclosure } from '@chakra-ui/react'
В компонент SlideFade затем нужно обернуть весь интерфейс меню. То есть все, что входит в блок Box в компоненте Menu.
Далее нужно создать особое состояние (уникальное для интерфейса Chakra) useDisclosure в компоненте Menu, чтобы контролировать появление и исчезновение меню, замкнутое в SlideFade. Оно содержит в себе переменную и метод.
const {isOpen, onToggle} = useDisclosure()
Теперь нужно привязать элемент isOpen в качестве атрибута для компонента SlideFade. Это необходимо, чтобы меню отображалось в интерфейсе только в случае задействования переменной isOpen.
<SlideFade in={isOpen}> </SlideFade>
А метод onToggle привязывает к иконке бургер-меню.
<HamburgerIcon onClick={onToggle} />
Также добавим атрибут unmountOnExit в блок SlideFade, чтобы он демонтировался из дерева HTML-элементов и не был доступен в «невидимом» состоянии. В противном случае пользователь может случайно кликнуть на какой-то из пунктов настроек, хотя на экране этого блока вовсе нет.
Само меню необходимо встроить в компонент App и поставить его над калькулятором.
Рекомендую к блоку Box в меню добавить атрибут position со значением absolute, чтобы список пунктов в SlideFade появлялся на экране и не сдвигал параллельно другие части интерфейса.
Также, возможно, лучше использовать такую вещь, как unstyled list, то есть вместо UnorderedList импортировать в файл List и обернуть меню именно в него. Тогда из интерфейса исчезнут ненужные точки и элементы списка перестанут сдвигаться в сторону.
Теперь у нас есть все необходимые компоненты, и можно переходить к реализации логики переключения режимов работы приложения.
Делаем переключатель режимов работы приложения
Логика для нашего меню может быть похожа на ту, что мы использовали для переключения между InputCalc и ClickCalc. Так и сделаем – будем менять состояние компонента App и на его основании выбирать, какой из интерфейсов отрисовывать на экране.
Для этого создаем состояние mode внутри функции App.
const [mode, setMode] = useState('Calculator')
Здесь же объявляем переменную application для хранения компонента, отображаемого в текущий момент времени.
Далее можно заниматься простым копированием собственного кода из других участков приложения. У нас уже есть один переключатель, можно повторить его и в файле App. Делаем функцию chageAppType.
function changeAppType () { mode == 'Calculator' ? setMode('Converter') : setMode('Calculator') }
Как видите, логика повторяется – проверяем активный тип компонента в текущий момент и заменяем его, если он не соответствует требованиям пользователя.
Добавляем конструкцию switch. Точнее копируем ее из Calculator.jsx, заменяя только используемые переменные. Например, application вместо calculator и Converter вместо ClickCalc.
Заменяем компонент <Calculator /> на переменную {application}. А вот в компонент <Menu /> нужно будет пробросить пропс с функцией changeAppType.
<Menu onClick={changeAppType} />
При такой логике возникает небольшая неурядица. На какой пункт списка мы не нажали бы, все равно произойдет переключение на противоположный компонент. То есть нет фиксированного и предсказуемого поведения. Поэтому все же придется внести некоторые изменения. Мы откажемся от функции changeAppType и удалим ее.
Конструкцию switch мы оставим прежней. В качестве пропса в компонент Menu будем передавать функцию setMode. То есть из меню мы будем напрямую влиять на состояние компонента App, а не проверять, каково сейчас состояние.
Заменим элементы списка на кнопки Button и к каждой кнопке привяжем функцию замены состояния компонента App.
-
Первая будет устанавливать значение на Calculator.
<Button onClick={() => {props.onClick('Calculator')}}> Calculator </Button>
-
А вторая будет менять состояние на Converter.
<Button onClick={() => {props.onClick('Converter')}}> Converter </Button>
Готово. Теперь меню более логично и точно понятно, куда ведет каждый из пунктов.
Реализуем функциональность конвертера
Начнем с примитивного варианта конвертера, который умеет преобразовывать значения из одной метрической единицы в другую. Возьмем самый элементарный пример – метры и сантиметры. Начнем реализацию интерфейса с них, чтобы не тратить много времени, так как базовой логики достаточно, чтобы в дальнейшем вы смогли самостоятельно проработать логику для любых других вариантов единиц, кроме динамических. Их мы отдельно разберем в одном из следующих материалов.
Для начала создадим базовый элемент Select, в котором можно будет выбрать один из видов метрических единиц. Но перед этим нужно импортировать соответствующий тип блоков из библиотеки Chakra UI. import { Select } from '@chakra-ui/react'
Теперь можно «сверстать» базовый вариант элемента Select. Он представляет собой выпадающий список с двумя значениями: Centimeters и Meters. Оба нужно подписать, и получится следующий код:
-
Для сантиметров:
<option value="Centimeters">Centimeters</option>
-
Для метров:
<option value="Meters">Meters</option>
Это код необходимо продублировать, так как выбирать единицы измерения мы будем сразу в двух частях интерфейса. Будем выбирать, что конвертировать и во что. Для этого создадим функцию-компонент Converter и оттуда вернем Flex-блок с двумя Select.
В компанию к блокам Select добавим Input. В него мы будем вводить значение, которое будет конвертироваться в обратную сторону. Укажем для него числовое значение, чтобы нельзя было вводить буквы.
<Input type="number">
Ниже добавим кнопку, запускающую процесс конвертации. Она будет актировать специальную функцию, отвечающую и за конвертацию, и за отображение полученного результата в интерфейсе. В нашем случае достаточно простой кнопки с подписью Convert.
<Button onClick={() => {convert()}}>Convert</Button>
Теперь немного подготовительных мер перед тем, как реализовать непосредственно логику конвертации. Создадим два состояния: одно будет отвечать за значение поля Input, а второе будет хранить в себе результат конвертации.
const [input, setInput] = useState(0) const [result, setResult] = useState(0)
Также нам нужно создать два референта для прямого обращения к элементам Select в интерфейсе конвертера.
Пока назовем их просто первым и вторым:
const first = useRef() const second = useRed()
useRed – это функция-хук, позволяющая взаимодействовать с HTML-элементами в рамках одного Реакт-компонента. Не нужно искать его вручную, используя метод querySelector. У вас появляется что-то в духе конкретного имени для HTML-элемента.
Далее нам нужно создать функцию изменения значения input, а вернее привязать изменение состояния input при вводе символов в строку Input. Для этого добавим соответствующий атрибут к подходящему HTML-элементу.
<Input onChange={(e) => {setInput(e.target.value)}} />
Тут же «прицепим» первый референс. Для этого надо найти блок с необходимыми данными и указать в качестве атрибута ref название референса, созданного внутри компонента. Например, в нашем случае один Select получит референс first.
<Select ref={first}
Это дает нам возможность получать информацию о выбранной единице измерения, указывая переменную first или second в теле методов, используемых в рамках функции Converter.
Мы создаем метод convert. Этот метод сначала проверяет, какая единица измерения установлена в первом Select. Если это метры, то метод переключается на формулы, разработанные для этой единицы измерения. В нашем случае задействуется конструкция switch, подбирающая формулу в соответствии с тем, какая единица измерения указана во втором референсе. Вот весь код по пунктам.
-
Объявляем функцию convert:
function convert() { }
-
Делаем проверку первого референса на соответствие значению Meters. Для этого обратимся к элементу current и его значению value.
if (first.current.value == 'Meters') { }
-
В теле проверки при ее успешном выполнении в ход вступает конструкция switch, она берет за основу значение второго референса.
switch(second.current.value) { }
-
Внутри конструкции switch происходит перебор всех возможных значений и формулы для них. У нас пока только два случая: Centimeters и Meters. Для сантиметров указывается формула умножения значения метров на 100.
case 'Centimeters': setResult(input * 100); break
-
А в случае с метрами и вовсе не нужно ничего менять.
case 'Meters': setResult(input * 100); break
Этот же код мы повторим и для сантиметров. То есть создаем проверку на соответствие первого референса значению Centimeters, потом снова включаем конструкцию switch, но уже с формулами для сантиметров. Например, для конвертации сантиметров в метры надо сделать case, делящий input на сотню.
case 'Meters': setResult(input / 100); break
Вместо заключения
Базовый конвертер готов. Теперь достаточно лишь добавлять новые формулы и пополнять элементы интерфейса новыми значениями. Рекомендую выносить формулы в другие функции или компоненты, чтобы хранить их в отдельном файле. Здесь они могут отнимать слишком много пространства, мешая восприятию кода.
Если возникнут какие-то вопросы, оставляйте комментарии. Далее создадим более продвинутый конвертер, черпающий данные из сети.
Продолжение: Калькулятор-конвертер на базе React. Часть 7: Drag & Drop
Комментарии