Skip to content

Commit ea5ce30

Browse files
authored
Forms (#15)
* add new form configuration object * add new localization * display tailored success messages based on the form auth state * return none when the form does not require auth * remove method * yarn build * access new property * add logs * update code * update jsdocs * remove autoMountForms from configuration * 1.8.0
1 parent f79ed2d commit ea5ce30

24 files changed

+350
-34
lines changed

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ Hellotext.initialize('HELLOTEXT_BUSINESS_ID', configurationOptions)
123123

124124
#### Configuration Options
125125

126-
| Property | Description | Type | Default |
127-
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|---------|
128-
| session | A valid Hellotext session which was stored previously. When not set, Hellotext attempts to retrieve the stored value from `document.cookie` when available, otherwise it creates a new session. | String | null |
129-
| autoGenerateSession | Whether the library should automatically generate a session when no session is found in the query or the cookies | Boolean | true |
130-
| autoMountForms | Whether the library should automatically mount forms collected or not | Boolean | true |
126+
| Property | Description | Type | Default |
127+
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|-------------------------------------------|
128+
| session | A valid Hellotext session which was stored previously. When not set, Hellotext attempts to retrieve the stored value from `document.cookie` when available, otherwise it creates a new session. | String | null |
129+
| autoGenerateSession | Whether the library should automatically generate a session when no session is found in the query or the cookies | Boolean | true |
130+
| forms | An object that controls how Hellotext should control the forms on the page. See [Forms](/docs/forms.md) documentation for more information. | Object | { autoMount: true, successMessage: true } |
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Forms } from '../../../src/core/configuration/forms'
2+
3+
describe('Forms', () => {
4+
describe('.autoMount', () => {
5+
it('is true by default', () => {
6+
expect(Forms.autoMount).toEqual(true)
7+
})
8+
9+
it('can be set to false', () => {
10+
Forms.autoMount = false
11+
expect(Forms.autoMount).toEqual(false)
12+
})
13+
})
14+
15+
describe('.successMessage', () => {
16+
it('is true by default', () => {
17+
expect(Forms.successMessage).toEqual(true)
18+
})
19+
20+
it('can be set to false', () => {
21+
Forms.successMessage = false
22+
expect(Forms.successMessage).toEqual(false)
23+
})
24+
25+
it('can be set to a string', () => {
26+
Forms.successMessage = 'Thank you for your submission'
27+
expect(Forms.successMessage).toEqual('Thank you for your submission')
28+
})
29+
})
30+
})

__tests__/core/configuration_test.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Configuration } from '../../src/core'
2+
3+
describe('Configuration', () => {
4+
describe('.autoGenerateSession', () => {
5+
it('is true by default', () => {
6+
expect(Configuration.autoGenerateSession).toEqual(true)
7+
})
8+
9+
it('can be set to false', () => {
10+
Configuration.autoGenerateSession = false
11+
expect(Configuration.autoGenerateSession).toEqual(false)
12+
})
13+
})
14+
15+
describe('.forms', () => {
16+
it('has default values', () => {
17+
expect(Configuration.forms.autoMount).toEqual(true)
18+
expect(Configuration.forms.successMessage).toEqual(true)
19+
});
20+
21+
it('can be modified', () => {
22+
Configuration.assign({ forms: { autoMount: false, successMessage: false } })
23+
24+
expect(Configuration.forms.autoMount).toEqual(false)
25+
expect(Configuration.forms.successMessage).toEqual(false)
26+
})
27+
})
28+
})

__tests__/models/form_collection_test.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ beforeEach(() => {
1010

1111
Hellotext.initialize('M01az53K', {
1212
autoGenerateSession: false,
13-
autoMountForms: false
13+
forms: {
14+
autoMount: false,
15+
}
1416
})
1517
})
1618

__tests__/models/form_test.js

+50
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,53 @@ describe('markAsCompleted', () => {
7979
expect(emit).toHaveBeenCalledWith('form:completed', { id: 1 })
8080
})
8181
})
82+
83+
describe('localeAuthKey', () => {
84+
it('is email when the first step has an email input', () => {
85+
const form = new Form({
86+
steps: [
87+
{
88+
inputs: [{ kind: 'email' }]
89+
}
90+
]
91+
})
92+
93+
expect(form.localeAuthKey).toBe('email')
94+
})
95+
96+
it('is phone when the first step has a phone input', () => {
97+
const form = new Form({
98+
steps: [
99+
{
100+
inputs: [{ kind: 'phone' }]
101+
}
102+
]
103+
})
104+
105+
expect(form.localeAuthKey).toBe('phone')
106+
})
107+
108+
it('is phone_and_email when the first step has both email and phone inputs', () => {
109+
const form = new Form({
110+
steps: [
111+
{
112+
inputs: [{ kind: 'email' }, { kind: 'phone' }]
113+
}
114+
]
115+
})
116+
117+
expect(form.localeAuthKey).toBe('phone_and_email')
118+
})
119+
120+
it('is none when the first step has neither email nor phone inputs', () => {
121+
const form = new Form({
122+
steps: [
123+
{
124+
inputs: [{ kind: 'first_name' }]
125+
}
126+
]
127+
})
128+
129+
expect(form.localeAuthKey).toBe('none')
130+
})
131+
})

dist/hellotext.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/forms.md

+17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ Then let Hellotext handle building the form, collecting, validating and authenti
55

66
For more information on how to create a form from the dashboard, view this [guide](https://help.hellotext.com/forms).
77

8+
### Configuration
9+
10+
Hellotext forms have a default configuration that can be overridden by passing an object when initializing the library. It has the following attributes by default:
11+
12+
- `autoMount`: Automatically mount forms to the DOM when a `form` element with the `data-hello-form` attribute is found. Default is `true`.
13+
- `successMessage`: Display a contextual success message when a form is submitted successfully. Default is `true`.
14+
You can turn this off by setting it to `false`, or provide your custom success message by setting it to a string.
15+
16+
```javascript
17+
Hellotext.initialize('HELLOTEXT_BUSINESS_ID', {
18+
forms: {
19+
autoMount: true,
20+
successMessage: 'Thank you for submitting the form'
21+
}
22+
})
23+
```
24+
825
### Collection Phase
926

1027
Hellotext uses the `MutationObserver` API to listen for changes in the DOM, specifically new form elements being added that have the `data-hello-form` attribute.

lib/controllers/form_controller.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var _stimulus = require("@hotwired/stimulus");
88
var _hellotext = _interopRequireDefault(require("../hellotext"));
99
var _models = require("../models");
1010
var _forms = _interopRequireDefault(require("../api/forms"));
11+
var _core = require("../core");
1112
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
1213
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
1314
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
@@ -92,7 +93,14 @@ var _default = /*#__PURE__*/function (_Controller) {
9293
key: "completed",
9394
value: function completed() {
9495
this.form.markAsCompleted(this.formData);
95-
this.element.remove();
96+
if (!_core.Configuration.forms.shouldShowSuccessMessage) {
97+
return this.element.remove();
98+
}
99+
if (typeof _core.Configuration.forms.successMessage === 'string') {
100+
this.element.innerHTML = _core.Configuration.forms.successMessage;
101+
} else {
102+
this.element.innerHTML = _hellotext.default.business.locale.forms[this.form.localeAuthKey];
103+
}
96104
}
97105

98106
// private

lib/core/configuration.js

+26-4
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,44 @@ Object.defineProperty(exports, "__esModule", {
44
value: true
55
});
66
exports.Configuration = void 0;
7+
var _forms = require("./configuration/forms");
78
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
89
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
910
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
1011
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
1112
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
13+
/**
14+
* @class Configuration
15+
* @classdesc
16+
* Configuration for Hellotext
17+
* @property {Boolean} [autoGenerateSession=true] - whether to auto generate session or not
18+
* @property {String} [session] - session id
19+
* @property {Forms} [forms] - form configuration
20+
*/
1221
var Configuration = /*#__PURE__*/function () {
1322
function Configuration() {
1423
_classCallCheck(this, Configuration);
1524
}
1625
_createClass(Configuration, null, [{
1726
key: "assign",
18-
value: function assign(props) {
27+
value:
28+
/**
29+
*
30+
* @param props
31+
* @param {Boolean} [props.autoGenerateSession=true] - whether to auto generate session or not
32+
* @param {String} [props.session] - session id
33+
* @param {Object} [props.forms] - form configuration
34+
* @returns {Configuration}
35+
*/
36+
function assign(props) {
1937
if (props) {
2038
Object.entries(props).forEach(_ref => {
2139
var [key, value] = _ref;
22-
this[key] = value;
40+
if (key === 'forms') {
41+
this.forms = _forms.Forms.assign(value);
42+
} else {
43+
this[key] = value;
44+
}
2345
});
2446
}
2547
return this;
@@ -35,5 +57,5 @@ var Configuration = /*#__PURE__*/function () {
3557
exports.Configuration = Configuration;
3658
Configuration.apiRoot = 'https://api.hellotext.com/v1';
3759
Configuration.autoGenerateSession = true;
38-
Configuration.autoMountForms = true;
39-
Configuration.session = null;
60+
Configuration.session = null;
61+
Configuration.forms = _forms.Forms;

lib/core/configuration/forms.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use strict";
2+
3+
Object.defineProperty(exports, "__esModule", {
4+
value: true
5+
});
6+
exports.Forms = void 0;
7+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
8+
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
9+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
10+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
11+
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
12+
/**
13+
* @class Forms
14+
* @classdesc
15+
* Configuration for forms
16+
* @property {Boolean} autoMount - whether to auto mount forms
17+
* @property {Boolean|String} successMessage - whether to show success message after form completion or not
18+
*/
19+
var Forms = /*#__PURE__*/function () {
20+
function Forms() {
21+
_classCallCheck(this, Forms);
22+
}
23+
_createClass(Forms, null, [{
24+
key: "assign",
25+
value:
26+
/**
27+
* @param {Object} props
28+
* @param {Boolean} [props.autoMount=true] - whether to auto mount forms
29+
* @param {Boolean|String} [props.successMessage=true] - whether to show success message after form completion or not
30+
*/
31+
function assign(props) {
32+
if (props) {
33+
Object.entries(props).forEach(_ref => {
34+
var [key, value] = _ref;
35+
this[key] = value;
36+
});
37+
}
38+
return this;
39+
}
40+
}, {
41+
key: "shouldShowSuccessMessage",
42+
get: function get() {
43+
return this.successMessage;
44+
}
45+
}]);
46+
return Forms;
47+
}();
48+
exports.Forms = Forms;
49+
Forms.autoMount = true;
50+
Forms.successMessage = true;

lib/hellotext.js

+1-6
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@ function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + n
2525
var _session = /*#__PURE__*/_classPrivateFieldLooseKey("session");
2626
var _query = /*#__PURE__*/_classPrivateFieldLooseKey("query");
2727
var _mintAnonymousSession = /*#__PURE__*/_classPrivateFieldLooseKey("mintAnonymousSession");
28-
/**
29-
* @typedef {Object} Config
30-
* @property {Boolean} autoGenerateSession
31-
* @property {Boolean} autoMountForms
32-
*/
3328
var Hellotext = /*#__PURE__*/function () {
3429
function Hellotext() {
3530
_classCallCheck(this, Hellotext);
@@ -40,7 +35,7 @@ var Hellotext = /*#__PURE__*/function () {
4035
/**
4136
* initialize the module.
4237
* @param business public business id
43-
* @param { Config } config
38+
* @param { Configuration } config
4439
*/
4540
function initialize(business, config) {
4641
_core.Configuration.assign(config);

lib/locales/en.js

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ var _default = {
1111
errors: {
1212
parameter_not_unique: 'This value is taken.',
1313
blank: 'This field is required.'
14+
},
15+
forms: {
16+
phone: 'Click the link sent via SMS to verify your submission.',
17+
email: 'Click the link sent via email to verify your submission.',
18+
phone_and_email: 'Click the links sent via SMS and email to verify your submission.',
19+
none: 'Your submission has been received.'
1420
}
1521
};
1622
exports.default = _default;

lib/locales/es.js

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ var _default = {
1111
errors: {
1212
parameter_not_unique: 'Este valor ya está en uso.',
1313
blank: 'Este campo es obligatorio.'
14+
},
15+
forms: {
16+
phone: 'Haga clic en el enlace enviado por SMS para verificar su envío.',
17+
email: 'Haga clic en el enlace enviado por e-mail para verificar su envío.',
18+
phone_and_email: 'Haga clic en los enlaces enviados por SMS y e-mail para verificar su envío.',
19+
none: 'Su envío ha sido recibido.'
1420
}
1521
};
1622
exports.default = _default;

lib/models/form.js

+14
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,20 @@ var Form = /*#__PURE__*/function () {
130130
get: function get() {
131131
return this.data.id;
132132
}
133+
}, {
134+
key: "localeAuthKey",
135+
get: function get() {
136+
var firstStep = this.data.steps[0];
137+
if (firstStep.inputs.some(input => input.kind === 'email') && firstStep.inputs.some(input => input.kind === 'phone')) {
138+
return 'phone_and_email';
139+
} else if (firstStep.inputs.some(input => input.kind === 'email')) {
140+
return 'email';
141+
} else if (firstStep.inputs.some(input => input.kind === 'phone')) {
142+
return 'phone';
143+
} else {
144+
return 'none';
145+
}
146+
}
133147
}, {
134148
key: "elementAttributes",
135149
get: function get() {

lib/models/form_collection.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ var FormCollection = /*#__PURE__*/function () {
4646
var mutation = mutations.find(mutation => mutation.type === 'childList' && mutation.addedNodes.length > 0);
4747
if (!mutation) return;
4848
var forms = Array.from(document.querySelectorAll('[data-hello-form]'));
49-
if (forms && _core.Configuration.autoMountForms) {
49+
if (forms && _core.Configuration.forms.autoMount) {
5050
this.collect();
5151
}
5252
}
@@ -68,7 +68,7 @@ var FormCollection = /*#__PURE__*/function () {
6868
}
6969
this.fetching = true;
7070
yield Promise.all(promises).then(forms => forms.forEach(this.add)).then(() => _hellotext.default.eventEmitter.dispatch('forms:collected', this)).then(() => this.fetching = false);
71-
if (_core.Configuration.autoMountForms) {
71+
if (_core.Configuration.forms.autoMount) {
7272
this.forms.forEach(form => form.mount());
7373
}
7474
});

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hellotext/hellotext",
3-
"version": "1.7.9",
3+
"version": "1.8.0",
44
"description": "Hellotext JavaScript Client",
55
"source": "src/index.js",
66
"main": "lib/index.js",

0 commit comments

Comments
 (0)