├── .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 |
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 | }
--------------------------------------------------------------------------------