Skip to content

Commit

Permalink
feat(TU-3717): Expose method to fetch form details in callback (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
mathio authored Dec 6, 2023
1 parent c625733 commit f667d42
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 50 deletions.
51 changes: 34 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,23 @@ window.tfEmbedAdmin.setDefaultConfiguration({

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

### selectForm({ callback})
### selectForm({ callback })

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

It accepts an object with the following props:

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

Example with JavaScript:

```javascript
window.tfEmbedAdmin.selectForm({
callback: ({ action, formId }) => console.log(`you just selected form id: ${formId}`),
callback: ({ action, formId, fetchFormDetails }) => console.log(`you just selected form id: ${formId}`),
})
```

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

It accepts an object with the following props:

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

Example with JavaScript:

```javascript
window.tfEmbedAdmin.editForm({
formId: myTypeformId,
callback: ({ action, formId }) => console.log(`you just edited form id: ${formId}`),
callback: ({ action, formId, fetchFormDetails }) => console.log(`you just edited form id: ${formId}`),
})
```

Expand All @@ -161,12 +161,27 @@ Or with HTML API:
edit typeform
</button>
<script>
function embedAdminCallback({ action, formId }) {
function embedAdminCallback({ action, formId, fetchFormDetails }) {
// callback function needs to be available on global scope (window)
}
</script>
```

### fetchFormDetails()

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.

Usage:

```javascript
window.tfEmbedAdmin.selectForm({
callback: async ({ action, formId, fetchFormDetails }) => {
const { title, url } = await fetchFormDetails()
console.log(`You selected form named ${title}. You can visit it at ${url}.`)
},
})
```

## Demo

Run:
Expand All @@ -175,10 +190,12 @@ Run:
yarn start
```

Demo implementation of the library will be served at http://localhost:9090
Demo implementation of the library will be served at http://localhost:1337

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

_Note:_ Examples with iframe only work on localhost.

## Development

Requirements:
Expand Down
17 changes: 3 additions & 14 deletions demo/embed.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,10 @@ <h1>Typeform Button Demo - with Embed SDK</h1>
<script>
window.tfEmbedAdmin.setDefaultConfiguration({ type: 'iframe', appName: 'embed-demo-app' })

const fetchTypeformDetails = async (formId) => {
const result = await fetch(
`https://form.typeform.com/oembed?url=${encodeURIComponent(`https://form.typeform.com/to/${formId}`)}`,
)
if (!result.ok) {
return {}
}
const data = await result.json()
const { title, author_url: url, thumbnail_url: image } = data || {}
return { title, url, image: image?.href ?? image }
}
const onSelect = async ({ action, formId }) => {
const onSelect = async ({ action, formId, fetchFormDetails }) => {
console.log('selected form:', formId)

const { title, image } = await fetchTypeformDetails(formId)
const { title, imageUrl } = await fetchFormDetails()

const container = document.createElement('li')

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

const thumbnail = document.createElement('img')
thumbnail.src = image
thumbnail.src = imageUrl
container.append(thumbnail)

const viewButton = document.createElement('button')
Expand Down
12 changes: 8 additions & 4 deletions demo/iframe-html.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ <h1>Typeform Button Demo - Iframe HTML</h1>
function handleClick() {
console.log('select button clicked')
}
function handleEdit({ action, formId }) {
console.log(`form ${formId} was edited`)
async function handleEdit({ action, formId, fetchFormDetails }) {
const { title } = await fetchFormDetails()
console.log(`form ${title} (${formId}) was edited`)
}
function handleSelect({ action, formId }) {
async function handleSelect({ action, formId, fetchFormDetails }) {
const id = `form-${Date.now()}`
document.querySelector('#typeforms').innerHTML += `<li id="${id}">loading...</li>`
const { title } = await fetchFormDetails()
console.log(action, formId)
const editButton = `<button data-tf-embed-admin-edit="${formId}" data-tf-embed-admin-type="iframe" data-tf-embed-admin-callback="handleEdit">edit</button>`
document.querySelector('#typeforms').innerHTML += `<li>${formId} ${editButton}</li>`
document.querySelector(`#${id}`).innerHTML = `${title} (${formId}) ${editButton}`
window.tfEmbedAdmin.load()
}
</script>
Expand Down
11 changes: 8 additions & 3 deletions demo/iframe-js.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,28 @@ <h1>Typeform Button Demo - Iframe JS</h1>
<script>
window.tfEmbedAdmin.setDefaultConfiguration({ type: 'iframe' })

const callback = ({ action, formId }) => {
const callback = async ({ action, formId, fetchFormDetails }) => {
if (action === 'edit') {
console.log(`form ${formId} was edited`)
const { title } = await fetchFormDetails()
console.log(`form ${title} (${formId}) was edited`)
return
}

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

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

const button = document.createElement('button')
button.onclick = () => editTypeform(formId)
button.innerText = 'Edit'
li.append(button)

document.querySelector('#typeforms').append(li)

const { title } = await fetchFormDetails()
li.querySelector('span').innerText = title
}
const selectTypeform = () => {
window.tfEmbedAdmin.selectForm({ callback })
Expand Down
21 changes: 15 additions & 6 deletions demo/popup-html.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@
</head>
<body>
<h1>Typeform Button Demo - Popup HTML</h1>
<button data-tf-embed-admin-select data-tf-embed-admin-callback="handleSelect">select typeform</button>
<button data-tf-embed-admin-select data-tf-embed-admin-callback="handleSelect" onclick="handleClick()">
select typeform
</button>
<ul id="typeforms"></ul>
<script src="dist/button.js"></script>
<script>
function handleEdit({ action, formId }) {
console.log(`form ${formId} was edited`)
function handleClick() {
console.log('select button clicked')
}
function handleSelect({ action, formId }) {
async function handleEdit({ action, formId, fetchFormDetails }) {
const { title } = await fetchFormDetails()
console.log(`form ${title} (${formId}) was edited`)
}
async function handleSelect({ action, formId, fetchFormDetails }) {
const id = `form-${Date.now()}`
document.querySelector('#typeforms').innerHTML += `<li id="${id}">loading...</li>`
const { title } = await fetchFormDetails()
console.log(action, formId)
const editButton = `<button data-tf-embed-admin-edit="${formId}" data-tf-embed-admin-callback="handleEdit">edit</button>`
document.querySelector('#typeforms').innerHTML += `<li>${formId} ${editButton}</li>`
const editButton = `<button data-tf-embed-admin-edit="${formId}" data-tf-embed-admin-type="iframe" data-tf-embed-admin-callback="handleEdit">edit</button>`
document.querySelector(`#${id}`).innerHTML = `${title} (${formId}) ${editButton}`
window.tfEmbedAdmin.load()
}
</script>
Expand Down
11 changes: 8 additions & 3 deletions demo/popup-js.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,28 @@
<h1>Typeform Button Demo - Popup JS</h1>
<script src="dist/button.js"></script>
<script>
const callback = ({ action, formId }) => {
const callback = async ({ action, formId, fetchFormDetails }) => {
if (action === 'edit') {
console.log(`form ${formId} was edited`)
const { title } = await fetchFormDetails()
console.log(`form ${title} (${formId}) was edited`)
return
}

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

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

const button = document.createElement('button')
button.onclick = () => editTypeform(formId)
button.innerText = 'Edit'
li.append(button)

document.querySelector('#typeforms').append(li)

const { title } = await fetchFormDetails()
li.querySelector('span').innerText = title
}
const selectTypeform = () => {
window.tfEmbedAdmin.selectForm({ callback })
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"build": "esbuild index=src/index.ts button=src/browser.ts --bundle --format=esm --minify --sourcemap",
"watch": "yarn build --watch",
"dist": "yarn build --outdir=dist",
"start": "yarn watch --serve=9090 --servedir=demo --outdir=demo/dist",
"start": "yarn watch --serve=1337 --servedir=demo --outdir=demo/dist",
"lint": "eslint src --ext .js,.ts,.jsx,.tsx --max-warnings=0 && yarn prettier-check",
"prettier-check": "prettier --check . --ignore-path .eslintignore",
"prettier": "prettier --write . --ignore-path .eslintignore",
Expand Down Expand Up @@ -40,6 +40,7 @@
"husky": "^8.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"jsdom": "^22.1.0",
"prettier": "^3.1.0",
"semantic-release": "^22.0.8",
Expand Down
43 changes: 43 additions & 0 deletions src/lib/fetch-form-details.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import fetchMock from 'jest-fetch-mock'

import { fetchFormDetails } from './fetch-form-details'

fetchMock.enableMocks()

describe('#fetchFormDetails', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('fetches data from oembed URL', async () => {
fetchMock.mockReturnValueOnce(new Promise((res) => res(new Response('{}'))))
await fetchFormDetails('12345')
expect(fetchMock).toHaveBeenCalledWith(
`https://form.typeform.com/oembed?url=${encodeURIComponent('https://form.typeform.com/to/12345')}`,
)
})

it('returns empty object when it fails to fetch form details', async () => {
fetchMock.mockReject(() => Promise.reject('error'))
const formDetails = await fetchFormDetails('12345')
expect(formDetails).toEqual({})
})

it('returns form details when it fetches form details', async () => {
const title = 'foobar'
const url = 'https://form.typeform.com/to/12345'
const imageUrl = 'https://images.typeform.com/images/abcde'
const oembedBodyMock = JSON.stringify({
title,
author_url: url,
thumbnail_url: imageUrl,
})
fetchMock.mockReturnValueOnce(new Promise((res) => res(new Response(oembedBodyMock))))
const formDetails = await fetchFormDetails('12345')
expect(formDetails).toEqual({
title,
url,
imageUrl,
})
})
})
21 changes: 21 additions & 0 deletions src/lib/fetch-form-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface FormDetails {
title?: string
url?: string
imageUrl?: string
}
export const fetchFormDetails = async (formId: string): Promise<FormDetails> => {
const host = 'https://form.typeform.com'
const formUrl = `${host}/to/${formId}`

try {
const result = await fetch(`${host}/oembed?url=${encodeURIComponent(formUrl)}`)
if (!result.ok) {
return {}
}
const data = await result.json()
const { title, author_url: url, thumbnail_url: image } = data || {}
return { title, url, imageUrl: image?.href ?? image }
} catch (e) {
return {}
}
}
6 changes: 4 additions & 2 deletions src/lib/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { getEmbedAdminDefaultAppName, getEmbedAdminUrl } from './utils'
import { buildIframe } from './build-iframe'
import { buildPopup } from './build-popup'
import { addMessageHandler, EmbedAdminAction } from './add-message-handler'
import { fetchFormDetails, FormDetails } from './fetch-form-details'

export type EmbedAdminActionPayload = {
formId: string
action: EmbedAdminAction
fetchFormDetails: () => Promise<FormDetails>
}

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

const removeMessageHandler = addMessageHandler((formId: string) => {
const removeMessageHandler = addMessageHandler(async (formId: string) => {
close()
callback && callback({ action, formId })
callback && callback({ action, formId, fetchFormDetails: () => fetchFormDetails(formId) })
})

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

0 comments on commit f667d42

Please sign in to comment.