on master help sign in
fix: stops validation flickering for unecessarilly async validations
dc028a2Matt Condon (shrugs)
src/form/validators.js+39−15
@@ -14,16 +14,18 @@ import {
14 validateEmail,
14 validateEmail,
15 validateNotNullAddress,
15 validateNotNullAddress,
16} from 'lib/validators';
16} from 'lib/validators';
17+import isPromise from 'lib/isPromise';
17
18
18// iterate over validators, exiting early if there's an error
19// iterate over validators, exiting early if there's an error
19−const buildValidator = (validators = []) => async value => {
20+const buildValidator = (validators = []) => value => {
20 for (const validator of validators) {
21 for (const validator of validators) {
21 try {
22 try {
22− const error = await validator(value);
23+ const error = validator(value);
23 if (error) {
24 if (error) {
24 return error;
25 return error;
25 }
26 }
26 } catch (error) {
27 } catch (error) {
28+ console.error(error);
27 return error.message;
29 return error.message;
28 }
30 }
29 }
31 }
@@ -93,26 +95,48 @@ export const composeValidator = (
93 );
95 );
94
96
95 // async reduce errors per-field into an errors object
97 // async reduce errors per-field into an errors object
96− const fieldLevelValidator = async values => {
98+ const fieldLevelValidator = values => {
97− const errors = await Promise.all(
99+ const errorsOrPromises = names.map((name, i) =>
98− names.map((name, i) => fieldLevelValidators[i](values[name]))
100+ fieldLevelValidators[i](values[name])
99 );
101 );
100
102
103+ const reduce = errors =>
101− return names.reduce(
104+ names.reduce(
102− (memo, name, i) => ({
105+ (memo, name, i) => ({
103− ...memo,
106+ ...memo,
104− [name]: errors[i],
107+ [name]: errors[i],
105− }),
108+ }),
109+ {}
110+ );
111+
112+ if (some(errorsOrPromises, isPromise)) {
113+ // if any of these results are a promise, await them all then reduce
114+ return Promise.all(errorsOrPromises).then(reduce);
106− {}
115+ }
107− );
116+
117+ // otherwise return immediately
118+ return reduce(errorsOrPromises);
108 };
119 };
109
120
110 // the final-form `validate` function
121 // the final-form `validate` function
122+ // NOTE: if we return a Promise to final-form it will toggle the `validating`
123+ // state, which is expected. If our promise resolves immediately, however,
124+ // that means our `validating` state flickers the UI and it looks pretty bad.
125+ // The solution is to conditionally return a promise only when necessary.
111− return async values => {
126+ return values => {
112 // ask for field-level errors
127 // ask for field-level errors
113− const errors = await fieldLevelValidator(values);
128+ const errorsOrPromise = fieldLevelValidator(values);
129+
114 // pass the current values and their validity to the form-level validator
130 // pass the current values and their validity to the form-level validator
115 // that can implement conditional logic and more complex validations
131 // that can implement conditional logic and more complex validations
116− return await formValidator(values, errors);
132+ const runFormValidator = errors => formValidator(values, errors);
133+
134+ if (errorsOrPromise.then) {
135+ // if promise, promise
136+ return errorsOrPromise.then(runFormValidator);
137+ }
138+
139+ // otherwise, it's an errors object
140+ return runFormValidator(errorsOrPromise);
117 };
141 };
118};
142};
src/lib/isPromise.jsnew file+6
@@ -0,0 +1,6 @@
1+// an object is a promise if it has a `.then` function
2+// https://github.com/then/is-promise/blob/master/index.js
3+export default obj =>
4+ !!obj &&
5+ (typeof obj === 'object' || typeof obj === 'function') &&
6+ typeof obj.then === 'function';
src/views/CreateGalaxy.js+12−9
@@ -59,16 +59,16 @@ export default function CreateGalaxy() {
59 bind,
59 bind,
60 } = useCreateGalaxy();
60 } = useCreateGalaxy();
61
61
62− const validateGalaxy = useCallback(
62+ const validateForm = useCallback(
63− async galaxyName => {
63+ async values => {
64 const currentOwner = await azimuth.azimuth.getOwner(
64 const currentOwner = await azimuth.azimuth.getOwner(
65 _contracts,
65 _contracts,
66− patp2dec(galaxyName)
66+ patp2dec(values.galaxyName)
67 );
67 );
68
68
69 const isAvailable = isZeroAddress(currentOwner);
69 const isAvailable = isZeroAddress(currentOwner);
70 if (!isAvailable) {
70 if (!isAvailable) {
71− return 'This galaxy is already spawned and owned.';
71+ return { galaxyName: 'This galaxy is already spawned and owned.' };
72 }
72 }
73 },
73 },
74 [_contracts]
74 [_contracts]
@@ -76,11 +76,14 @@ export default function CreateGalaxy() {
76
76
77 const validate = useMemo(
77 const validate = useMemo(
78 () =>
78 () =>
79− composeValidator({
79+ composeValidator(
80+ {
80− galaxyName: buildPointValidator(1, [validateGalaxy]),
81+ galaxyName: buildPointValidator(1),
81− owner: buildAddressValidator(),
82+ owner: buildAddressValidator(),
83+ },
84+ validateForm
82− }),
85+ ),
83− [validateGalaxy]
86+ [validateForm]
84 );
87 );
85
88
86 const onValues = useCallback(
89 const onValues = useCallback(
src/views/IssueChild.js+12−9
@@ -81,13 +81,13 @@ export default function IssueChild() {
81 bind,
81 bind,
82 } = useIssueChild();
82 } = useIssueChild();
83
83
84− const validatePoint = useCallback(
84+ const validateForm = useCallback(
85− async name => {
85+ async values => {
86− const point = patp2dec(name);
86+ const point = patp2dec(values.name);
87 const hasPoint = (await availablePointsPromise).has(point);
87 const hasPoint = (await availablePointsPromise).has(point);
88
88
89 if (!hasPoint) {
89 if (!hasPoint) {
90− return 'This point cannot be spawned.';
90+ return { point: 'This point cannot be spawned.' };
91 }
91 }
92 },
92 },
93 [availablePointsPromise]
93 [availablePointsPromise]
@@ -95,11 +95,14 @@ export default function IssueChild() {
95
95
96 const validate = useMemo(
96 const validate = useMemo(
97 () =>
97 () =>
98− composeValidator({
98+ composeValidator(
99+ {
99− point: buildPointValidator(4, [validatePoint]),
100+ point: buildPointValidator(4),
100− owner: buildAddressValidator(),
101+ owner: buildAddressValidator(),
102+ },
103+ validateForm
101− }),
104+ ),
102− [validatePoint]
105+ [validateForm]
103 );
106 );
104
107
105 const onValues = useCallback(
108 const onValues = useCallback(
src/views/Login/Ticket.js+36−29
@@ -43,34 +43,13 @@ export default function Ticket({ className, goHome }) {
43
43
44 const cachedUrbitWallet = useRef(Nothing());
44 const cachedUrbitWallet = useRef(Nothing());
45
45
46− const validateForm = useCallback(
46+ const validateFormAsync = useCallback(
47− async (values, errors) => {
47+ async (values, ticket) => {
48− if (errors.point) {
49− return errors;
50− }
51−
52− await timeout(16); // allow ui events to flush
53−
54− let ticket;
55− if (values.useShards) {
56− if (errors.shard1 || errors.shard2 || errors.shard3) {
57− return errors;
58− }
59−
60− ticket = kg.combine([values.shard1, values.shard2, values.shard3]);
61− } else {
62− if (errors.ticket) {
63− return errors;
64− }
65−
66− ticket = values.ticket;
67− }
68−
69 try {
48 try {
70− // ticket
71 const _contracts = need.contracts(contracts);
49 const _contracts = need.contracts(contracts);
72 const point = patp2dec(values.point);
50 const point = patp2dec(values.point);
73
51
52+ await timeout(16); // allow ui events to flush
74 cachedUrbitWallet.current = await urbitWalletFromTicket(
53 cachedUrbitWallet.current = await urbitWalletFromTicket(
75 ticket,
54 ticket,
76 point,
55 point,
@@ -92,11 +71,12 @@ export default function Ticket({ className, goHome }) {
92
71
93 const noPermissions = !isOwner && !isTransferProxy;
72 const noPermissions = !isOwner && !isTransferProxy;
94 // notify the user, but allow login regardless
73 // notify the user, but allow login regardless
95− addWarning({
96− point: noPermissions
74+ if (noPermissions) {
75+ addWarning({
76+ point:
97− ? 'This wallet is not the owner or transfer proxy for this point.'
77+ 'This wallet is not the owner or transfer proxy for this point.',
98− : null,
99− });
78+ });
79+ }
100 } catch (error) {
80 } catch (error) {
101 console.error(error);
81 console.error(error);
102 return {
82 return {
@@ -109,6 +89,33 @@ export default function Ticket({ className, goHome }) {
109 [addWarning, contracts]
89 [addWarning, contracts]
110 );
90 );
111
91
92+ const validateForm = useCallback(
93+ (values, errors) => {
94+ if (errors.point) {
95+ addWarning({ point: null });
96+ return errors;
97+ }
98+
99+ let ticket;
100+ if (values.useShards) {
101+ if (errors.shard1 || errors.shard2 || errors.shard3) {
102+ return errors;
103+ }
104+
105+ ticket = kg.combine([values.shard1, values.shard2, values.shard3]);
106+ } else {
107+ if (errors.ticket) {
108+ return errors;
109+ }
110+
111+ ticket = values.ticket;
112+ }
113+
114+ return validateFormAsync(values, ticket);
115+ },
116+ [addWarning, validateFormAsync]
117+ );
118+
112 const validate = useMemo(
119 const validate = useMemo(
113 () =>
120 () =>
114 composeValidator(
121 composeValidator(