ducky/web

Paddy 2015-05-31 Parent:bd64a7d043d0 Child:e6da0d35a533

6:a641906b8267 Go to Latest

ducky/web/src/pages/register.jsx

Enable catch-all in our ValidationError component. We're doing this an ugly, hacky way. But it works, and right now, that's what counts. To match our params/fields/headers properties on the ValidationError component, we're going to add the notParams/notFields/notHeaders properties--they match any error _not_ targeting those params/fields/headers. Basically, "any error that wouldn't be caught by these filters". Which is an ugly, but workable, solution for a catch-all ValidationError--just tell it to catch anything but the params/fields/headers that are being handled by the other ValidationErrors. Our implementation of this in the RegisterPage component validates (ha!) that it's at least workable model, if not overly pretty. Also, I anticipate some human error bugs in the future, where one of the field-specific ValidationErrors gets updated and the catch-all ValidationError does not. But whatever. For now, this is Good Enough™.

History
1 import app from 'ampersand-app'
2 import React from 'react/addons'
3 import localLinks from 'local-links'
4 import LaddaButton from 'react-ladda'
5 import LaddaCSS from '../../node_modules/ladda/dist/ladda.min.css'
6 import HeroUnit from '../components/hero'
7 import ValidationError from '../components/validation-error'
8 import onboardingStyles from '../styles/onboarding.scss'
9 import flashStyles from '../styles/_flashes.scss'
10 import debounce from 'lodash.debounce'
12 export default React.createClass({
13 displayName: 'RegisterPage',
14 mixins: [React.addons.LinkedStateMixin],
16 getInitialState () {
17 return {
18 email: null,
19 emailConfirmation: null,
20 passphrase: null,
21 passphraseConfirmation: null,
22 active: false,
23 clientErrors: [],
24 serverErrors: [],
25 }
26 },
28 emailValidationOutputs: {
29 'missing': 'We need to know how to contact you. We promise not to share it.',
30 'overflow': 'Hm, that’s a bit long. Do you have a shorter email address?',
31 'invalid_format': 'That doesn’t look like an email address… Double check it?',
32 },
34 emailConfirmationValidationOutputs: {
35 'invalid_value': 'Oops! Those emails don’t match. Maybe double-check them?',
36 },
38 passphraseValidationOutputs: {
39 'insufficient': 'A longer passphrase would be better.',
40 'missing': 'Looks like you forgot to enter a passphrase. You need one!',
41 'overflow': 'We can’t store that long a passphrase. Try a shorter one.',
42 },
44 passphraseConfirmationValidationOutputs: {
45 'invalid_value': 'Oops! Those passphrases don’t match. Maybe double-check them?',
46 },
48 catchAllValidationOutputs: {
49 },
51 debouncedValidateForm (event) {
52 this._validateForm(event)
53 },
55 _validateForm (event) {
56 const fields = {
57 email: this.state.email,
58 emailConfirmation: this.state.emailConfirmation,
59 passphrase: this.state.passphrase,
60 passphraseConfirmation: this.state.passphraseConfirmation,
61 }
62 if (event != null) {
63 if (event.target.id == 'emailInput') {
64 fields.email = event.target.value
65 }
66 else if (event.target.id == 'emailConfirmationInput') {
67 fields.emailConfirmation = event.target.value
68 }
69 else if (event.target.id == 'passphraseInput') {
70 fields.passphrase = event.target.value
71 }
72 else if (event.target.id == 'passphraseConfirmationInput') {
73 fields.passphraseConfirmation = event.target.value
74 }
75 }
76 const errors = this.validate(fields, event == null)
77 this.setState({clientErrors: errors})
78 return errors.length <= 0
79 },
81 validateForm (event) {
82 event.persist()
83 this.debouncedValidateForm(event)
84 },
86 validate (fields, all) {
87 const errors = []
88 if (fields.email != null) {
89 if (fields.email.length == 0) {
90 errors.push({'error': 'missing', 'field': '/email'})
91 } else if (fields.email.length > 64) {
92 errors.push({'error': 'overflow', 'field': '/email'})
93 } else if (!fields.email.match(/.+@.+\..+/)) {
94 errors.push({'error': 'invalid_format', 'field': '/email'})
95 }
96 } else if (all) {
97 errors.push({'error': 'missing', 'field': '/email'})
98 }
99 if (fields.emailConfirmation != null || all) {
100 if (fields.emailConfirmation != fields.email && (fields.email || fields.emailConfirmation)) {
101 errors.push({'error': 'invalid_value', 'field': '/email_confirmation'})
102 }
103 }
104 if (fields.passphrase != null) {
105 if (fields.passphrase.length == 0) {
106 errors.push({'error': 'missing', 'field': '/passphrase'})
107 } else if (fields.passphrase.length < 6) {
108 errors.push({'error': 'insufficient', 'field': '/passphrase'})
109 } else if (fields.passphrase.length > 64) {
110 errors.push({'error': 'overflow', 'field': '/passphrase'})
111 }
112 } else if (all) {
113 errors.push({'error': 'missing', 'field': '/passphrase'})
114 }
115 if (fields.passphraseConfirmation != null || all) {
116 if (fields.passphraseConfirmation != fields.passphrase && (fields.passphrase || fields.passphraseConfirmation)) {
117 errors.push({'error': 'invalid_value', 'field': '/passphrase_confirmation'})
118 }
119 }
120 return errors
121 },
123 componentDidMount () {
124 app.profiles.on('request', (moc, xhr, options) => {
125 this.setState({active: true})
126 })
127 app.profiles.on('error', (moc, xhr, options) => {
128 let state = {active: false}
129 if (xhr.errors && xhr.errors.length) {
130 state.serverErrors = this.state.serverErrors.concat(xhr.errors)
131 } else {
132 state.serverErrors = [{'error': 'act_of_god'}]
133 }
134 this.setState(state)
135 })
136 app.profiles.on('sync', (moc, xhr, options) => {
137 app.me.login(this.state.email, this.state.passphrase)
138 })
139 app.me.on('sync', (moc, xhr, options) => {
140 this.setState({active: false})
141 app.router.navigate('/register/payment')
142 })
143 app.me.on('error', (moc, xhr, options) => {
144 let state = {active: false}
145 if (xhr.errors && xhr.errors.length) {
146 state.serverErrors = this.state.serverErrors.concat(xhr.errors)
147 } else {
148 state.serverErrors = [{'error': 'act_of_god'}]
149 }
150 this.setState(state)
151 })
152 },
154 componentWillMount () {
155 this.debouncedValidateForm = debounce(this.debouncedValidateForm, 900)
156 },
158 register (e) {
159 e.preventDefault()
160 const success = this._validateForm(null)
161 if (!success) {
162 return
163 }
164 app.profiles.register(this.state.email, this.state.passphrase)
165 },
167 onBackClick (event) {
168 event.preventDefault()
169 window.history.back()
170 },
172 render () {
173 return (
174 <div className='container'>
175 <HeroUnit title='Create an Account'>We’d like to get to know you better.</HeroUnit>
176 <article className='onboarding register'>
177 <form onSubmit={this.register}>
178 <div>
179 <label htmlFor='emailInput'>Email</label>
180 <input id='emailInput' type='email' placeholder='Ours is quack@useducky.com' valueLink={this.linkState('email')} disabled={this.state.active} onInput={this.validateForm} />
181 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/email' outputs={this.emailValidationOutputs} />
183 <label htmlFor='emailConfirmationInput'>Verify Email</label>
184 <input id='emailConfirmationInput' type='email' placeholder='Typos are the absolute worst.' valueLink={this.linkState('emailConfirmation')} disabled={this.state.active} onInput={this.validateForm} />
185 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/email_confirmation' outputs={this.emailConfirmationValidationOutputs} />
187 <label htmlFor='passphraseInput'>Passphrase</label>
188 <input id='passphraseInput' type='password' placeholder='We use a sentence. Try it!' valueLink={this.linkState('passphrase')} disabled={this.state.active} onInput={this.validateForm} />
189 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/passphrase' outputs={this.passphraseValidationOutputs} />
191 <label htmlFor='passphraseConfirmationInput'>Verify Passphrase</label>
192 <input id='passphraseConfirmationInput' type='password' placeholder='Just to make sure you know it.' valueLink={this.linkState('passphraseConfirmation')} disabled={this.state.active} onInput={this.validateForm} />
193 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} field='/passphrase_confirmation' outputs={this.passphraseConfirmationValidationOutputs} />
195 <ValidationError errors={this.state.clientErrors.concat(this.state.serverErrors)} notFields={['/email', '/email_confirmation', '/passphrase', '/passphrase_confirmation']} notHeaders={[]} notParams={[]} outputs={this.catchAllValidationOutputs} />
196 </div>
197 <div className='actionbuttons'>
198 <button onClick={this.onBackClick} disabled={this.state.active} type='button' className='ladda-button'>Back</button>
199 <LaddaButton style='expand-right' active={this.state.active}>
200 <button type='submit' className='primary' disabled={this.state.active || this.state.clientErrors.length}>Register</button>
201 </LaddaButton>
202 </div>
203 </form>
204 </article>
205 </div>
206 )
207 }
208 })