Skip to content

Commit c420230

Browse files
committed
feat(TU-3717): Fetch form details when selected
1 parent c625733 commit c420230

7 files changed

Lines changed: 152 additions & 34 deletions

File tree

README.md

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,23 +89,23 @@ window.tfEmbedAdmin.setDefaultConfiguration({
8989

9090
When using HTML API you don't need to call this method separately. You need to specify config options on the button itself.
9191

92-
### selectForm({ callback})
92+
### selectForm({ callback })
9393

9494
Open embed admin to select form or create a new one.
9595

9696
It accepts an object with the following props:
9797

98-
| name | type | description |
99-
| -------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
100-
| callback | `(payload: { action: string, formId: string }) => void` | Method to be called when a form is selected in Typeform Admin UI. |
101-
| type | `"iframe" \| "popup"` | Optional. See `setDefaultConfiguration` above. |
102-
| appName | `string` | Optional. See `setDefaultConfiguration` above. |
98+
| name | type | description |
99+
| -------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
100+
| callback | `(payload: { action: string, formId: string, fetchFormDetails: () => Promise<{}> }) => void` | Method to be called when a form is selected in Typeform Admin UI. |
101+
| type | `"iframe" \| "popup"` | Optional. See `setDefaultConfiguration` above. |
102+
| appName | `string` | Optional. See `setDefaultConfiguration` above. |
103103

104104
Example with JavaScript:
105105

106106
```javascript
107107
window.tfEmbedAdmin.selectForm({
108-
callback: ({ action, formId }) => console.log(`you just selected form id: ${formId}`),
108+
callback: ({ action, formId, fetchFormDetails }) => console.log(`you just selected form id: ${formId}`),
109109
})
110110
```
111111

@@ -121,7 +121,7 @@ Or with HTML API:
121121
select typeform
122122
</button>
123123
<script>
124-
function embedAdminCallback({ action }) {
124+
function embedAdminCallback({ action, formId, fetchFormDetails }) {
125125
// callback function needs to be available on global scope (window)
126126
}
127127
</script>
@@ -133,19 +133,19 @@ Open embed admin to edit a specific form.
133133

134134
It accepts an object with the following props:
135135

136-
| name | type | description |
137-
| -------- | ------------------------------------------------------- | --------------------------------------------------------------- |
138-
| formId | `string` | ID of the typeform to edit |
139-
| callback | `(payload: { action: string, formId: string }) => void` | Method to be called when a form is edited in Typeform Admin UI. |
140-
| type | `"iframe" \| "popup"` | Optional. See `setDefaultConfiguration` above. |
141-
| appName | `string` | Optional. See `setDefaultConfiguration` above. |
136+
| name | type | description |
137+
| -------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
138+
| formId | `string` | ID of the typeform to edit |
139+
| callback | `(payload: { action: string, formId: string, fetchFormDetails: () => Promise<{}> }) => void` | Method to be called when a form is edited in Typeform Admin UI. |
140+
| type | `"iframe" \| "popup"` | Optional. See `setDefaultConfiguration` above. |
141+
| appName | `string` | Optional. See `setDefaultConfiguration` above. |
142142

143143
Example with JavaScript:
144144

145145
```javascript
146146
window.tfEmbedAdmin.editForm({
147147
formId: myTypeformId,
148-
callback: ({ action, formId }) => console.log(`you just edited form id: ${formId}`),
148+
callback: ({ action, formId, fetchFormDetails }) => console.log(`you just edited form id: ${formId}`),
149149
})
150150
```
151151

@@ -161,12 +161,27 @@ Or with HTML API:
161161
edit typeform
162162
</button>
163163
<script>
164-
function embedAdminCallback({ action, formId }) {
164+
function embedAdminCallback({ action, formId, fetchFormDetails }) {
165165
// callback function needs to be available on global scope (window)
166166
}
167167
</script>
168168
```
169169

170+
### fetchFormDetails()
171+
172+
The callback receives `fetchFormDetails` async method in the payload. You can use this method to fetch details about currently selected / edited form. It returns `title`, `url` and `imageUrl` of the meta image.
173+
174+
Usage:
175+
176+
```javascript
177+
window.tfEmbedAdmin.selectForm({
178+
callback: async ({ action, formId, fetchFormDetails }) => {
179+
const { title, url } = await fetchFormDetails()
180+
console.log(`You selected form named ${title}. You can visit it at ${url}.`)
181+
},
182+
})
183+
```
184+
170185
## Demo
171186

172187
Run:
@@ -175,10 +190,12 @@ Run:
175190
yarn start
176191
```
177192

178-
Demo implementation of the library will be served at http://localhost:9090
193+
Demo implementation of the library will be served at http://localhost:1337
179194

180195
Or [open the demo in CodeSandbox](https://codesandbox.io/s/github/Typeform/button), directly in your browser.
181196

197+
_Note:_ Examples with iframe only work on localhost.
198+
182199
## Development
183200

184201
Requirements:

demo/embed.html

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,10 @@ <h1>Typeform Button Demo - with Embed SDK</h1>
4747
<script>
4848
window.tfEmbedAdmin.setDefaultConfiguration({ type: 'iframe', appName: 'embed-demo-app' })
4949

50-
const fetchTypeformDetails = async (formId) => {
51-
const result = await fetch(
52-
`https://form.typeform.com/oembed?url=${encodeURIComponent(`https://form.typeform.com/to/${formId}`)}`,
53-
)
54-
if (!result.ok) {
55-
return {}
56-
}
57-
const data = await result.json()
58-
const { title, author_url: url, thumbnail_url: image } = data || {}
59-
return { title, url, image: image?.href ?? image }
60-
}
61-
const onSelect = async ({ action, formId }) => {
50+
const onSelect = async ({ action, formId, fetchFormDetails }) => {
6251
console.log('selected form:', formId)
6352

64-
const { title, image } = await fetchTypeformDetails(formId)
53+
const { title, imageUrl } = await fetchFormDetails()
6554

6655
const container = document.createElement('li')
6756

@@ -70,7 +59,7 @@ <h1>Typeform Button Demo - with Embed SDK</h1>
7059
container.append(heading)
7160

7261
const thumbnail = document.createElement('img')
73-
thumbnail.src = image
62+
thumbnail.src = imageUrl
7463
container.append(thumbnail)
7564

7665
const viewButton = document.createElement('button')

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"build": "esbuild index=src/index.ts button=src/browser.ts --bundle --format=esm --minify --sourcemap",
1111
"watch": "yarn build --watch",
1212
"dist": "yarn build --outdir=dist",
13-
"start": "yarn watch --serve=9090 --servedir=demo --outdir=demo/dist",
13+
"start": "yarn watch --serve=1337 --servedir=demo --outdir=demo/dist",
1414
"lint": "eslint src --ext .js,.ts,.jsx,.tsx --max-warnings=0 && yarn prettier-check",
1515
"prettier-check": "prettier --check . --ignore-path .eslintignore",
1616
"prettier": "prettier --write . --ignore-path .eslintignore",
@@ -40,6 +40,7 @@
4040
"husky": "^8.0.3",
4141
"jest": "^29.7.0",
4242
"jest-environment-jsdom": "^29.7.0",
43+
"jest-fetch-mock": "^3.0.3",
4344
"jsdom": "^22.1.0",
4445
"prettier": "^3.1.0",
4546
"semantic-release": "^22.0.8",

src/lib/fetch-form-details.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import fetchMock from 'jest-fetch-mock'
2+
3+
import { fetchFormDetails } from './fetch-form-details'
4+
5+
fetchMock.enableMocks()
6+
7+
describe('#fetchFormDetails', () => {
8+
beforeEach(() => {
9+
fetchMock.resetMocks()
10+
})
11+
12+
it('fetches data from oembed URL', async () => {
13+
fetchMock.mockReturnValueOnce(new Promise((res) => res(new Response('{}'))))
14+
await fetchFormDetails('12345')
15+
expect(fetchMock).toHaveBeenCalledWith(
16+
`https://form.typeform.com/oembed?url=${encodeURIComponent('https://form.typeform.com/to/12345')}`,
17+
)
18+
})
19+
20+
it('returns empty object when it fails to fetch form details', async () => {
21+
fetchMock.mockReject(() => Promise.reject('error'))
22+
const formDetails = await fetchFormDetails('12345')
23+
expect(formDetails).toEqual({})
24+
})
25+
26+
it('returns form details when it fetches form details', async () => {
27+
const title = 'foobar'
28+
const url = 'https://form.typeform.com/to/12345'
29+
const imageUrl = 'https://images.typeform.com/images/abcde'
30+
const oembedBodyMock = JSON.stringify({
31+
title,
32+
author_url: url,
33+
thumbnail_url: imageUrl,
34+
})
35+
fetchMock.mockReturnValueOnce(new Promise((res) => res(new Response(oembedBodyMock))))
36+
const formDetails = await fetchFormDetails('12345')
37+
expect(formDetails).toEqual({
38+
title,
39+
url,
40+
imageUrl,
41+
})
42+
})
43+
})

src/lib/fetch-form-details.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export interface FormDetails {
2+
title?: string
3+
url?: string
4+
imageUrl?: string
5+
}
6+
export const fetchFormDetails = async (formId: string): Promise<FormDetails> => {
7+
const host = 'https://form.typeform.com'
8+
const formUrl = `${host}/to/${formId}`
9+
10+
try {
11+
const result = await fetch(`${host}/oembed?url=${encodeURIComponent(formUrl)}`)
12+
if (!result.ok) {
13+
return {}
14+
}
15+
const data = await result.json()
16+
const { title, author_url: url, thumbnail_url: image } = data || {}
17+
return { title, url, imageUrl: image?.href ?? image }
18+
} catch (e) {
19+
return {}
20+
}
21+
}

src/lib/open.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { getEmbedAdminDefaultAppName, getEmbedAdminUrl } from './utils'
22
import { buildIframe } from './build-iframe'
33
import { buildPopup } from './build-popup'
44
import { addMessageHandler, EmbedAdminAction } from './add-message-handler'
5+
import { fetchFormDetails, FormDetails } from './fetch-form-details'
56

67
export type EmbedAdminActionPayload = {
78
formId: string
89
action: EmbedAdminAction
10+
fetchFormDetails: () => Promise<FormDetails>
911
}
1012

1113
export type EmbedAdminCallback = (payload: EmbedAdminActionPayload) => void
@@ -43,9 +45,9 @@ export const open: OpenTypeformEmbedAdmin = (config) => {
4345
const formId = hasFormId(config) ? config.formId : undefined
4446
const url = getEmbedAdminUrl(action, appName ?? getEmbedAdminDefaultAppName(), formId)
4547

46-
const removeMessageHandler = addMessageHandler((formId: string) => {
48+
const removeMessageHandler = addMessageHandler(async (formId: string) => {
4749
close()
48-
callback && callback({ action, formId })
50+
callback && callback({ action, formId, fetchFormDetails: () => fetchFormDetails(formId) })
4951
})
5052

5153
const { close } = type === 'iframe' ? buildIframe(url, removeMessageHandler) : buildPopup(url, removeMessageHandler)

yarn.lock

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3542,6 +3542,13 @@ create-jest@^29.7.0:
35423542
jest-util "^29.7.0"
35433543
prompts "^2.0.1"
35443544

3545+
cross-fetch@^3.0.4:
3546+
version "3.1.8"
3547+
resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82"
3548+
integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==
3549+
dependencies:
3550+
node-fetch "^2.6.12"
3551+
35453552
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
35463553
version "7.0.3"
35473554
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -5665,6 +5672,14 @@ jest-environment-node@^29.7.0:
56655672
jest-mock "^29.7.0"
56665673
jest-util "^29.7.0"
56675674

5675+
jest-fetch-mock@^3.0.3:
5676+
version "3.0.3"
5677+
resolved "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
5678+
integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
5679+
dependencies:
5680+
cross-fetch "^3.0.4"
5681+
promise-polyfill "^8.1.3"
5682+
56685683
jest-get-type@^29.6.3:
56695684
version "29.6.3"
56705685
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1"
@@ -6693,6 +6708,13 @@ node-emoji@^2.1.0:
66936708
emojilib "^2.4.0"
66946709
skin-tone "^2.0.0"
66956710

6711+
node-fetch@^2.6.12:
6712+
version "2.7.0"
6713+
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
6714+
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
6715+
dependencies:
6716+
whatwg-url "^5.0.0"
6717+
66966718
node-gyp@^10.0.0, node-gyp@^10.0.1:
66976719
version "10.0.1"
66986720
resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-10.0.1.tgz#205514fc19e5830fa991e4a689f9e81af377a966"
@@ -7388,6 +7410,11 @@ promise-inflight@^1.0.1:
73887410
resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
73897411
integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==
73907412

7413+
promise-polyfill@^8.1.3:
7414+
version "8.3.0"
7415+
resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
7416+
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==
7417+
73917418
promise-retry@^2.0.1:
73927419
version "2.0.1"
73937420
resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
@@ -8419,6 +8446,11 @@ tr46@^4.1.1:
84198446
dependencies:
84208447
punycode "^2.3.0"
84218448

8449+
tr46@~0.0.3:
8450+
version "0.0.3"
8451+
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
8452+
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
8453+
84228454
traverse@~0.6.6:
84238455
version "0.6.7"
84248456
resolved "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz#46961cd2d57dd8706c36664acde06a248f1173fe"
@@ -8767,6 +8799,11 @@ wcwidth@^1.0.0:
87678799
dependencies:
87688800
defaults "^1.0.3"
87698801

8802+
webidl-conversions@^3.0.0:
8803+
version "3.0.1"
8804+
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
8805+
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
8806+
87708807
webidl-conversions@^7.0.0:
87718808
version "7.0.0"
87728809
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
@@ -8800,6 +8837,14 @@ whatwg-url@^12.0.0, whatwg-url@^12.0.1:
88008837
tr46 "^4.1.1"
88018838
webidl-conversions "^7.0.0"
88028839

8840+
whatwg-url@^5.0.0:
8841+
version "5.0.0"
8842+
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
8843+
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
8844+
dependencies:
8845+
tr46 "~0.0.3"
8846+
webidl-conversions "^3.0.0"
8847+
88038848
which-boxed-primitive@^1.0.2:
88048849
version "1.0.2"
88058850
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"

0 commit comments

Comments
 (0)