Прототип интерактивного текстового поля с помощью Framer. Часть 1.
Я расскажу как реализовать в своем прототипе интерактивные текстовые поля.
Дизайн
Делаем дизайн всех состояний в Figma, определяем размер поля и отступы.
У текстового поля будет 4 состояния:
- Базовое
- При наведении
- Активное
- Ошибка
Создание элементов прототипа
Создаем новый проект во Framer, переключаемся во вкладку Code.
Делаем новый слой в котором будут лежать все элементы текстового поля:
inputContainer = new Layer
width: 360
height: 76
x: 170
y: 162
Чтобы избавиться от серого фона, допишем:
backgroundColor: "transparent"
Теперь добавим лейбл. Для этого вместо обычного Layer
будем использовать TextLayer
:
inputLabel = new TextLayer
¬ text: "Your name"
¬ parent: inputContainer # Размещаем inputLabel внутри inputContainer
¬ fontFamily: "Basis Grotesque Pro"
¬ fontSize: 16
¬ lineHeight: 1.25
¬ y: 18
¬ color: "#808080"
Добавляем линию подчеркивания текстового поля:
inputLine = new Layer
¬ parent: inputContainer
¬ height: 2
¬ width: inputContainer.width
¬ y: 54
¬ backgroundColor: "#e5e5e5"
И сообщение об ошибке:
inputError = new TextLayer
¬ text: "" # Пока оставляем это поле пустым
¬ parent: inputContainer
¬ fontFamily: "Basis Grotesque Pro"
¬ fontSize: 12
¬ lineHeight: 1.25
¬ y: 60
¬ color: "#bf3048"
¬ opacity: 0
В нашем инпуте не хватает редактируемой области. Во Framer нет компонента для создания редактируемого поля, но так как Framer сделан на Javascript, мы можем воспользоваться методами document.createElement
и _element.appendChild
чтобы cамостоятельно создать в DOM нашего прототипа редактируемое поле:
inputControl = document.createElement("input")
inputControl.style["width"] = "360px"
inputControl.style["height"] = "56px"
inputControl.style["font-family"] = "Basis Grotesque Pro"
inputControl.style["font-size"] = "16px"
inputControl.style["line-height"] = "2"
inputControl.style["outline"] = "none"
inputControl.style["color"] = "#000000"
inputControl.placeholder = ""
inputControl.value = ""
inputControl.focus()
inputContainer._element.appendChild(inputControl)
Итак, теперь у нас есть все необходимые элементы нашего редактируемого поля.
Интерактив
1. Анимация лейбла
При клике в редактируемое поле, лейбл должен уменьшаться и смещаться вверх. Это значит что у inputLabel
будут меняться значения y
и fontSize
.
Чтобы переключаться между двумя состояниями добавимstates
к нашему лейблу:
inputLabel.states =
¬ basic:
¬ ¬ fontSize: 16
¬ ¬ y: 18
¬ active:
¬ ¬ fontSize: 12
¬ ¬ y: 0
Теперь мы можем добавить анимацию лейбла при установке курсора в поле. Для этого достаточно добавить простую функцию:
inputContainer.onClick ->
¬ inputLabel.animate("active")
Анимация работает, но выглядит не очень приятно. Анимация по умолчанию во Framer медленная и линейная. Чтобы не задавать настройки анимации каждый раз, я устанавливаю дефолтное значение анимации для всего прототипа, в самом начале кода:
Framer.Defaults.Animation =
¬ curve: "ease-in-out"
¬ time: 0.15
2. Анимация линии
У каждого состояния инпута свой цвет линии подчеркивания. Реализуем эти состояния также при помощи states
:
inputLine.states =
¬ basic:
¬ ¬ backgroundColor: "#e5e5e5" # Серый
¬ hover:
¬ ¬ backgroundColor: "#bfbfbf" # Темно-серый
¬ active:
¬ ¬ backgroundColor: "#000000" # Черный
¬ error:
¬ ¬ backgroundColor: "#bf3048" # Красный
Добавим анимацию линии при наведении и при клике:
inputContainer.onClick ->
¬ inputLine.animate("active")
¬ inputLabel.animate("active")inputContainer.onMouseOver ->
¬ inputLine.animate("hover")
inputContainer.onMouseOut ->
¬ inputLine.animate("basic")
Пока еще поле ведет себя не так, как хотелось бы. При клике линия становится темной, но как только мы двигаем курсор мыши, она становится серой, потому что срабатывает событие onMouseOut
.
Чтобы это исправить, добавим проверку — является ли поле в фокусе. Для этого в самом начале зададим переменную inputFocus = false
. И перепишем наши события:
inputContainer.onClick ->
¬ inputFocus = true
¬ inputLine.animate("active")
¬ inputLabel.animate("active")inputContainer.onMouseOver ->
¬ if !inputFocus
¬ ¬ inputLine.animate("hover")
inputContainer.onMouseOut ->
¬ if !inputFocus
¬ ¬ inputLine.animate("basic")
Теперь клик и ввод текста работает хорошо. Но мы не сделали еще две вещи:
- Что происходит, когда пользователь закончил вводить текст? Сейчас поле все еще остается активным даже если кликнуть за его пределами;
- Если пользователь ничего не ввел, мы должны показать возвращение лейбла обратно в середину поля.
3. Возврат в базовое состояние
Чтобы отработать клик за пределами инпута создадим фоновый слой:
backLayer = new Layer
¬ width: Canvas.width
¬ height: Canvas.height
¬ backgroundColor: "transparent"
И добавим несколько событий, происходящих при клике на него:
backLayer.onClick ->
¬ inputFocus = false
¬ inputLine.animate("basic") # Возвращаем линию в базовое состояние
¬ if inputControl.value == "" # Проверяем, пустое ли поле ввода
¬ ¬ inputLabel.animate("basic")
4. Подсветка ошибки
Осталось добавить проверку на недопустимое заполнение поля. Для этого воспользуемся addEventListener "keyup"
:
inputControl.addEventListener "keyup", (ev) ->
¬ if inputControl.value == "Donald Trump" # Недопустимое значение
¬ ¬ inputError.text = "You can't use this name" # Текст ошибки
¬ ¬ inputError.animate(opacity: 1)
¬ ¬ inputLine.animate("error")
¬ else # Если ошибка исправлена, возвращаем все обратно
¬ ¬ inputError.animate(opacity: 0)
¬ ¬ inputLine.animate("active")
¬ ¬ Utils.delay 0.15, -> # Задержка, чтобы надпись успела плавно исчезнуть
¬ ¬ ¬ inputError.text = ""
Посмотрим, что получилось:
Заключение
Готовый код можно скачать на github: https://github.com/monographic/framer-tutorial-input
У меня получилось больше 100 строчек кода. Довольно много для одного инпута. К счастью, нам не нужно писать столько кода для каждого текстового поля. В следующей части я расскажу, как переиспользовать получившийся код.