├── .env ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── env.d.ts ├── hubspot-form.component.vue ├── hubspot-form.ts ├── main.ts ├── types.ts └── use-hubspot-form.ts ├── tsconfig.json └── vite.config.ts /.env: -------------------------------------------------------------------------------- 1 | # these values are fake 2 | # create own .env.local file that will override these 3 | VITE_REGION="eu1" 4 | VITE_PORTAL_ID="83991272" 5 | VITE_FORM_ID="25f1e214-1236-45c3-810m-d8dk31736c72" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .env.local 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["vue.volar", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.renderWhitespace": "all", 3 | "[typescript][vue][html][json][jsonc][markdown]": { 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HubSpot Vue Form 2 | 3 | A Vue wrapper for HubSpot Forms 4 | 5 | ## Usage 6 | 7 | ```shell 8 | npm install @jagaad/vue-hubspot-form 9 | ``` 10 | 11 | ### Component usage: 12 | 13 | ```vue 14 | 17 | 25 | ``` 26 | 27 | ### Hook usage: 28 | 29 | ```vue 30 | 34 | 39 | ``` 40 | 41 | All examples make use partially of code blocks defined below 42 | 43 |
44 | Example 1: using `onReady` as emit 45 | 46 | ```vue 47 | 55 | ``` 56 | 57 |
58 | 59 |
60 | Example 2: using `onReady` as callback 61 | 62 | ```vue 63 | 71 | ``` 72 | 73 |
74 | 75 |
76 | Example 3: inject CSS via options 77 | 78 | ```tsx 79 | import { CreateOptions } from '@jagaad/vue-hubspot-form'; 80 | 81 | // these values are fake, add your own 82 | const options: CreateOptions = { 83 | // ... 84 | // Read the official docs for more info 85 | cssRequired: `.hubspot-link__container { display: none }`, 86 | // ... 87 | }; 88 | ``` 89 | 90 |
91 | 92 |
93 | Example 4: inject CSS in `onReady` callback 94 | 95 | ```tsx 96 | import { Payload } from '@jagaad/vue-hubspot-form'; 97 | 98 | function onReady({ iframeDocument: doc }: Payload) { 99 | const element = doc.createElement('style'); 100 | const styles = `.hubspot-link__container { display: none }`; 101 | element.appendChild(doc.createTextNode(styles)); 102 | doc.head.appendChild(element); 103 | } 104 | ``` 105 | 106 |
107 | 108 | 109 | 110 |
111 | Example 5: inject CSS using JSS via options 112 | 113 | ```tsx 114 | import jss, { Rule } from 'jss'; 115 | 116 | jss.use({ 117 | // this will make JSS to use selectors as names 118 | onCreateRule(name, _decl, options) { 119 | options.selector = name; 120 | return null as unknown as Rule; 121 | }, 122 | }); 123 | 124 | const styleSheet = jss.createStyleSheet({ 125 | '.hubspot-link__container': { 126 | display: 'none', 127 | }, 128 | }); 129 | 130 | const options = { 131 | // ... 132 | cssRequired: styleSheet.toString(), 133 | }; 134 | ``` 135 | 136 |
137 | 138 |
139 | Code Blocks 140 | 141 | **Options:** 142 | 143 | ```tsx 144 | import { CreateOptions } from '@jagaad/vue-hubspot-form'; 145 | 146 | // these values are fake, add your own 147 | const options: CreateOptions = { 148 | region: 'eu1', 149 | portalId: '83991272', 150 | formId: '25f1e214-1236-45c3-810m-d8dk31736c72', 151 | // ... 152 | }; 153 | ``` 154 | 155 | **On Ready callback:** 156 | 157 | ```tsx 158 | import { Payload } from '@jagaad/vue-hubspot-form'; 159 | 160 | const onReady = (payload: Payload) => console.log(payload); 161 | ``` 162 | 163 | **Fallback Components:** 164 | 165 | ```tsx 166 | import { defineComponent } from 'vue'; 167 | 168 | // Loading Component 169 | const fallback = defineComponent({ 170 | /* ... */ 171 | }); 172 | // Error Component 173 | const error = defineComponent({ 174 | /* ... */ 175 | }); 176 | ``` 177 | 178 |
179 | 180 | ## Contributing 181 | 182 | ```shell 183 | git clone git@github.com:jagaad/vue-hubspot-form.git 184 | cd vue-hubspot-form 185 | npm install 186 | ``` 187 | 188 | - Create a copy of `.env` file to `.env.local` 189 | - Adjust `.env.local` to your HubSpot values 190 | 191 | ``` 192 | npm run dev 193 | ``` 194 | 195 | ## Links 196 | 197 | - https://developers.hubspot.com/docs/cms/building-blocks/forms 198 | - https://legacydocs.hubspot.com/docs/methods/forms/advanced_form_options 199 | - https://github.com/escaladesports/react-hubspot-form/blob/master/src/index.js 200 | 201 | # Vue 3 + Typescript + Vite 202 | 203 | This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jagaad/vue-hubspot-form", 3 | "version": "2.2.3", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/jagaad/vue-hubspot-form.git" 7 | }, 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "scripts": { 12 | "dev": "vite", 13 | "build": "vue-tsc --noEmit && vite build", 14 | "serve": "vite preview", 15 | "types": "vue-tsc --declaration --emitDeclarationOnly src/hubspot-form.ts --declarationDir dist", 16 | "prepublishOnly": "npm run build && npm run types", 17 | "test": "echo \"Error: no test specified\" && exit 0", 18 | "prepare": "husky install" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "main": "./dist/hubspot-form.umd.js", 24 | "module": "./dist/hubspot-form.es.js", 25 | "types": "./dist/hubspot-form.d.ts", 26 | "exports": { 27 | ".": { 28 | "import": "./dist/hubspot-form.es.js", 29 | "require": "./dist/hubspot-form.umd.js" 30 | } 31 | }, 32 | "peerDependencies": { 33 | "@vue/composition-api": "^1.4.0" 34 | }, 35 | "dependencies": { 36 | "vue-demi": "^0.13.11" 37 | }, 38 | "peerDependenciesMeta": { 39 | "@vue/composition-api": { 40 | "optional": true 41 | } 42 | }, 43 | "devDependencies": { 44 | "@babel/types": "^7.20.2", 45 | "@jagaad/prettier-config": "^1.1.1", 46 | "@types/node": "^18.11.9", 47 | "@vitejs/plugin-vue": "^3.2.0", 48 | "husky": "^8.0.2", 49 | "jss": "^10.9.2", 50 | "lint-staged": "^13.0.3", 51 | "np": "^7.6.2", 52 | "prettier": "^2.7.1", 53 | "typescript": "^4.9.3", 54 | "vite": "^3.2.4", 55 | "vue": "^3.2.23", 56 | "vue-tsc": "^1.0.9" 57 | }, 58 | "prettier": "@jagaad/prettier-config", 59 | "lint-staged": { 60 | "*.{ts,vue,json,html,md}": "npx prettier --write" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | 10 | interface ImportMetaEnv extends Readonly> { 11 | readonly VITE_REGION: string; 12 | readonly VITE_PORTAL_ID: string; 13 | readonly VITE_FORM_ID: string; 14 | } 15 | 16 | interface ImportMeta { 17 | readonly env: ImportMetaEnv; 18 | } 19 | -------------------------------------------------------------------------------- /src/hubspot-form.component.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 48 | -------------------------------------------------------------------------------- /src/hubspot-form.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { useHubspotForm } from './use-hubspot-form'; 3 | export { default } from './hubspot-form.component.vue'; 4 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, defineComponent, h } from 'vue'; 2 | import HubSpotForm from './hubspot-form.component.vue'; 3 | import jss, { Rule } from 'jss'; 4 | 5 | document.body.style.margin = '0'; 6 | const size = { width: '100vw', height: '100vh' }; 7 | const createSquare = (backgroundColor: string) => 8 | defineComponent({ 9 | render: () => h('div', { style: { ...size, backgroundColor } }), 10 | }); 11 | 12 | const fallback = createSquare('#bada55'); 13 | const error = createSquare('#b00b55'); 14 | 15 | jss.use({ 16 | onCreateRule(name, _decl, options) { 17 | options.selector = name; 18 | return null as unknown as Rule; 19 | }, 20 | }); 21 | 22 | const styleSheet = jss.createStyleSheet({ 23 | '.hubspot-link__container': { 24 | display: 'none', 25 | }, 26 | }); 27 | 28 | const options = { 29 | region: import.meta.env.VITE_REGION, 30 | portalId: import.meta.env.VITE_PORTAL_ID, 31 | formId: import.meta.env.VITE_FORM_ID, 32 | cssRequired: styleSheet.toString(), 33 | }; 34 | 35 | const props = { fallback, error, options }; 36 | createApp(HubSpotForm, props).mount('#app'); 37 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Payload = { 2 | hbspt: HubSpot; 3 | form: Form; 4 | iframe: HTMLIFrameElement; 5 | iframeDocument: Document; 6 | }; 7 | export type Form = { id: string; onReady: (cb: () => void) => void }; 8 | export type HubSpot = { forms: { create: (options: _CreateOptions) => Form } }; 9 | export type OnReady = (payload: Payload) => void | Promise; 10 | 11 | /** 12 | * All possible options for creating HubSpot form 13 | * `target` is added by wrapper 14 | * https://legacydocs.hubspot.com/docs/methods/forms/advanced_form_options 15 | */ 16 | export type CreateOptions = Omit<_CreateOptions, 'target'>; 17 | type _CreateOptions = { 18 | portalId: string; 19 | formId: string; 20 | region: string; 21 | target: string; 22 | redirectUrl?: string; 23 | inlineMessage?: string; 24 | pageId?: string; 25 | cssRequired?: string; 26 | cssClass?: string; 27 | submitButtonClass?: string; 28 | errorClass?: string; 29 | errorMessageClass?: string; 30 | groupErrors?: boolean; 31 | locale?: string; 32 | translations?: Record; 33 | manuallyBlockedEmailDomain?: string[]; 34 | formInstanceId?: string; 35 | onBeforeFormInit?: (...args: unknown[]) => unknown; 36 | // Works only if you have jQuery on page 37 | onFormReady?: (...args: unknown[]) => unknown; 38 | onFormSubmit?: (...args: unknown[]) => unknown; 39 | onFormSubmitted?: (...args: unknown[]) => unknown; 40 | }; 41 | -------------------------------------------------------------------------------- /src/use-hubspot-form.ts: -------------------------------------------------------------------------------- 1 | import { ref, watchPostEffect, onErrorCaptured } from 'vue-demi'; 2 | import { CreateOptions, HubSpot, OnReady } from './types'; 3 | 4 | // This will load script only once, even if form is rendered multiple times 5 | let loadingScript = 6 | typeof window !== 'undefined' ? loadHubSpotScript() : undefined; 7 | const noop = () => {}; 8 | 9 | export function useHubspotForm( 10 | options: CreateOptions, 11 | onReady: OnReady = noop, 12 | ) { 13 | const isLoading = ref(true); 14 | const isError = ref(false); 15 | const divRef = ref(); 16 | 17 | function error() { 18 | if (divRef.value) divRef.value.hidden = true; 19 | isLoading.value = false; 20 | isError.value = true; 21 | } 22 | 23 | onErrorCaptured(error); 24 | 25 | watchPostEffect(async () => { 26 | if (!divRef.value) return error(); 27 | 28 | divRef.value.hidden = true; 29 | isLoading.value = true; 30 | isError.value = false; 31 | 32 | loadingScript = loadingScript ?? loadHubSpotScript(); 33 | 34 | const hbspt = await loadingScript.catch(error); 35 | 36 | if (!hbspt) return error(); 37 | 38 | const id = `id-${Math.random().toString().slice(2)}`; 39 | divRef.value.id = id; 40 | 41 | const form = hbspt.forms.create({ ...options, target: `#${id}` }); 42 | 43 | if (!form) return error(); 44 | 45 | form.onReady(async () => { 46 | if (!divRef.value) return; 47 | 48 | const iframe = 49 | divRef.value.querySelector(':scope iframe') ?? 50 | undefined; 51 | const iframeDocument = iframe?.contentDocument ?? undefined; 52 | const html = iframeDocument?.documentElement; 53 | 54 | if (!html || !iframe) return error(); 55 | 56 | const payload = { hbspt, form, iframe, iframeDocument }; 57 | 58 | await onReady(payload); 59 | 60 | isLoading.value = false; 61 | isError.value = false; 62 | divRef.value.hidden = false; 63 | }); 64 | }); 65 | 66 | return { divRef, isLoading, isError }; 67 | } 68 | 69 | function loadHubSpotScript() { 70 | return loadScript('//js-eu1.hsforms.net/forms/shell.js', 'hbspt'); 71 | } 72 | 73 | function loadScript(src: string, umdName: string) { 74 | return new Promise((resolve, reject) => { 75 | const script = window.document.createElement('script'); 76 | script.src = src; 77 | script.defer = true; 78 | script.onload = () => resolve((window as any)[umdName] as Type); 79 | script.onerror = reject; 80 | window.document.head.appendChild(script); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"], 13 | "skipLibCheck": true 14 | }, 15 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import path from 'path'; 3 | import vue from '@vitejs/plugin-vue'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | optimizeDeps: { 9 | exclude: ['vue-demi'], 10 | }, 11 | build: { 12 | lib: { 13 | entry: path.resolve(__dirname, 'src/hubspot-form.ts'), 14 | name: 'HubSpotForm', 15 | fileName: (format) => `hubspot-form.${format}.js`, 16 | }, 17 | rollupOptions: { 18 | external: ['vue', 'vue-demi'], 19 | output: { 20 | exports: 'named', 21 | globals: { 22 | 'vue': 'Vue', 23 | 'vue-demi': 'VueDemi', 24 | }, 25 | }, 26 | }, 27 | }, 28 | }); 29 | --------------------------------------------------------------------------------