fix validation visual feedback and disabled grey tick

Former-commit-id: e947bb0ea1
This commit is contained in:
Adrian Tiberius 2016-06-29 23:06:50 +03:00
parent b505a3dc05
commit 15ddd891a1
6 changed files with 270 additions and 36 deletions

View File

@ -14,6 +14,9 @@
(defn anim-sequence [animations]
(.sequence animated (clj->js animations)))
(defn parallel [animations]
(.parallel animated (clj->js animations)))
(defn anim-delay [duration]
(.delay animated duration))

View File

@ -0,0 +1,40 @@
(ns status-im.components.text-field.styles)
(def text-field-container
{:position :relative
:height 72
:paddingTop 30
:paddingBottom 7})
(def text-input
{:fontSize 16
:height 34
:lineHeight 34
:paddingBottom 5
:textAlignVertical :top})
(defn label [top font-size color]
{:position :absolute
:top top
:left 0
:color color
:fontSize font-size
:backgroundColor :transparent})
(def label-float
{})
(defn underline-container [backgroundColor]
{:backgroundColor backgroundColor
:height 1
:alignItems :center})
(defn underline [backgroundColor width]
{:backgroundColor backgroundColor
:height 1
:width width})
(defn error-text [color]
{:color color
:fontSize 12})

View File

@ -0,0 +1,189 @@
(ns status-im.components.text-field.view
(:require [clojure.string :as s]
[re-frame.core :refer [subscribe dispatch dispatch-sync]]
[reagent.core :as r]
[status-im.components.react :refer [react
view
text
animated-text
animated-view
text-input
touchable-opacity]]
[status-im.components.text-field.styles :as st]
[status-im.i18n :refer [label]]
[status-im.components.animation :as anim]
[status-im.utils.logging :as log]))
(def config {:label-top 16
:label-bottom 37
:label-font-large 16
:label-font-small 12
:label-animation-duration 200})
(def default-props {:wrapperStyle {}
:inputStyle {}
:lineStyle {}
:labelColor "#838c93"
:lineColor "#0000001f"
:focusLineColor "#0000001f"
:errorColor "#d50000"
:onFocus #()
:onBlur #()
:onChangeText #()
:onChange #()})
(defn field-animation [{:keys [top to-top font-size to-font-size
line-width to-line-width]}]
(let [duration (:label-animation-duration config)
animation (anim/parallel [(anim/timing top {:toValue to-top
:duration duration})
(anim/timing font-size {:toValue to-font-size
:duration duration})
(anim/timing line-width {:toValue to-line-width
:duration duration})])]
(anim/start animation (fn [arg]
(when (.-finished arg)
(log/debug "Field animation finished"))))))
; Invoked once before the component is mounted. The return value will be used
; as the initial value of this.state.
(defn get-initial-state [component]
{:has-focus false
:float-label? false
:label-top 0
:label-font-size 0
:line-width (anim/create-value 0)
:max-line-width 100})
; Invoked once, both on the client and server, immediately before the initial
; rendering occurs. If you call setState within this method, render() will see
; the updated state and will be executed only once despite the state change.
(defn component-will-mount [component]
(let [{:keys [value] :as props} (r/props component)
data {:label-top (anim/create-value (if (s/blank? value)
(:label-bottom config)
(:label-top config)))
:label-font-size (anim/create-value (if (s/blank? value)
(:label-font-large config)
(:label-font-small config)))
:float-label? (if (s/blank? value) false true)}]
(log/debug "component-will-mount")
(r/set-state component data)))
; Invoked once, only on the client (not on the server), immediately after the
; initial rendering occurs. At this point in the lifecycle, you can access any
; refs to your children (e.g., to access the underlying DOM representation).
; The componentDidMount() method of child components is invoked before that of
; parent components.
(defn component-did-mount [component]
(let [props (r/props component)]
(log/debug "component-did-mount:")))
; Invoked when a component is receiving new props. This method is not called for
; the initial render. Use this as an opportunity to react to a prop transition
; before render() is called by updating the state using this.setState().
; The old props can be accessed via this.props. Calling this.setState() within
; this function will not trigger an additional render.
(defn component-will-receive-props [component new-props]
(log/debug "component-will-receive-props: new-props=" new-props))
; Invoked before rendering when new props or state are being received. This method
; is not called for the initial render or when forceUpdate is used. Use this as
; an opportunity to return false when you're certain that the transition to the
; new props and state will not require a component update.
; If shouldComponentUpdate returns false, then render() will be completely skipped
; until the next state change. In addition, componentWillUpdate and
; componentDidUpdate will not be called.
(defn should-component-update [component next-props next-state]
(log/debug "should-component-update: " next-props next-state)
true)
; Invoked immediately before rendering when new props or state are being received.
; This method is not called for the initial render. Use this as an opportunity
; to perform preparation before an update occurs.
(defn component-will-update [component next-props next-state]
(log/debug "component-will-update: " next-props next-state))
; Invoked immediately after the component's updates are flushed to the DOM.
; This method is not called for the initial render. Use this as an opportunity
; to operate on the DOM when the component has been updated.
(defn component-did-update [component prev-props prev-state]
(log/debug "component-did-update: " prev-props prev-state))
(defn on-focus [{:keys [component animation onFocus]}]
(do
(log/debug "input focused")
(r/set-state component {:has-focus true
:float-label? true})
(field-animation animation)
(when onFocus (onFocus))))
(defn on-blur [{:keys [component value animation onBlur]}]
(do
(log/debug "Input blurred")
(r/set-state component {:has-focus false
:float-label? (if (s/blank? value) false true)})
(when (s/blank? value)
(field-animation animation))
(when onBlur (onBlur))))
(defn get-width [event]
(.-width (.-layout (.-nativeEvent event))))
(defn reagent-render [data children]
(let [component (r/current-component)
{:keys [has-focus
float-label?
label-top
label-font-size
line-width
max-line-width] :as state} (r/state component)
{:keys [wrapperStyle inputStyle lineColor focusLineColor
labelColor errorColor error label value onFocus onBlur
onChangeText onChange] :as props} (merge default-props (r/props component))
lineColor (if error errorColor lineColor)
focusLineColor (if error errorColor focusLineColor)
labelColor (if (and error (not float-label?)) errorColor labelColor)
label (if error (str label " *") label)]
(log/debug "reagent-render: " data state)
[view (merge st/text-field-container wrapperStyle)
[animated-text {:style (st/label label-top label-font-size labelColor)} label]
[text-input {:style (merge st/text-input inputStyle)
:placeholder ""
:onFocus #(on-focus {:component component
:animation {:top label-top
:to-top (:label-top config)
:font-size label-font-size
:to-font-size (:label-font-small config)
:line-width line-width
:to-line-width max-line-width}
:onFocus onFocus})
:onBlur #(on-blur {:component component
:value value
:animation {:top label-top
:to-top (:label-bottom config)
:font-size label-font-size
:to-font-size (:label-font-large config)
:line-width line-width
:to-line-width 0}
:onBlur onBlur})
:onChangeText #(onChangeText %)
:onChange #(onChange %)} value]
[view {:style (st/underline-container lineColor)
:onLayout #(r/set-state component {:max-line-width (get-width %)})}
[animated-view {:style (st/underline focusLineColor line-width)}]]
[text {:style (st/error-text errorColor)} error]]))
(defn text-field [data children]
(let [component-data {:get-initial-state get-initial-state
:component-will-mount component-will-mount
:component-did-mount component-did-mount
:component-will-receive-props component-will-receive-props
:should-component-update should-component-update
:component-will-update component-will-update
:component-did-update component-did-update
:display-name "text-field"
:reagent-render reagent-render}]
(log/debug "Creating text-field component: " data)
(r/create-class component-data)))

View File

@ -173,6 +173,7 @@
(def address-explication-container
{:flex 1
:margin-top 30
:paddingLeft 16
:paddingRight 16})

View File

@ -8,6 +8,7 @@
image
linear-gradient
touchable-highlight]]
[status-im.components.text-field.view :refer [text-field]]
[status-im.utils.identicon :refer [identicon]]
[status-im.components.toolbar :refer [toolbar]]
[status-im.components.styles :refer [color-purple
@ -28,7 +29,6 @@
[status-im.contacts.styles :as st]))
(def toolbar-title
[view toolbar-title-container
[text {:style toolbar-title-text}
@ -36,42 +36,43 @@
(defview contact-name-input [name]
[]
[text-input
{:underlineColorAndroid "#0000001f"
:placeholderTextColor "#838c93de"
:style form-text-input
:autoFocus true
:placeholder (label :t/name)
:onChangeText #(dispatch [:set-in [:new-contact :name] %])}
name])
[text-field
{:error (if (str/blank? name) "" nil)
:value name
:label (label :t/name)
:onChangeText #(dispatch [:set-in [:new-contact :name] %])}])
(defview contact-whisper-id-input [whisper-identity]
[view button-input-container
[text-input
{:underlineColorAndroid "#0000001f"
:placeholderTextColor "#838c93de"
:style (merge form-text-input button-input)
:autoFocus true
:placeholder (label :t/address)
:onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])}
whisper-identity]
[scan-button #(dispatch [:scan-qr-code {:toolbar-title (label :t/new-contact)} :set-new-contact-from-qr])]])
[]
(let [error (if (str/blank? whisper-identity) "" nil)
error (if (s/valid? ::v/whisper-identity whisper-identity)
error
"Please enter a valid address or scan a QR code")]
[view button-input-container
[text-field
{:error error
:value whisper-identity
:wrapperStyle (merge button-input)
:label (label :t/address)
:onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])}]
[scan-button #(dispatch [:scan-qr-code {:toolbar-title (label :t/new-contact)} :set-new-contact-from-qr])]]))
(defview new-contact []
[{:keys [name whisper-identity phone-number] :as new-contact} [:get :new-contact]]
[view st/contact-form-container
[toolbar {:background-color :white
:nav-action {:image {:source {:uri :icon_back}
:style icon-back}
:handler #(dispatch [:navigate-back])}
:custom-content toolbar-title
:action {:image {:source {:uri (if (s/valid? ::v/contact new-contact)
:icon_ok_blue
:icon_ok_disabled)}
:style icon-search}
:handler #(dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)])}}]
[view st/form-container
[contact-whisper-id-input whisper-identity]
[contact-name-input name]]
[view st/address-explication-container
[text {:style st/address-explication} (label :t/address-explication)]]])
(let [valid-contact? (s/valid? ::v/contact new-contact)]
[view st/contact-form-container
[toolbar {:background-color :white
:nav-action {:image {:source {:uri :icon_back}
:style icon-back}
:handler #(dispatch [:navigate-back])}
:custom-content toolbar-title
:action {:image {:source {:uri (if valid-contact?
:icon_ok_blue
:icon_ok_disabled)}
:style icon-search}
:handler #(when valid-contact? dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)])}}]
[view st/form-container
[contact-name-input name]
[contact-whisper-id-input whisper-identity]]
[view st/address-explication-container
[text {:style st/address-explication} (label :t/address-explication)]]]))

View File

@ -78,7 +78,7 @@
(def scan-button
{:position :absolute
:top 5
:bottom 0
:right 16
:flex 1
:height 50