├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── blocks.ts ├── components.ts ├── index.ts └── traits.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .settings/ 3 | .sass-cache/ 4 | .project 5 | .eslintrc 6 | .npmrc 7 | npm-debug.log 8 | style/.sass-cache/ 9 | 10 | dist/ 11 | img/ 12 | images/ 13 | private/ 14 | docs/ 15 | vendor/ 16 | coverage/ 17 | node_modules/ 18 | bower_components/ 19 | _index.html 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Artur Arseniev 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | - Neither the name "GrapesJS" nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrapesJS Forms 2 | 3 | This plugin adds some of the basic form components and blocks which help in working with forms easier 4 | 5 | [Demo](http://grapesjs.com/demo.html) 6 |
7 | 8 | Available components: 9 | `form` 10 | `input` 11 | `textarea` 12 | `select` 13 | `option` 14 | `checkbox` 15 | `radio` 16 | `button` 17 | `label` 18 | 19 | 20 | 21 | ## Options 22 | 23 | | Option | Description | Default| 24 | | --------------- | -------------------------------- | ----------------------------------------------------------------------------------------| 25 | |`blocks`|Which blocks to add| `['form', 'input', 'textarea', 'select', 'button', 'label', 'checkbox', 'radio']` (all) | 26 | |`category`|Category name|`Forms`| 27 | |`block`|Add custom block options, based on block id.|`(blockId) => ({})`| 28 | 29 | 30 | 31 | ## Download 32 | 33 | * CDN 34 | * `https://unpkg.com/grapesjs-plugin-forms` 35 | * NPM 36 | * `npm i grapesjs-plugin-forms` 37 | * GIT 38 | * `git clone https://github.com/GrapesJS/components-forms.git` 39 | 40 | 41 | 42 | ## Usage 43 | 44 | Directly in the browser 45 | 46 | ```html 47 | 48 | 49 | 50 | 51 |
52 | 53 | 63 | ``` 64 | 65 | Modern javascript 66 | 67 | ```js 68 | import grapesjs from 'grapesjs'; 69 | import gjsForms from 'grapesjs-plugin-forms'; 70 | 71 | const editor = grapesjs.init({ 72 | container : '#gjs', 73 | // ... 74 | plugins: [gjsForms], 75 | pluginsOpts: { 76 | [gjsForms]: { /* options */ } 77 | } 78 | // or 79 | plugins: [ 80 | editor => gjsForms(editor, { /* options */ }), 81 | ], 82 | }); 83 | ``` 84 | 85 | ## I18n 86 | 87 | If you need to change some of the components/traits labels, you can rely on the i18n module, here a complete example for the default `en` language 88 | 89 | ```js 90 | editor.I18n.addMessages({ 91 | en: { 92 | blockManager: { 93 | labels: { 94 | form: 'EN Form', 95 | input: 'EN Input', 96 | textarea: 'EN Textarea', 97 | select: 'EN Select', 98 | checkbox: 'EN Checkbox', 99 | radio: 'EN Radio', 100 | button: 'EN Button', 101 | label: 'EN Label', 102 | }, 103 | categories: { 104 | forms: 'EN Forms', 105 | } 106 | }, 107 | domComponents: { 108 | names: { 109 | form: 'EN Form', 110 | input: 'EN Input', 111 | textarea: 'EN Textarea', 112 | select: 'EN Select', 113 | checkbox: 'EN Checkbox', 114 | radio: 'EN Radio', 115 | button: 'EN Button', 116 | label: 'EN Label', 117 | }, 118 | }, 119 | traitManager: { 120 | traits: { 121 | labels: { 122 | method: 'EN Method', 123 | action: 'EN Action', 124 | name: 'EN Name', 125 | placeholder: 'EN Placeholder', 126 | type: 'EN Type', 127 | required: 'EN Required', 128 | options: 'EN Options', 129 | id: 'EN Id', 130 | for: 'EN For', 131 | value: 'EN Value', 132 | checked: 'EN Checked', 133 | text: 'EN Text', 134 | }, 135 | options: { 136 | type: { 137 | text: 'EN Text', 138 | email: 'EN Email', 139 | password: 'EN Password', 140 | number: 'EN Number', 141 | submit: 'EN Submit', 142 | reset: 'EN Reset', 143 | button: 'EN Button', 144 | } 145 | } 146 | }, 147 | }, 148 | } 149 | }); 150 | ``` 151 | 152 | 153 | ## Development 154 | 155 | Clone the repository 156 | 157 | ```sh 158 | $ git clone https://github.com/GrapesJS/components-forms.git 159 | $ cd grapesjs-plugin-forms 160 | ``` 161 | 162 | Install it 163 | 164 | ```sh 165 | $ npm i 166 | ``` 167 | 168 | Start the dev server 169 | 170 | ```sh 171 | $ npm start 172 | ``` 173 | 174 | 175 | ## License 176 | 177 | BSD 3-Clause 178 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GrapesJS Plugin Forms 6 | 7 | 8 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 41 |
42 |
43 | Test 44 |
45 |
46 | 1 47 | 2 48 | 3 49 |
50 | 51 |
52 | 54 |
55 | 56 | 57 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjs-plugin-forms", 3 | "version": "2.0.6", 4 | "description": "Set of forms components and blocks for GrapesJS editor", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/" 8 | ], 9 | "scripts": { 10 | "build": "grapesjs-cli build", 11 | "start": "grapesjs-cli serve" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/GrapesJS/components-forms.git" 16 | }, 17 | "keywords": [ 18 | "grapesjs", 19 | "plugin", 20 | "form", 21 | "builder" 22 | ], 23 | "author": "Artur Arseniev", 24 | "license": "BSD-3-Clause", 25 | "devDependencies": { 26 | "grapesjs": "^0.21.2", 27 | "grapesjs-cli": "^4.1.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/blocks.ts: -------------------------------------------------------------------------------- 1 | import type { BlockProperties, Editor } from 'grapesjs'; 2 | import { PluginOptions } from '.'; 3 | import { 4 | typeButton, 5 | typeCheckbox, 6 | typeForm, 7 | typeInput, 8 | typeLabel, 9 | typeRadio, 10 | typeSelect, 11 | typeTextarea, 12 | } from './components'; 13 | 14 | export default function (editor: Editor, opt: Required) { 15 | const opts = opt; 16 | const bm = editor.BlockManager; 17 | const addBlock = (id: string, def: BlockProperties) => { 18 | opts.blocks?.indexOf(id)! >= 0 && bm.add(id, { 19 | ...def, 20 | category: opts.category, 21 | select: true, 22 | ...opt.block(id), 23 | }); 24 | } 25 | 26 | addBlock(typeForm, { 27 | label: 'Form', 28 | media: '', 29 | content: { 30 | type: typeForm, 31 | components: [ 32 | { 33 | components: [ 34 | { type: typeLabel, components: 'Name' }, 35 | { type: typeInput }, 36 | ] 37 | }, { 38 | components: [ 39 | { type: typeLabel, components: 'Email' }, 40 | { type: typeInput, attributes: { type: 'email' } }, 41 | ] 42 | }, { 43 | components: [ 44 | { type: typeLabel, components: 'Gender' }, 45 | { type: typeCheckbox, attributes: { value: 'M' } }, 46 | { type: typeLabel, components: 'M' }, 47 | { type: typeCheckbox, attributes: { value: 'F' } }, 48 | { type: typeLabel, components: 'F' }, 49 | ] 50 | }, { 51 | components: [ 52 | { type: typeLabel, components: 'Message' }, 53 | { type: typeTextarea }, 54 | ] 55 | }, { 56 | components: [{ type: typeButton }] 57 | }, 58 | ] 59 | } 60 | }); 61 | 62 | addBlock(typeInput, { 63 | label: 'Input', 64 | media: '', 65 | content: { type: typeInput }, 66 | }); 67 | 68 | addBlock(typeTextarea, { 69 | label: 'Textarea', 70 | media: '', 71 | content: { type: typeTextarea }, 72 | }); 73 | 74 | addBlock(typeSelect, { 75 | label: 'Select', 76 | media: '', 77 | content: { type: typeSelect }, 78 | }); 79 | 80 | addBlock(typeButton, { 81 | label: 'Button', 82 | media: '', 83 | content: { type: typeButton }, 84 | }); 85 | 86 | addBlock(typeLabel, { 87 | label: 'Label', 88 | media: '', 89 | content: { type: typeLabel }, 90 | }); 91 | 92 | addBlock(typeCheckbox, { 93 | label: 'Checkbox', 94 | media: '', 95 | content: { type: typeCheckbox }, 96 | }); 97 | 98 | addBlock(typeRadio, { 99 | label: 'Radio', 100 | media: '', 101 | content: { type: typeRadio }, 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /src/components.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from 'grapesjs'; 2 | 3 | export const typeForm = 'form'; 4 | export const typeInput = 'input'; 5 | export const typeTextarea = 'textarea'; 6 | export const typeSelect = 'select'; 7 | export const typeCheckbox = 'checkbox'; 8 | export const typeRadio = 'radio'; 9 | export const typeButton = 'button'; 10 | export const typeLabel = 'label'; 11 | export const typeOption = 'option'; 12 | 13 | export default function(editor: Editor) { 14 | const { Components } = editor; 15 | 16 | const idTrait = { 17 | name: 'id', 18 | }; 19 | 20 | const forTrait = { 21 | name: 'for', 22 | }; 23 | 24 | const nameTrait = { 25 | name: 'name', 26 | }; 27 | 28 | const placeholderTrait = { 29 | name: 'placeholder', 30 | }; 31 | 32 | const valueTrait = { 33 | name: 'value', 34 | }; 35 | 36 | const requiredTrait = { 37 | type: 'checkbox', 38 | name: 'required', 39 | }; 40 | 41 | const checkedTrait = { 42 | type: 'checkbox', 43 | name: 'checked', 44 | }; 45 | 46 | const createOption = (value: string, content: string) => { 47 | return { type: typeOption, content, attributes: { value } }; 48 | }; 49 | 50 | const checkIfInPreview = (ev: Event) => { 51 | if (!editor.Commands.isActive('preview')) { 52 | ev.preventDefault(); 53 | } 54 | }; 55 | 56 | Components.addType(typeForm, { 57 | isComponent: el => el.tagName == 'FORM', 58 | 59 | model: { 60 | defaults: { 61 | tagName: 'form', 62 | droppable: ':not(form)', 63 | draggable: ':not(form)', 64 | attributes: { method: 'get' }, 65 | traits: [{ 66 | type: 'select', 67 | name: 'method', 68 | options: [ 69 | {value: 'get', name: 'GET'}, 70 | {value: 'post', name: 'POST'}, 71 | ], 72 | }, { 73 | name: 'action', 74 | }], 75 | }, 76 | }, 77 | 78 | view: { 79 | events: { 80 | // The submit of the form might redirect the user from the editor so 81 | // we should always prevent the default here. 82 | submit: (e: Event) => e.preventDefault(), 83 | } as any 84 | }, 85 | }); 86 | 87 | 88 | 89 | 90 | 91 | // INPUT 92 | Components.addType(typeInput, { 93 | isComponent: el => el.tagName == 'INPUT', 94 | 95 | model: { 96 | defaults: { 97 | tagName: 'input', 98 | droppable: false, 99 | highlightable: false, 100 | attributes: { type: 'text' }, 101 | traits: [ 102 | nameTrait, 103 | placeholderTrait, 104 | { 105 | type: 'select', 106 | name: 'type', 107 | options: [ 108 | { value: 'text' }, 109 | { value: 'email' }, 110 | { value: 'password' }, 111 | { value: 'number' }, 112 | ] 113 | }, 114 | requiredTrait 115 | ], 116 | }, 117 | }, 118 | 119 | extendFnView: ['updateAttributes'], 120 | view: { 121 | updateAttributes() { 122 | this.el.setAttribute('autocomplete', 'off'); 123 | }, 124 | } 125 | }); 126 | 127 | 128 | 129 | 130 | 131 | // TEXTAREA 132 | Components.addType(typeTextarea, { 133 | extend: typeInput, 134 | isComponent: el => el.tagName == 'TEXTAREA', 135 | 136 | model: { 137 | defaults: { 138 | tagName: 'textarea', 139 | attributes: {}, 140 | traits: [ 141 | nameTrait, 142 | placeholderTrait, 143 | requiredTrait 144 | ] 145 | }, 146 | }, 147 | }); 148 | 149 | 150 | 151 | 152 | 153 | // OPTION 154 | Components.addType(typeOption, { 155 | isComponent: el => el.tagName == 'OPTION', 156 | 157 | model: { 158 | defaults: { 159 | tagName: 'option', 160 | layerable: false, 161 | droppable: false, 162 | draggable: false, 163 | highlightable: false, 164 | }, 165 | }, 166 | }); 167 | 168 | 169 | 170 | 171 | 172 | // SELECT 173 | Components.addType(typeSelect, { 174 | isComponent: el => el.tagName == 'SELECT', 175 | 176 | model: { 177 | defaults: { 178 | tagName: 'select', 179 | droppable: false, 180 | highlightable: false, 181 | components: [ 182 | createOption('opt1', 'Option 1'), 183 | createOption('opt2', 'Option 2'), 184 | ], 185 | traits: [ 186 | nameTrait, 187 | { 188 | name: 'options', 189 | type: 'select-options' 190 | }, 191 | requiredTrait 192 | ], 193 | }, 194 | }, 195 | 196 | view: { 197 | events: { 198 | mousedown: checkIfInPreview, 199 | } as any, 200 | }, 201 | }); 202 | 203 | 204 | 205 | 206 | 207 | // CHECKBOX 208 | Components.addType(typeCheckbox, { 209 | extend: typeInput, 210 | isComponent: (el) => el.tagName == 'INPUT' && (el as HTMLInputElement).type == 'checkbox', 211 | 212 | model: { 213 | defaults: { 214 | copyable: false, 215 | attributes: { type: 'checkbox' }, 216 | traits: [ 217 | idTrait, 218 | nameTrait, 219 | valueTrait, 220 | requiredTrait, 221 | checkedTrait 222 | ], 223 | }, 224 | }, 225 | 226 | view: { 227 | events: { 228 | click: checkIfInPreview, 229 | } as any, 230 | 231 | init() { 232 | this.listenTo(this.model, 'change:attributes:checked', this.handleChecked); 233 | }, 234 | 235 | handleChecked() { 236 | (this.el as any).checked = !!this.model.get('attributes')?.checked; 237 | }, 238 | }, 239 | }); 240 | 241 | 242 | 243 | 244 | 245 | // RADIO 246 | Components.addType(typeRadio, { 247 | extend: typeCheckbox, 248 | isComponent: el => el.tagName == 'INPUT' && (el as HTMLInputElement).type == 'radio', 249 | 250 | model: { 251 | defaults: { 252 | attributes: { type: 'radio' }, 253 | }, 254 | }, 255 | }); 256 | 257 | 258 | 259 | 260 | 261 | Components.addType(typeButton, { 262 | extend: typeInput, 263 | isComponent: el => el.tagName == 'BUTTON', 264 | 265 | model: { 266 | defaults: { 267 | tagName: 'button', 268 | attributes: { type: 'button' }, 269 | text: 'Send', 270 | traits: [ 271 | { 272 | name: 'text', 273 | changeProp: true, 274 | }, { 275 | type: 'select', 276 | name: 'type', 277 | options: [ 278 | { value: 'button' }, 279 | { value: 'submit' }, 280 | { value: 'reset' }, 281 | ] 282 | }] 283 | }, 284 | 285 | init() { 286 | const comps = this.components(); 287 | const tChild = comps.length === 1 && comps.models[0]; 288 | const chCnt = (tChild && tChild.is('textnode') && tChild.get('content')) || ''; 289 | const text = chCnt || this.get('text'); 290 | this.set('text', text); 291 | this.on('change:text', this.__onTextChange); 292 | (text !== chCnt) && this.__onTextChange(); 293 | }, 294 | 295 | __onTextChange() { 296 | this.components(this.get('text')); 297 | }, 298 | }, 299 | 300 | view: { 301 | events: { 302 | click: checkIfInPreview, 303 | } as any, 304 | }, 305 | }); 306 | 307 | 308 | 309 | 310 | 311 | // LABEL 312 | Components.addType(typeLabel, { 313 | extend: 'text', 314 | isComponent: el => el.tagName == 'LABEL', 315 | 316 | model: { 317 | defaults: { 318 | tagName: 'label', 319 | components: 'Label' as any, 320 | traits: [forTrait], 321 | }, 322 | }, 323 | }); 324 | } 325 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { BlockProperties, Plugin } from 'grapesjs'; 2 | import loadBlocks from './blocks'; 3 | import loadComponents from './components'; 4 | import loadTraits from './traits'; 5 | 6 | export type PluginOptions = { 7 | /** 8 | * Which blocks to add. 9 | * @default ['form', 'input', 'textarea', 'select', 'button', 'label', 'checkbox', 'radio'] 10 | */ 11 | blocks?: string[]; 12 | 13 | /** 14 | * Category name for blocks. 15 | * @default 'Forms' 16 | */ 17 | category?: BlockProperties["category"]; 18 | 19 | /** 20 | * Add custom block options, based on block id. 21 | * @default (blockId) => ({}) 22 | * @example (blockId) => blockId === 'input' ? { attributes: {...} } : {}; 23 | */ 24 | block?: (blockId: string) => ({}); 25 | }; 26 | 27 | const plugin: Plugin = (editor, opts = {}) => { 28 | const config: Required = { 29 | blocks: ['form', 'input', 'textarea', 'select', 'button', 'label', 'checkbox', 'radio'], 30 | category: { id: 'forms', label: 'Forms' }, 31 | block: () => ({}), 32 | ...opts 33 | }; 34 | 35 | loadComponents(editor); 36 | loadTraits(editor); 37 | loadBlocks(editor, config); 38 | }; 39 | 40 | export default plugin; 41 | -------------------------------------------------------------------------------- /src/traits.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from 'grapesjs'; 2 | import { typeOption } from './components'; 3 | 4 | export default function (editor: Editor) { 5 | const trm = editor.TraitManager; 6 | 7 | trm.addType('select-options', { 8 | events:{ 9 | keyup: 'onChange', 10 | }, 11 | 12 | onValueChange() { 13 | const { model, target } = this; 14 | const optionsStr = model.get('value').trim(); 15 | const options = optionsStr.split('\n'); 16 | const optComps = []; 17 | 18 | for (let i = 0; i < options.length; i++) { 19 | const optionStr = options[i]; 20 | const option = optionStr.split('::'); 21 | optComps.push({ 22 | type: typeOption, 23 | components: option[1] || option[0], 24 | attributes: { value: option[0] }, 25 | }); 26 | } 27 | 28 | target.components().reset(optComps); 29 | target.view?.render(); 30 | }, 31 | 32 | getInputEl() { 33 | if (!this.$input) { 34 | const optionsArr = []; 35 | const options = this.target.components(); 36 | 37 | for (let i = 0; i < options.length; i++) { 38 | const option = options.models[i]; 39 | const optAttr = option.get('attributes'); 40 | const optValue = optAttr?.value || ''; 41 | const optTxtNode = option.components().models[0]; 42 | const optLabel = optTxtNode && optTxtNode.get('content') || ''; 43 | optionsArr.push(`${optValue}::${optLabel}`); 44 | } 45 | 46 | const el = document.createElement('textarea'); 47 | el.value = optionsArr.join("\n"); 48 | this.$input = el as any; 49 | } 50 | 51 | return this.$input; 52 | }, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/grapesjs-cli/dist/template/tsconfig.json", 3 | "include": ["src"] 4 | } --------------------------------------------------------------------------------