Skip to content

Commit f667d42

Browse files
authored
feat(TU-3717): Expose method to fetch form details in callback (#4)
1 parent c625733 commit f667d42

11 files changed

+191
-50
lines changed

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')

demo/iframe-html.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ <h1>Typeform Button Demo - Iframe HTML</h1>
2121
function handleClick() {
2222
console.log('select button clicked')
2323
}
24-
function handleEdit({ action, formId }) {
25-
console.log(`form ${formId} was edited`)
24+
async function handleEdit({ action, formId, fetchFormDetails }) {
25+
const { title } = await fetchFormDetails()
26+
console.log(`form ${title} (${formId}) was edited`)
2627
}
27-
function handleSelect({ action, formId }) {
28+
async function handleSelect({ action, formId, fetchFormDetails }) {
29+
const id = `form-${Date.now()}`
30+
document.querySelector('#typeforms').innerHTML += `<li id="${id}">loading...</li>`
31+
const { title } = await fetchFormDetails()
2832
console.log(action, formId)
2933
const editButton = `<button data-tf-embed-admin-edit="${formId}" data-tf-embed-admin-type="iframe" data-tf-embed-admin-callback="handleEdit">edit</button>`
30-
document.querySelector('#typeforms').innerHTML += `<li>${formId} ${editButton}</li>`
34+
document.querySelector(`#${id}`).innerHTML = `${title} (${formId}) ${editButton}`
3135
window.tfEmbedAdmin.load()
3236
}
3337
</script>

demo/iframe-js.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,28 @@ <h1>Typeform Button Demo - Iframe JS</h1>
1111
<script>
1212
window.tfEmbedAdmin.setDefaultConfiguration({ type: 'iframe' })
1313

14-
const callback = ({ action, formId }) => {
14+
const callback = async ({ action, formId, fetchFormDetails }) => {
1515
if (action === 'edit') {
16-
console.log(`form ${formId} was edited`)
16+
const { title } = await fetchFormDetails()
17+
console.log(`form ${title} (${formId}) was edited`)
1718
return
1819
}
1920

2021
console.log('selected form:', formId)
2122

2223
const li = document.createElement('li')
23-
li.innerText = `Form: ${formId}`
24+
li.id = `form-${Date.now()}`
25+
li.innerHTML = `Form: <span>....</span> (${formId})`
2426

2527
const button = document.createElement('button')
2628
button.onclick = () => editTypeform(formId)
2729
button.innerText = 'Edit'
2830
li.append(button)
2931

3032
document.querySelector('#typeforms').append(li)
33+
34+
const { title } = await fetchFormDetails()
35+
li.querySelector('span').innerText = title
3136
}
3237
const selectTypeform = () => {
3338
window.tfEmbedAdmin.selectForm({ callback })

demo/popup-html.html

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,26 @@
77
</head>
88
<body>
99
<h1>Typeform Button Demo - Popup HTML</h1>
10-
<button data-tf-embed-admin-select data-tf-embed-admin-callback="handleSelect">select typeform</button>
10+
<button data-tf-embed-admin-select data-tf-embed-admin-callback="handleSelect" onclick="handleClick()">
11+
select typeform
12+
</button>
1113
<ul id="typeforms"></ul>
1214
<script src="dist/button.js"></script>
1315
<script>
14-
function handleEdit({ action, formId }) {
15-
console.log(`form ${formId} was edited`)
16+
function handleClick() {
17+
console.log('select button clicked')
1618
}
17-
function handleSelect({ action, formId }) {
19+
async function handleEdit({ action, formId, fetchFormDetails }) {
20+
const { title } = await fetchFormDetails()
21+
console.log(`form ${title} (${formId}) was edited`)
22+
}
23+
async function handleSelect({ action, formId, fetchFormDetails }) {
24+
const id = `form-${Date.now()}`
25+
document.querySelector('#typeforms').innerHTML += `<li id="${id}">loading...</li>`
26+
const { title } = await fetchFormDetails()
1827
console.log(action, formId)
19-
const editButton = `<button data-tf-embed-admin-edit="${formId}" data-tf-embed-admin-callback="handleEdit">edit</button>`
20-
document.querySelector('#typeforms').innerHTML += `<li>${formId} ${editButton}</li>`
28+
const editButton = `<button data-tf-embed-admin-edit="${formId}" data-tf-embed-admin-type="iframe" data-tf-embed-admin-callback="handleEdit">edit</button>`
29+
document.querySelector(`#${id}`).innerHTML = `${title} (${formId}) ${editButton}`
2130
window.tfEmbedAdmin.load()
2231
}
2332
</script>

demo/popup-js.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,28 @@
99
<h1>Typeform Button Demo - Popup JS</h1>
1010
<script src="dist/button.js"></script>
1111
<script>
12-
const callback = ({ action, formId }) => {
12+
const callback = async ({ action, formId, fetchFormDetails }) => {
1313
if (action === 'edit') {
14-
console.log(`form ${formId} was edited`)
14+
const { title } = await fetchFormDetails()
15+
console.log(`form ${title} (${formId}) was edited`)
1516
return
1617
}
1718

1819
console.log('selected form:', formId)
1920

2021
const li = document.createElement('li')
21-
li.innerText = `Form: ${formId}`
22+
li.id = `form-${Date.now()}`
23+
li.innerHTML = `Form: <span>....</span> (${formId})`
2224

2325
const button = document.createElement('button')
2426
button.onclick = () => editTypeform(formId)
2527
button.innerText = 'Edit'
2628
li.append(button)
2729

2830
document.querySelector('#typeforms').append(li)
31+
32+
const { title } = await fetchFormDetails()
33+
li.querySelector('span').innerText = title
2934
}
3035
const selectTypeform = () => {
3136
window.tfEmbedAdmin.selectForm({ callback })

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)

0 commit comments

Comments
 (0)