273 | *
274 | * Display:
275 | * Column Header: Content Here
276 | */
277 | }
278 | table caption {
279 | display: block;
280 | }
281 | table thead {
282 | display: none;
283 | visibility: hidden;
284 | }
285 | table tbody, table tr, table th, table td {
286 | border: 0;
287 | display: block;
288 | padding: 0;
289 | text-align: left;
290 | white-space: normal;
291 | }
292 | table tr {
293 | margin-bottom: 1.5rem;
294 | }
295 | table th[data-title]:before, table td[data-title]:before {
296 | content: attr(data-title) ": ";
297 | font-weight: bold;
298 | }
299 | table th:not([data-title]) {
300 | display: none;
301 | font-weight: bold;
302 | }
303 | table td:empty {
304 | display: none;
305 | }
306 | }
307 |
308 | /* footer */
309 | .app-footer {
310 | color: silver;
311 | position: relative;
312 | line-height: 0.1rem;
313 | margin-top: 6rem;
314 | }
315 |
--------------------------------------------------------------------------------
/src/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | font-family: sans-serif; /* 1 */
11 | -ms-text-size-adjust: 100%; /* 2 */
12 | -webkit-text-size-adjust: 100%; /* 2 */
13 | }
14 |
15 | /**
16 | * Remove default margin.
17 | */
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | /* HTML5 display definitions
24 | ========================================================================== */
25 |
26 | /**
27 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
29 | * and Firefox.
30 | * Correct `block` display not defined for `main` in IE 11.
31 | */
32 |
33 | article,
34 | aside,
35 | details,
36 | figcaption,
37 | figure,
38 | footer,
39 | header,
40 | hgroup,
41 | main,
42 | menu,
43 | nav,
44 | section,
45 | summary {
46 | display: block;
47 | }
48 |
49 | /**
50 | * 1. Correct `inline-block` display not defined in IE 8/9.
51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
52 | */
53 |
54 | audio,
55 | canvas,
56 | progress,
57 | video {
58 | display: inline-block; /* 1 */
59 | vertical-align: baseline; /* 2 */
60 | }
61 |
62 | /**
63 | * Prevent modern browsers from displaying `audio` without controls.
64 | * Remove excess height in iOS 5 devices.
65 | */
66 |
67 | audio:not([controls]) {
68 | display: none;
69 | height: 0;
70 | }
71 |
72 | /**
73 | * Address `[hidden]` styling not present in IE 8/9/10.
74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
75 | */
76 |
77 | [hidden],
78 | template {
79 | display: none;
80 | }
81 |
82 | /* Links
83 | ========================================================================== */
84 |
85 | /**
86 | * Remove the gray background color from active links in IE 10.
87 | */
88 |
89 | a {
90 | background-color: transparent;
91 | }
92 |
93 | /**
94 | * Improve readability when focused and also mouse hovered in all browsers.
95 | */
96 |
97 | a:active,
98 | a:hover {
99 | outline: 0;
100 | }
101 |
102 | /* Text-level semantics
103 | ========================================================================== */
104 |
105 | /**
106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
107 | */
108 |
109 | abbr[title] {
110 | border-bottom: 1px dotted;
111 | }
112 |
113 | /**
114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
115 | */
116 |
117 | b,
118 | strong {
119 | font-weight: bold;
120 | }
121 |
122 | /**
123 | * Address styling not present in Safari and Chrome.
124 | */
125 |
126 | dfn {
127 | font-style: italic;
128 | }
129 |
130 | /**
131 | * Address variable `h1` font-size and margin within `section` and `article`
132 | * contexts in Firefox 4+, Safari, and Chrome.
133 | */
134 |
135 | h1 {
136 | font-size: 2em;
137 | margin: 0.67em 0;
138 | }
139 |
140 | /**
141 | * Address styling not present in IE 8/9.
142 | */
143 |
144 | mark {
145 | background: #ff0;
146 | color: #000;
147 | }
148 |
149 | /**
150 | * Address inconsistent and variable font size in all browsers.
151 | */
152 |
153 | small {
154 | font-size: 80%;
155 | }
156 |
157 | /**
158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
159 | */
160 |
161 | sub,
162 | sup {
163 | font-size: 75%;
164 | line-height: 0;
165 | position: relative;
166 | vertical-align: baseline;
167 | }
168 |
169 | sup {
170 | top: -0.5em;
171 | }
172 |
173 | sub {
174 | bottom: -0.25em;
175 | }
176 |
177 | /* Embedded content
178 | ========================================================================== */
179 |
180 | /**
181 | * Remove border when inside `a` element in IE 8/9/10.
182 | */
183 |
184 | img {
185 | border: 0;
186 | }
187 |
188 | /**
189 | * Correct overflow not hidden in IE 9/10/11.
190 | */
191 |
192 | svg:not(:root) {
193 | overflow: hidden;
194 | }
195 |
196 | /* Grouping content
197 | ========================================================================== */
198 |
199 | /**
200 | * Address margin not present in IE 8/9 and Safari.
201 | */
202 |
203 | figure {
204 | margin: 1em 40px;
205 | }
206 |
207 | /**
208 | * Address differences between Firefox and other browsers.
209 | */
210 |
211 | hr {
212 | -moz-box-sizing: content-box;
213 | box-sizing: content-box;
214 | height: 0;
215 | }
216 |
217 | /**
218 | * Contain overflow in all browsers.
219 | */
220 |
221 | pre {
222 | overflow: auto;
223 | }
224 |
225 | /**
226 | * Address odd `em`-unit font size rendering in all browsers.
227 | */
228 |
229 | code,
230 | kbd,
231 | pre,
232 | samp {
233 | font-family: monospace, monospace;
234 | font-size: 1em;
235 | }
236 |
237 | /* Forms
238 | ========================================================================== */
239 |
240 | /**
241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
242 | * styling of `select`, unless a `border` property is set.
243 | */
244 |
245 | /**
246 | * 1. Correct color not being inherited.
247 | * Known issue: affects color of disabled elements.
248 | * 2. Correct font properties not being inherited.
249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
250 | */
251 |
252 | button,
253 | input,
254 | optgroup,
255 | select,
256 | textarea {
257 | color: inherit; /* 1 */
258 | font: inherit; /* 2 */
259 | margin: 0; /* 3 */
260 | }
261 |
262 | /**
263 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
264 | */
265 |
266 | button {
267 | overflow: visible;
268 | }
269 |
270 | /**
271 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
272 | * All other form control elements do not inherit `text-transform` values.
273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
274 | * Correct `select` style inheritance in Firefox.
275 | */
276 |
277 | button,
278 | select {
279 | text-transform: none;
280 | }
281 |
282 | /**
283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
284 | * and `video` controls.
285 | * 2. Correct inability to style clickable `input` types in iOS.
286 | * 3. Improve usability and consistency of cursor style between image-type
287 | * `input` and others.
288 | */
289 |
290 | button,
291 | html input[type="button"], /* 1 */
292 | input[type="reset"],
293 | input[type="submit"] {
294 | -webkit-appearance: button; /* 2 */
295 | cursor: pointer; /* 3 */
296 | }
297 |
298 | /**
299 | * Re-set default cursor for disabled elements.
300 | */
301 |
302 | button[disabled],
303 | html input[disabled] {
304 | cursor: default;
305 | }
306 |
307 | /**
308 | * Remove inner padding and border in Firefox 4+.
309 | */
310 |
311 | button::-moz-focus-inner,
312 | input::-moz-focus-inner {
313 | border: 0;
314 | padding: 0;
315 | }
316 |
317 | /**
318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
319 | * the UA stylesheet.
320 | */
321 |
322 | input {
323 | line-height: normal;
324 | }
325 |
326 | /**
327 | * It's recommended that you don't attempt to style these elements.
328 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
329 | *
330 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
331 | * 2. Remove excess padding in IE 8/9/10.
332 | */
333 |
334 | input[type="checkbox"],
335 | input[type="radio"] {
336 | box-sizing: border-box; /* 1 */
337 | padding: 0; /* 2 */
338 | }
339 |
340 | /**
341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
342 | * `font-size` values of the `input`, it causes the cursor style of the
343 | * decrement button to change from `default` to `text`.
344 | */
345 |
346 | input[type="number"]::-webkit-inner-spin-button,
347 | input[type="number"]::-webkit-outer-spin-button {
348 | height: auto;
349 | }
350 |
351 | /**
352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
354 | * (include `-moz` to future-proof).
355 | */
356 |
357 | input[type="search"] {
358 | -webkit-appearance: textfield; /* 1 */
359 | -moz-box-sizing: content-box;
360 | -webkit-box-sizing: content-box; /* 2 */
361 | box-sizing: content-box;
362 | }
363 |
364 | /**
365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
366 | * Safari (but not Chrome) clips the cancel button when the search input has
367 | * padding (and `textfield` appearance).
368 | */
369 |
370 | input[type="search"]::-webkit-search-cancel-button,
371 | input[type="search"]::-webkit-search-decoration {
372 | -webkit-appearance: none;
373 | }
374 |
375 | /**
376 | * Define consistent border, margin, and padding.
377 | */
378 |
379 | fieldset {
380 | border: 1px solid #c0c0c0;
381 | margin: 0 2px;
382 | padding: 0.35em 0.625em 0.75em;
383 | }
384 |
385 | /**
386 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
388 | */
389 |
390 | legend {
391 | border: 0; /* 1 */
392 | padding: 0; /* 2 */
393 | }
394 |
395 | /**
396 | * Remove default vertical scrollbar in IE 8/9/10/11.
397 | */
398 |
399 | textarea {
400 | overflow: auto;
401 | }
402 |
403 | /**
404 | * Don't inherit the `font-weight` (applied by a rule above).
405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
406 | */
407 |
408 | optgroup {
409 | font-weight: bold;
410 | }
411 |
412 | /* Tables
413 | ========================================================================== */
414 |
415 | /**
416 | * Remove most spacing between table cells.
417 | */
418 |
419 | table {
420 | border-collapse: collapse;
421 | border-spacing: 0;
422 | }
423 |
424 | td,
425 | th {
426 | padding: 0;
427 | }
--------------------------------------------------------------------------------
/src/examples/Step2.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, { Component, PropTypes } from 'react';
4 |
5 | export default class Step2 extends Component {
6 | // constructor(props) {
7 | // super(props);
8 |
9 | // this.state = {
10 | // emailEmergency: props.getStore().emailEmergency
11 | // };
12 |
13 | // this.validatorTypes = {
14 | // emailEmergency: Joi.string().email().required()
15 | // };
16 |
17 | // this.getValidatorData = this.getValidatorData.bind(this);
18 | // this.renderHelpText = this.renderHelpText.bind(this);
19 | // this.isValidated = this.isValidated.bind(this);
20 | // }
21 |
22 | // isValidated() {
23 | // return new Promise((resolve, reject) => {
24 | // this.props.validate((error) => {
25 | // if (error) {
26 | // reject(); // form contains errors
27 | // return;
28 | // }
29 |
30 | // if (this.props.getStore().emailEmergency != this.getValidatorData().emailEmergency) { // only update store of something changed
31 | // this.props.updateStore({
32 | // ...this.getValidatorData(),
33 | // savedToCloud: false // use this to notify step4 that some changes took place and prompt the user to save again
34 | // }); // Update store here (this is just an example, in reality you will do it via redux or flux)
35 | // }
36 |
37 | // resolve(); // form is valid, fire action
38 | // });
39 | // });
40 | // }
41 |
42 | // getValidatorData() {
43 | // return {
44 | // emailEmergency: this.refs.emailEmergency.value,
45 | // }
46 | // };
47 |
48 | // onChange(e) {
49 | // let newState = {};
50 | // newState[e.target.name] = e.target.value;
51 | // this.setState(newState);
52 | // }
53 |
54 | // renderHelpText(message, id) {
55 | // return (
{message}
);
56 | // };
57 |
58 | // render() {
59 | // // explicit class assigning based on validation
60 | // let notValidClasses = {};
61 | // notValidClasses.emailEmergencyCls = this.props.isValid('emailEmergency') ?
62 | // 'no-error col-md-8' : 'has-error col-md-8';
63 | constructor(props) {
64 | super(props);
65 |
66 | this.state = {
67 | day: props.getStore().day,
68 | month: props.getStore().month,
69 | year: props.getStore().year,
70 | gender: props.getStore().gender
71 | };
72 |
73 | this.genderValue = '';
74 |
75 | this._validateOnDemand = true; // this flag enables onBlur validation as user fills forms
76 |
77 | this.validationCheck = this.validationCheck.bind(this);
78 | this.isValidated = this.isValidated.bind(this);
79 | }
80 |
81 | componentDidMount() {}
82 |
83 | componentWillUnmount() {}
84 |
85 | isValidated() {
86 | const userInput = this._grabUserInput(); // grab user entered vals
87 | const validateNewInput = this._validateData(userInput); // run the new input against the validator
88 |
89 | let isDataValid = false;
90 | // if full validation passes then save to store and pass as valid
91 | if (Object.keys(validateNewInput).every((k) => { return validateNewInput[k] === true })) {
92 | if (this.props.getStore().day != userInput.day || this.props.getStore().month != userInput.month || this.props.getStore().year != userInput.year || this.props.getStore().gender != userInput.gender) { // only update store of something changed
93 |
94 | this.props.updateStore({
95 | ...userInput,
96 | savedToCloud: false // use this to notify step4 that some changes took place and prompt the user to save again
97 | }); // Update store here (this is just an example, in reality you will do it via redux or flux)
98 | }
99 | isDataValid = true;
100 | }
101 | else {
102 | // if anything fails then update the UI validation state but NOT the UI Data State
103 | this.setState(Object.assign(userInput, validateNewInput, this._validationErrors(validateNewInput)));
104 | }
105 |
106 | return isDataValid;
107 | }
108 |
109 | validationCheck() {
110 | this.isValidated();
111 | if (!this._validateOnDemand)
112 | return;
113 |
114 | const userInput = this._grabUserInput(); // grab user entered vals
115 | const validateNewInput = this._validateData(userInput); // run the new input against the validator
116 |
117 | this.setState(Object.assign(userInput, validateNewInput, this._validationErrors(validateNewInput)));
118 | }
119 |
120 | _validateData(data) {
121 | return {
122 | dayVal: (data.day > 0 && (data.month == 2 ? data.day < 29 : data.day < 31)),
123 | monthVal: (data.month > 0 && data.month < 13),
124 | yearVal: (data.year > 1900 && data.year < 1999),
125 | genderVal: (data.gender != '')
126 | }
127 | }
128 |
129 | _validationErrors(val) {
130 | const errMsgs = {
131 | dayValMsg: val.dayVal ? '' : 'A valid day',
132 | monthValMsg: val.monthVal ? '' : 'A valid month',
133 | yearValMsg: val.yearVal ? '' : 'A valid year',
134 | dateValMsg: val.dayVal && val.monthVal && val.yearVal ? '' : 'A valid date is required',
135 | genderValMsg: val.genderVal ? '' : 'A valid gender is required'
136 | }
137 | return errMsgs;
138 | }
139 |
140 | _grabUserInput() {
141 | return {
142 | day: this.refs.day.value,
143 | month: this.refs.month.value,
144 | year: this.refs.year.value,
145 | gender: this.genderValue
146 | };
147 | }
148 |
149 | maleBtn() {
150 | this.genderValue = 'male';
151 | this.props.getStore().gender = 'male';
152 | this.validationCheck();
153 | }
154 |
155 | femaleBtn() {
156 |
157 | this.genderValue = 'female';
158 | this.props.getStore().gender = 'female';
159 | this.validationCheck();
160 | }
161 |
162 | unspecifedBtn() {
163 | this.genderValue = 'unspecifed';
164 | this.props.getStore().gender = 'unspecifed'
165 | this.validationCheck();
166 | }
167 |
168 | render() {
169 | // explicit class assigning based on validation
170 | let notValidClasses = {};
171 |
172 | if (typeof this.state.dayVal == 'undefined' || this.state.dayVal) {
173 | notValidClasses.dayCls = 'no-error col-md-8';
174 | }
175 | else {
176 | notValidClasses.dayCls = 'has-error col-md-8';
177 | notValidClasses.dayValGrpCls = 'val-err-tooltip';
178 | notValidClasses.dateValGrpCls = 'val-err-tooltip';
179 | }
180 |
181 | if (typeof this.state.monthVal == 'undefined' || this.state.monthVal) {
182 | notValidClasses.monthCls = 'no-error col-md-8';
183 | }
184 | else {
185 | notValidClasses.monthCls = 'has-error col-md-8';
186 | notValidClasses.monthValGrpCls = 'val-err-tooltip';
187 | notValidClasses.dateValGrpCls = 'val-err-tooltip';
188 | }
189 |
190 | if (typeof this.state.yearVal == 'undefined' || this.state.yearVal) {
191 | notValidClasses.yearCls = 'no-error col-md-8';
192 | }
193 | else {
194 | notValidClasses.yearCls = 'has-error col-md-8';
195 | notValidClasses.yearValGrpCls = 'val-err-tooltip';
196 | notValidClasses.dateValGrpCls = 'val-err-tooltip';
197 | }
198 |
199 | let buttonClasses = {maleCls: "u-date-width", femaleCls: "u-date-width", unspecifiedCls: "u-date-width"};
200 |
201 | if (this.genderValue == 'male') {
202 | buttonClasses.maleCls = 'u-date-width button-primary';
203 | buttonClasses.femaleCls = 'u-date-width';
204 | buttonClasses.unspecifiedCls = 'u-date-width';
205 | }
206 | else if (this.genderValue == 'female') {
207 | buttonClasses.maleCls = 'u-date-width';
208 | buttonClasses.femaleCls = 'u-date-width button-primary';
209 | buttonClasses.unspecifiedCls = 'u-date-width';
210 | }
211 | else if (this.genderValue == 'unspecifed') {
212 | buttonClasses.maleCls = 'u-date-width';
213 | buttonClasses.femaleCls = 'u-date-width';
214 | buttonClasses.unspecifiedCls = 'u-date-width button-primary';
215 | }
216 | else if (this.genderValue == ''){
217 | buttonClasses.genderValGrpCls = 'val-err-tooltip';
218 | }
219 |
220 | return (
221 |
222 |
223 |
224 |
225 |
233 |
240 |
247 |
248 |
249 |
250 |
251 |
{this.state.dateValMsg}
252 |
253 |
254 |
255 |
256 |
257 |
260 |
262 |
264 |
265 |
266 |
267 |
268 |
{this.state.genderValMsg}
269 |
270 |
271 |
272 |
273 |
274 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 | )
291 | }
292 | }
293 |
294 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Promise from 'promise';
3 |
4 | export default class StepZilla extends Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | this.state = {
9 | ...this.getPrevNextBtnState(this.props.startAtStep),
10 | compState: this.props.startAtStep,
11 | navState: this.getNavStates(this.props.startAtStep, this.props.steps.length),
12 | };
13 |
14 | this.hidden = {
15 | display: 'none'
16 | };
17 |
18 | this.applyValidationFlagsToSteps();
19 | }
20 |
21 | // extend the "steps" array with flags to indicate if they have been validated
22 | applyValidationFlagsToSteps() {
23 | this.props.steps.map((i) => {
24 | if (this.props.dontValidate) {
25 | i.validated = true;
26 | }
27 | else {
28 | i.validated = (typeof i.component.type.prototype.isValidated == 'undefined') ? true : false;
29 | }
30 |
31 | return i;
32 | });
33 | }
34 |
35 | // update the header nav states via classes so they can be styled via css
36 | getNavStates(indx, length) {
37 | let styles = [];
38 |
39 | for (let i=0; i 0 && currentStep !== this.props.steps.length - 1) {
58 | if (currentStep === this.props.steps.length - 2) {
59 | correctNextText = this.props.nextTextOnFinalActionStep; // we are in the one before final step
60 | }
61 | return {
62 | showPreviousBtn: true,
63 | showNextBtn: true,
64 | nextStepText: correctNextText
65 | };
66 | } else if (currentStep === 0) {
67 | return {
68 | showPreviousBtn: false,
69 | showNextBtn: true,
70 | nextStepText: correctNextText
71 | };
72 | }
73 | return {
74 | showPreviousBtn: this.props.prevBtnOnLastStep,
75 | showNextBtn: false,
76 | nextStepText: correctNextText
77 | };
78 | }
79 |
80 | // which step are we in?
81 | checkNavState(currentStep) {
82 | this.setState(this.getPrevNextBtnState(currentStep));
83 | }
84 |
85 | // set the nav state
86 | setNavState(next) {
87 | this.setState({navState: this.getNavStates(next, this.props.steps.length)});
88 |
89 | if (next < this.props.steps.length) {
90 | this.setState({compState: next});
91 | }
92 |
93 | this.checkNavState(next);
94 | }
95 |
96 | // handles keydown on enter being pressed in any Child component input area. in this case it goes to the next
97 | handleKeyDown(evt) {
98 | if (evt.which === 13) {
99 | if (!this.props.preventEnterSubmission) {
100 | this.next();
101 | }
102 | else {
103 | evt.preventDefault();
104 | }
105 | }
106 | }
107 |
108 | // this utility method lets Child components invoke a direct jump to another step
109 | jumpToStep(evt) {
110 | if (evt.target == undefined) {
111 | // a child step wants to invoke a jump between steps. in this case 'evt' is the numeric step number and not the JS event
112 | this.setNavState(evt);
113 | }
114 | else { // the main navigation step ui is invoking a jump between steps
115 | if (!this.props.stepsNavigation || evt.target.value == this.state.compState) { // if stepsNavigation is turned off or user clicked on existing step again (on step 2 and clicked on 2 again) then ignore
116 | evt.preventDefault();
117 | evt.stopPropagation();
118 |
119 | return;
120 | }
121 |
122 | evt.persist(); // evt is a react event so we need to persist it as we deal with aync promises which nullifies these events (https://facebook.github.io/react/docs/events.html#event-pooling)
123 |
124 | const movingBack = evt.target.value < this.state.compState; // are we trying to move back or front?
125 | let passThroughStepsNotValid = false; // if we are jumping forward, only allow that if inbetween steps are all validated. This flag informs the logic...
126 | let proceed = false; // flag on if we should move on
127 |
128 | this.abstractStepMoveAllowedToPromise(movingBack)
129 | .then((valid = true) => { // validation was a success (promise or sync validation). In it was a Promise's resolve() then proceed will be undefined, so make it true. Or else 'proceed' will carry the true/false value from sync v
130 | proceed = valid;
131 |
132 | if (!movingBack) {
133 | this.updateStepValidationFlag(proceed);
134 | }
135 |
136 | if (proceed) {
137 | if (!movingBack) {
138 | // looks like we are moving forward, 'reduce' a new array of step>validated values we need to check and 'some' that to get a decision on if we should allow moving forward
139 | passThroughStepsNotValid = this.props.steps
140 | .reduce((a, c, i) => {
141 | if (i >= this.state.compState && i < evt.target.value) {
142 | a.push(c.validated);
143 | }
144 | return a;
145 | }, [])
146 | .some((c) => {
147 | return c === false
148 | })
149 | }
150 | }
151 | })
152 | .catch((e) => {
153 | // Promise based validation was a fail (i.e reject())
154 | if (!movingBack) {
155 | this.updateStepValidationFlag(false);
156 | }
157 | })
158 | .then(() => {
159 | // this is like finally(), executes if error no no error
160 | if (proceed && !passThroughStepsNotValid) {
161 | if (evt.target.value === (this.props.steps.length - 1) &&
162 | this.state.compState === (this.props.steps.length - 1)) {
163 | this.setNavState(this.props.steps.length);
164 | }
165 | else {
166 | this.setNavState(evt.target.value);
167 | }
168 | }
169 | })
170 | .catch(e => {
171 | if (e) {
172 | // see note below called "CatchRethrowing"
173 | // ... plus the finally then() above is what throws the JS Error so we need to catch that here specifically
174 | setTimeout(function() { throw e; });
175 | }
176 | });
177 | }
178 | }
179 |
180 | // move next via next button
181 | next() {
182 | this.abstractStepMoveAllowedToPromise()
183 | .then((proceed = true) => {
184 | // validation was a success (promise or sync validation). In it was a Promise's resolve() then proceed will be undefined, so make it true. Or else 'proceed' will carry the true/false value from sync validation
185 | this.updateStepValidationFlag(proceed);
186 | if (proceed) {
187 | this.setNavState(this.state.compState + 1);
188 | }
189 | })
190 | .catch((e) => {
191 | if (e) {
192 |
193 | setTimeout(function() { throw e; });
194 | }
195 |
196 | // Promise based validation was a fail (i.e reject())
197 | this.updateStepValidationFlag(false);
198 | });
199 | }
200 |
201 | // move behind via previous button
202 | previous() {
203 | if (this.state.compState > 0) {
204 | this.setNavState(this.state.compState - 1);
205 | }
206 | }
207 |
208 | // update step's validation flag
209 | updateStepValidationFlag(val = true) {
210 | this.props.steps[this.state.compState].validated = val; // note: if a step component returns 'underfined' then treat as "true".
211 | }
212 |
213 | // are we allowed to move forward? via the next button or via jumpToStep?
214 | stepMoveAllowed(skipValidationExecution = false) {
215 | let proceed = false;
216 |
217 | if (this.props.dontValidate) {
218 | proceed = true;
219 | }
220 | else {
221 | if (skipValidationExecution) {
222 | // we are moving backwards in steps, in this case dont validate as it means the user is not commiting to "save"
223 | proceed = true;
224 | }
225 | else if (this.props.hocValidationAppliedTo.length > 0 && this.props.hocValidationAppliedTo.indexOf(this.state.compState) > -1) {
226 | // the user is using a higer order component (HOC) for validation (e.g react-validation-mixin), this wraps the StepZilla steps as a HOC, so use hocValidationAppliedTo to determine if this step needs the aync validation as per react-validation-mixin interface
227 | proceed = this.refs.activeComponent.refs.component.isValidated();
228 | }
229 | else if (Object.keys(this.refs).length == 0 || typeof this.refs.activeComponent.isValidated == 'undefined') {
230 | // if its a form component, it should have implemeted a public isValidated class (also pure componenets wont even have refs - i.e. a empty object). If not then continue
231 | proceed = true;
232 | }
233 | else {
234 | // user is moving forward in steps, invoke validation as its available
235 | proceed = this.refs.activeComponent.isValidated();
236 | }
237 | }
238 |
239 | return proceed;
240 | }
241 |
242 | // a validation method is each step can be sync or async (Promise based), this utility abstracts the wrapper stepMoveAllowed to be Promise driven regardless of validation return type
243 | abstractStepMoveAllowedToPromise(movingBack) {
244 | return Promise.resolve(this.stepMoveAllowed(movingBack));
245 | }
246 |
247 | // get the classmame of steps
248 | getClassName(className, i){
249 | let liClassName = className + "-" + this.state.navState.styles[i];
250 |
251 | // if step ui based navigation is disabled, then dont highlight step
252 | if (!this.props.stepsNavigation)
253 | liClassName += " no-hl";
254 |
255 | return liClassName;
256 | }
257 |
258 | // render the steps as stepsNavigation
259 | renderSteps() {
260 | return this.props.steps.map((s, i)=> (
261 |
265 | ));
266 | }
267 |
268 | // main render of stepzilla container
269 | render() {
270 | let compToRender;
271 |
272 | // clone the step component dynamically and tag it as activeComponent so we can validate it on next. also bind the jumpToStep piping method
273 | let cloneExtensions = {
274 | jumpToStep: (t) => {
275 | this.jumpToStep(t);
276 | }
277 | };
278 |
279 | const componentPointer = this.props.steps[this.state.compState].component;
280 |
281 | // can only update refs if its a regular React component (not a pure component), so lets check that
282 | if (componentPointer instanceof Component || // unit test deteceted that instanceof Component can be in either of these locations so test both (not sure why this is the case)
283 | (componentPointer.type && componentPointer.type.prototype instanceof Component)) {
284 | cloneExtensions.ref = 'activeComponent';
285 | }
286 |
287 | compToRender = React.cloneElement(componentPointer, cloneExtensions);
288 |
289 | return (
290 |