Skip to content

Commit 0af2e20

Browse files
authored
Chrome extension support (#150)
* chore(ts): upgrade `target` version to `es6` * feat: add chrome extension support * ci: upgrade node version to 16 * refactor: optimize getting control
1 parent a9f75d3 commit 0af2e20

File tree

9 files changed

+179
-24
lines changed

9 files changed

+179
-24
lines changed

.github/workflows/main.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ jobs:
2828
run: |
2929
npm run lint:types
3030
npm run lint
31-
# - name: Test
32-
# run: npm test
33-
# env:
34-
# CI: true
31+
# - name: Test
32+
# run: npm test
33+
# env:
34+
# CI: true
3535
- name: Build
3636
run: npm run build

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414
with:
1515
fetch-depth: 0
1616

17-
- name: Use Node.js 12.x
17+
- name: Use Node.js 16.x
1818
uses: actions/setup-node@v1
1919
with:
20-
version: 12.x
20+
version: 16.x
2121

2222
- name: Install Dependencies
2323
run: yarn --frozen-lockfile

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@
6363
"@types/lodash": "^4.14.168",
6464
"little-state-machine": "^4.1.0",
6565
"lodash": "^4.17.21",
66-
"react-simple-animate": "^3.3.12"
66+
"nanoid": "^4.0.0",
67+
"react-simple-animate": "^3.3.12",
68+
"use-deep-compare-effect": "^1.8.1"
6769
},
6870
"devDependencies": {
6971
"@babel/core": "^7.13.16",

src/devTool.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { createStore, StateMachineProvider } from 'little-state-machine';
12
import * as React from 'react';
2-
import { StateMachineProvider, createStore } from 'little-state-machine';
33
import { Control, FieldValues, useFormContext } from 'react-hook-form';
44
import { DevToolUI } from './devToolUI';
5+
import { useExportControlToExtension } from './extension/useExportControlToExtension';
56
import type { PLACEMENT } from './position';
67

78
if (typeof window !== 'undefined') {
@@ -25,10 +26,17 @@ export const DevTool = <T extends FieldValues>(props?: {
2526
}) => {
2627
const methods = useFormContext();
2728

29+
const { isExtensionEnabled } = useExportControlToExtension(
30+
props?.control ?? methods.control,
31+
);
32+
if (isExtensionEnabled) {
33+
return null;
34+
}
35+
2836
return (
2937
<StateMachineProvider>
3038
<DevToolUI
31-
control={(props && props.control) || methods.control}
39+
control={props?.control ?? methods.control}
3240
placement={props?.placement}
3341
/>
3442
</StateMachineProvider>

src/extension/types.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export type UpdatePayload<
2+
TFieldValues extends Record<string, any> = Record<string, any>,
3+
> = {
4+
id: string;
5+
data: {
6+
formValues: TFieldValues;
7+
formState: {
8+
errors: Record<keyof TFieldValues, { type?: string; message?: string }>;
9+
dirtyFields: Record<keyof TFieldValues, boolean>;
10+
touchedFields: Record<keyof TFieldValues, boolean>;
11+
nativeFields: Record<keyof TFieldValues, boolean>;
12+
submitCount: number;
13+
isSubmitted: boolean;
14+
isSubmitting: boolean;
15+
isSubmitSuccessful: boolean;
16+
isValid: boolean;
17+
isValidating: boolean;
18+
isDirty: boolean;
19+
};
20+
};
21+
};
22+
23+
type InitMessageData = {
24+
source: string;
25+
type: 'INIT' | 'WELCOME';
26+
};
27+
28+
type UpdateMessageData<
29+
TFieldValues extends Record<string, any> = Record<string, any>,
30+
> = {
31+
source: string;
32+
type: 'UPDATE';
33+
payload: UpdatePayload<TFieldValues>;
34+
};
35+
36+
export type MessageData<
37+
TFieldValues extends Record<string, any> = Record<string, any>,
38+
> = InitMessageData | UpdateMessageData<TFieldValues>;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import _ from 'lodash';
2+
import { nanoid } from 'nanoid';
3+
import { useEffect, useState } from 'react';
4+
import { Control, useFormState, useWatch } from 'react-hook-form';
5+
import useDeepCompareEffect from 'use-deep-compare-effect';
6+
import { MessageData, UpdatePayload } from './types';
7+
import { proxyToObject } from './utils';
8+
9+
const id = nanoid();
10+
11+
export function useExportControlToExtension(control: Control<any>) {
12+
const nestedFormValues = useWatch({ control });
13+
const formState = useFormState({ control });
14+
15+
const [isExtensionEnabled, setIsExtensionEnabled] = useState(false);
16+
17+
const handleInitMessage = (message: MessageEvent<MessageData>) => {
18+
if (
19+
message.data.source !== 'react-hook-form-bridge' ||
20+
message.data.type !== 'INIT'
21+
) {
22+
return;
23+
}
24+
window.postMessage({
25+
source: 'react-hook-form-bridge',
26+
type: 'WELCOME',
27+
} as MessageData);
28+
setIsExtensionEnabled(true);
29+
};
30+
31+
useEffect(() => {
32+
window.addEventListener('message', handleInitMessage);
33+
return () => window.removeEventListener('message', handleInitMessage);
34+
}, []);
35+
36+
const toFlat = <V>(obj: object, defaultValue?: V) => {
37+
return [...control._names.mount].reduce((perv, name) => {
38+
// nested field may be `undefined`
39+
perv[name] = _.get(obj, name) || defaultValue;
40+
return perv;
41+
}, {} as Record<string, V>);
42+
};
43+
44+
useDeepCompareEffect(() => {
45+
if (!isExtensionEnabled) {
46+
return;
47+
}
48+
49+
const {
50+
errors: nestedErrors,
51+
dirtyFields: nestedDirtyFields,
52+
touchedFields: nestedTouchedFields,
53+
...formStatus
54+
} = proxyToObject(formState);
55+
56+
const formValues = toFlat(nestedFormValues, '');
57+
const dirtyFields = toFlat(nestedDirtyFields, false);
58+
const touchedFields = toFlat(nestedTouchedFields, false);
59+
60+
const errors = Object.entries(
61+
toFlat<{ type: string; message: string }>(nestedErrors),
62+
).reduce((perv, [key, value]) => {
63+
perv[key] = {
64+
type: value?.type as string,
65+
message: value?.message as string,
66+
};
67+
return perv;
68+
}, {} as Record<string, { type?: string; message?: string }>);
69+
70+
const nativeFields = [...control._names.mount].reduce((perv, name) => {
71+
perv[name] = !!_.get(control._fields, name)?._f?.ref?.type;
72+
return perv;
73+
}, {} as Record<string, boolean>);
74+
75+
const updateMessagePayload: UpdatePayload = {
76+
id,
77+
data: {
78+
formValues,
79+
formState: {
80+
errors,
81+
dirtyFields,
82+
touchedFields,
83+
nativeFields,
84+
...formStatus,
85+
},
86+
},
87+
};
88+
window.postMessage({
89+
source: 'react-hook-form-bridge',
90+
type: 'UPDATE',
91+
payload: updateMessagePayload,
92+
} as MessageData);
93+
}, [isExtensionEnabled, nestedFormValues, proxyToObject(formState)]);
94+
95+
return { isExtensionEnabled };
96+
}

src/extension/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function proxyToObject<T extends Record<string, any>>(proxy: T) {
2+
return Reflect.ownKeys(proxy).reduce((perv, key) => {
3+
perv[key as keyof T] = proxy[key as keyof T];
4+
return perv;
5+
}, {} as T);
6+
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"sourceMap": true,
44
"module": "esnext",
5-
"target": "es5",
5+
"target": "es6",
66
"moduleResolution": "node",
77
"outDir": "./dist",
88
"jsx": "react",

yarn.lock

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5486,6 +5486,11 @@ [email protected]:
54865486
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
54875487
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
54885488

5489+
dequal@^2.0.2:
5490+
version "2.0.2"
5491+
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
5492+
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
5493+
54895494
des.js@^1.0.0:
54905495
version "1.0.1"
54915496
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@@ -6402,11 +6407,6 @@ fill-range@^4.0.0:
64026407
version "4.0.0"
64036408
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
64046409
integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==
6405-
dependencies:
6406-
extend-shallow "^2.0.1"
6407-
is-number "^3.0.0"
6408-
repeat-string "^1.6.1"
6409-
to-regex-range "^2.1.0"
64106410

64116411
fill-range@^7.0.1:
64126412
version "7.0.1"
@@ -9188,6 +9188,11 @@ nanoid@^3.3.1:
91889188
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
91899189
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
91909190

9191+
nanoid@^4.0.0:
9192+
version "4.0.0"
9193+
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.0.tgz#6e144dee117609232c3f415c34b0e550e64999a5"
9194+
integrity sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==
9195+
91919196
nanomatch@^1.2.9:
91929197
version "1.2.13"
91939198
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -10692,7 +10697,7 @@ repeat-element@^1.1.2:
1069210697
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
1069310698
integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
1069410699

10695-
repeat-string@^1.5.4, repeat-string@^1.6.1:
10700+
repeat-string@^1.5.4:
1069610701
version "1.6.1"
1069710702
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
1069810703
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
@@ -11928,14 +11933,6 @@ to-object-path@^0.3.0:
1192811933
dependencies:
1192911934
kind-of "^3.0.2"
1193011935

11931-
to-regex-range@^2.1.0:
11932-
version "2.1.1"
11933-
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
11934-
integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==
11935-
dependencies:
11936-
is-number "^3.0.0"
11937-
repeat-string "^1.6.1"
11938-
1193911936
to-regex-range@^5.0.1:
1194011937
version "5.0.1"
1194111938
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -12381,6 +12378,14 @@ url@^0.11.0:
1238112378
punycode "1.3.2"
1238212379
querystring "0.2.0"
1238312380

12381+
use-deep-compare-effect@^1.8.1:
12382+
version "1.8.1"
12383+
resolved "https://registry.yarnpkg.com/use-deep-compare-effect/-/use-deep-compare-effect-1.8.1.tgz#ef0ce3b3271edb801da1ec23bf0754ef4189d0c6"
12384+
integrity sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==
12385+
dependencies:
12386+
"@babel/runtime" "^7.12.5"
12387+
dequal "^2.0.2"
12388+
1238412389
use@^3.1.0:
1238512390
version "3.1.1"
1238612391
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"

0 commit comments

Comments
 (0)