Skip to content

Commit a88a34f

Browse files
authored
Automatic form mounting (#9)
* automatic form mounting * await promise resolution * WIP * WIP * WIP * update code * update code * WIP * update code * WIP * WIP * log form ids to fetch * WIP * update code. CI SKIP * WIP * update code * ensure to only loaded non-loaded forms * prettier * disable automatic form mounting in tests * update jsdoc declarations * ignore lib from prettier * 1.6.0 * update forms readme * only run code when props is present * 1.6.1
1 parent 4d86959 commit a88a34f

19 files changed

+140
-87
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
__tests__
22
dist
3+
lib

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
# Hellotext.js
22

3-
Official [Hellotext](https://www.hellotext.com) (client-side) JavaScript library.
3+
Official [Hellotext](https://www.hellotext.com) (client-side) JavaScript library.
44

55
This library allows you the following,
66

77
- Track events happening on your site to [Hellotext](https://www.hellotext.com) in real-time.
88
- Use Hellotext Forms to dynamically collect data from your customers based on your specific business requirements.
99

10-
1110
## Installation
1211

1312
### Using NPM
@@ -120,3 +119,4 @@ Hellotext.initialize('HELLOTEXT_BUSINESS_ID', configurationOptions)
120119
| Property | Description | Type | Default |
121120
| ------------------- | ---------------------------------------------------------------------------------------------------------------- | ------- | ------- |
122121
| autogenerateSession | Whether the library should automatically generate a session when no session is found in the query or the cookies | Boolean | true |
122+
| autoMountForms | Whether the library should automatically mount forms collected or not | Boolean | true |

__tests__/models/form_collection_test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ beforeEach(() => {
99
Business.prototype.fetchPublicData = jest.fn().mockResolvedValue({ whitelist: 'disabled' })
1010

1111
Hellotext.initialize('M01az53K', {
12-
autogenerateSession: false
12+
autogenerateSession: false,
13+
autoMountForms: false
1314
})
1415
})
1516

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

+33-30
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
## Forms
22

3-
Create dynamic forms based on built-in subscriber attributes such as name, email, phone or other custom properties unique to your business.
4-
Then let Hellotext handle building the form, collecting, validating and authenticating the data users submit.
3+
Create dynamic forms based on built-in subscriber attributes such as name, email, phone or other custom properties unique to your business.
4+
Then let Hellotext handle building the form, collecting, validating and authenticating the data users submit.
55

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

88
### Collection Phase
99

10-
After initializing the `Hellotext` class, it attaches a `load` event listener and once the page is loaded,
11-
it looks for any HTML element that is on the page that has a `data-hello-form` attribute.
10+
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.
1211

13-
You can access the forms object to also trigger the form collection phase manually.
14-
This is useful if you have a Single Page Application(SPA) and cannot hardcode the `data-hello-form` element on the rendered page.
12+
You can access the forms object to also trigger the form collection phase manually.
13+
This is useful if you have a Single Page Application(SPA) and cannot hardcode the `data-hello-form` element on the rendered page.
1514

16-
To manually collect forms, do the following.
15+
To manually collect forms, do the following.
1716

1817
```javascript
1918
Hellotext.initialize('HELLOTEXT_BUSINESS_ID')
@@ -22,18 +21,18 @@ Hellotext.forms.collect()
2221

2322
Once loaded, you can access the `FormCollection` object by calling `Hellotext.forms`.
2423

25-
Make sure you have initialized with `Hellotext.initialize` otherwise an error is reported.
24+
Make sure you have initialized with `Hellotext.initialize` otherwise an error is reported.
2625

2726
Form collection finishes once Hellotext has fetched the data for the form elements present on the page from the Hellotext API.
28-
Afterwards, it dispatches a `forms:collected` event that you can subscribe to.
27+
Afterwards, it dispatches a `forms:collected` event that you can subscribe to.
2928

3029
```javascript
31-
Hellotext.on('forms:collected', (forms) => {
30+
Hellotext.on('forms:collected', forms => {
3231
console.log(forms) // Instance of FormCollection
3332
})
3433
```
3534

36-
The `FormCollection` class is a wrapper around the forms, which providers other useful methods.
35+
The `FormCollection` class is a wrapper around the forms, which providers other useful methods.
3736

3837
- `getById(id: string): Form` - Get a form by it's id
3938
- `getByIndex(index: number): Form` - Get a form by it's index
@@ -44,38 +43,44 @@ The `FormCollection` class is a wrapper around the forms, which providers other
4443

4544
### Mounting forms
4645

47-
After the collection phase, form elements would be available to be mounted. Hellotext does not automatically mount form elements,
48-
you have total control on when and where to mount the form elements. To mount a form object, you call the `mount` method on the form object.
46+
Hellotext.js by default automatically mounts forms collected to the DOM. You can disable this behaviour by passing the `autoMountForms` option as `false` when initializing the library.
4947

5048
```javascript
51-
Hellotext.on('forms:collected', (forms) => {
49+
Hellotext.initialize('HELLOTEXT_BUSINESS_ID', { autoMountForms: false })
50+
```
51+
52+
If form mounting is disabled, Hellotext does not automatically mount form elements,
53+
you will have total control on when and where to mount the form elements. To mount a form object, you call the `mount` method on the form object.
54+
55+
```javascript
56+
Hellotext.on('forms:collected', forms => {
5257
forms.getByIndex(0).mount()
5358
})
5459
```
5560

56-
Mounting a form creates the form and it's components that are associated to it, and attaches it to the DOM.
57-
Hellotext looks for a `form` element with the `data-hello-form` attribute and mounts the form inside it.
61+
Mounting a form creates the form and it's components that are associated to it, and attaches it to the DOM.
62+
Hellotext looks for a `form` element with the `data-hello-form` attribute and mounts the form inside it.
5863
If this condition is not met, Hellotext creates the form manually and appends it to the body of the document.
5964
We recommend to make the criteria met to ensure the form is loaded into an expected place in your page.
6065

6166
### Validation
6267

63-
Hellotext automatically validates the form inputs based on how they were configured on the dashboard
64-
using browser's native [checkValidity()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/checkValidity).
65-
Once the user tries to submit the form and there are missing required fields,
68+
Hellotext automatically validates the form inputs based on how they were configured on the dashboard
69+
using browser's native [checkValidity()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/checkValidity).
70+
Once the user tries to submit the form and there are missing required fields,
6671
the submission is halted and we display default browser's error message using [HTMLObjectElement.validationMessage](https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/validationMessage) property.
6772

6873
### Authentication
6974

70-
Hellotext protects you from bot submissions and protects your customers from identity theft and impersonation.
75+
Hellotext protects you from bot submissions and protects your customers from identity theft and impersonation.
7176
When a subscriber fills the form, Hellotext sends a One Time Password (OTP) code to the subscriber. If they have an email,
72-
an email is sent to them, otherwise an SMS is sent to their phone number.
77+
an email is sent to them, otherwise an SMS is sent to their phone number.
7378
Until a valid OTP has been entered, data will not show up on the dashboard and will not contribute to attribution marketing.
7479

7580
### Form Completion
7681

77-
Once the user enters the OTP they received. The form is considered to be complete and will be sent to the Hellotext API to create(or update) a profile from the submission information.
78-
The library also dispatches a `form:completed` event that you can subscribe to. In addition, a Session object is set and stored on the browser's cookies.
82+
Once the user enters the OTP they received. The form is considered to be complete and will be sent to the Hellotext API to create(or update) a profile from the submission information.
83+
The library also dispatches a `form:completed` event that you can subscribe to. In addition, a Session object is set and stored on the browser's cookies.
7984
Additionally, the `Hellotext.session` is also set if no session was present already, you can listen for the session events by subscribing to `session-set` event.
8085

8186
```javascript
@@ -91,28 +96,26 @@ Hellotext.on('form:completed', (form) => {
9196
phone: "+1234567890",
9297
property_by_id[xxxxx]: "value"
9398
}
94-
```
99+
```
95100

96101
The data in the from will differ based on the inputs you have configured on the dashboard.
97102

98103
### Understanding form's layout
99104

100-
Hellotext assumes a fixed layout for forms, which are in order of Header, Inputs, Button and Notice.
105+
Hellotext assumes a fixed layout for forms, which are in order of Header, Inputs, Button and Notice.
101106

102107
But you can override this layout if you want. Overriding a form's layout can be achieved
103108
by moving the placement of the form's components. For example, if you want to display the Button component after the Footer, here's how you can do that
104109

105110
```html
106111
<form data-hello-form=":id">
107-
<footer data-form-notice>
108-
</footer>
112+
<footer data-form-notice></footer>
109113

110-
<button data-form-button>
111-
</button>
114+
<button data-form-button></button>
112115
</form>
113116
```
114117

115-
Hellotext would simply load the contents inside the respective elements without creating the default layout.
118+
Hellotext would simply load the contents inside the respective elements without creating the default layout.
116119
If these elements were not defined, Hellotext would render the button then the notice component.
117120

118121
### Customizing the Form's styles

lib/core/configuration.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ var Configuration = /*#__PURE__*/function () {
1515
}
1616
_createClass(Configuration, null, [{
1717
key: "assign",
18-
value: function assign(_ref) {
19-
var {
20-
apiRoot,
21-
autoGenerateSession
22-
} = _ref;
23-
this.apiRoot = apiRoot || this.apiRoot;
24-
this.autoGenerateSession = autoGenerateSession;
18+
value: function assign(props) {
19+
if (props) {
20+
Object.entries(props).forEach(_ref => {
21+
var [key, value] = _ref;
22+
this[key] = value;
23+
});
24+
}
2525
return this;
2626
}
2727
}, {
@@ -34,4 +34,5 @@ var Configuration = /*#__PURE__*/function () {
3434
}();
3535
exports.Configuration = Configuration;
3636
Configuration.apiRoot = 'https://api.hellotext.com/v1';
37-
Configuration.autoGenerateSession = true;
37+
Configuration.autoGenerateSession = true;
38+
Configuration.autoMountForms = true;

lib/hellotext.js

+2-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var _mintAnonymousSession = /*#__PURE__*/_classPrivateFieldLooseKey("mintAnonymo
2929
/**
3030
* @typedef {Object} Config
3131
* @property {Boolean} autogenerateSession
32+
* @property {Boolean} autoMountForms
3233
*/
3334
var Hellotext = /*#__PURE__*/function () {
3435
function Hellotext() {
@@ -42,17 +43,11 @@ var Hellotext = /*#__PURE__*/function () {
4243
* @param business public business id
4344
* @param { Config } config
4445
*/
45-
function initialize(business) {
46-
var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
47-
autogenerateSession: true
48-
};
46+
function initialize(business, config) {
4947
_classPrivateFieldLooseBase(this, _config)[_config] = _core.Configuration.assign(config);
5048
_classPrivateFieldLooseBase(this, _query)[_query] = new _models.Query();
5149
this.business = new _models.Business(business);
5250
this.forms = new _models.FormCollection();
53-
addEventListener('load', () => {
54-
this.forms.collect();
55-
});
5651
if (_classPrivateFieldLooseBase(this, _query)[_query].inPreviewMode) return;
5752
if (_classPrivateFieldLooseBase(this, _query)[_query].session) {
5853
_classPrivateFieldLooseBase(this, _session)[_session] = _models.Cookies.set('hello_session', _classPrivateFieldLooseBase(this, _query)[_query].session);

lib/locales/en.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var _default = {
1414
throttled: 'You have reached the maximum number of attempts. Please try again in 1 minute.'
1515
},
1616
white_label: {
17-
powered_by: "Powered by"
17+
powered_by: 'Powered by'
1818
}
1919
};
2020
exports.default = _default;

lib/locales/es.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var _default = {
1414
throttled: 'Has alcanzado el número máximo de intentos. Por favor intenta de nuevo en 1 minuto.'
1515
},
1616
white_label: {
17-
powered_by: "Desarrollado por"
17+
powered_by: 'Desarrollado por'
1818
}
1919
};
2020
exports.default = _default;

lib/models/form_collection.js

+40-13
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ Object.defineProperty(exports, "__esModule", {
44
value: true
55
});
66
exports.FormCollection = void 0;
7-
var _hellotext = _interopRequireDefault(require("../hellotext.js"));
7+
var _hellotext = _interopRequireDefault(require("../hellotext"));
88
var _forms = _interopRequireDefault(require("../api/forms"));
9+
var _core = require("../core");
910
var _form = require("./form");
1011
var _errors = require("../errors");
1112
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13+
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); } }
14+
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); }); }; }
1215
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
1316
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); } }
1417
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
@@ -29,23 +32,47 @@ var FormCollection = /*#__PURE__*/function () {
2932
this.includes = this.includes.bind(this);
3033
this.excludes = this.excludes.bind(this);
3134
this.add = this.add.bind(this);
35+
this.mutationObserver = new MutationObserver(this.formMutationObserver.bind(this));
36+
this.mutationObserver.observe(document.body, {
37+
childList: true,
38+
subtree: true
39+
});
3240
}
3341
_createClass(FormCollection, [{
34-
key: "collect",
35-
value: function collect() {
36-
if (_hellotext.default.notInitialized) {
37-
throw new _errors.NotInitializedError();
42+
key: "formMutationObserver",
43+
value: function formMutationObserver(mutations) {
44+
var mutation = mutations.find(mutation => mutation.type === 'childList' && mutation.addedNodes.length > 0);
45+
if (!mutation) return;
46+
var forms = Array.from(document.querySelectorAll('[data-hello-form]'));
47+
if (forms && _core.Configuration.autoMountForms) {
48+
this.collect();
3849
}
39-
var formsIdsToFetch = _classPrivateFieldLooseBase(this, _formIdsToFetch)[_formIdsToFetch];
40-
if (formsIdsToFetch.length === 0) return;
41-
var promises = formsIdsToFetch.map(id => {
42-
return _forms.default.get(id).then(response => response.json());
50+
}
51+
}, {
52+
key: "collect",
53+
value: function () {
54+
var _collect = _asyncToGenerator(function* () {
55+
if (_hellotext.default.notInitialized) {
56+
throw new _errors.NotInitializedError();
57+
}
58+
var formsIdsToFetch = _classPrivateFieldLooseBase(this, _formIdsToFetch)[_formIdsToFetch].filter(this.excludes);
59+
if (formsIdsToFetch.length === 0) return;
60+
var promises = formsIdsToFetch.map(id => {
61+
return _forms.default.get(id).then(response => response.json());
62+
});
63+
if (!_hellotext.default.business.enabledWhitelist) {
64+
console.warn('No whitelist has been configured. It is advised to whitelist the domain to avoid bots from submitting forms.');
65+
}
66+
yield Promise.all(promises).then(forms => forms.forEach(this.add)).then(() => _hellotext.default.eventEmitter.dispatch('forms:collected', this));
67+
if (_core.Configuration.autoMountForms) {
68+
this.forms.forEach(form => form.mount());
69+
}
4370
});
44-
if (!_hellotext.default.business.enabledWhitelist) {
45-
console.warn('No whitelist has been configured. It is advised to whitelist the domain to avoid bots from submitting forms.');
71+
function collect() {
72+
return _collect.apply(this, arguments);
4673
}
47-
Promise.all(promises).then(forms => forms.forEach(this.add)).then(() => _hellotext.default.eventEmitter.dispatch('forms:collected', this));
48-
}
74+
return collect;
75+
}()
4976
}, {
5077
key: "forEach",
5178
value: function forEach(callback) {

package.json

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

src/controllers/form_controller.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default class extends Controller {
4343
const response = await FormsAPI.submit(this.form.id, this.formData)
4444
this.buttonTarget.disabled = false
4545

46-
if(response.failed) {
46+
if (response.failed) {
4747
return
4848
}
4949

@@ -52,7 +52,7 @@ export default class extends Controller {
5252

5353
const submission = await response.json()
5454

55-
if(submission.identified) {
55+
if (submission.identified) {
5656
this.completed()
5757
} else {
5858
Hellotext.setSession(submission.session)

src/core/configuration.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
class Configuration {
22
static apiRoot = 'https://api.hellotext.com/v1'
33
static autoGenerateSession = true
4+
static autoMountForms = true
5+
6+
static assign(props) {
7+
if(props) {
8+
Object.entries(props).forEach(([key, value]) => {
9+
this[key] = value
10+
})
11+
}
412

5-
static assign({ apiRoot, autoGenerateSession }) {
6-
this.apiRoot = apiRoot || this.apiRoot
7-
this.autoGenerateSession = autoGenerateSession
813
return this
914
}
1015

src/hellotext.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { NotInitializedError } from './errors'
88
/**
99
* @typedef {Object} Config
1010
* @property {Boolean} autogenerateSession
11+
* @property {Boolean} autoMountForms
1112
*/
1213

1314
class Hellotext {
@@ -24,17 +25,13 @@ class Hellotext {
2425
* @param business public business id
2526
* @param { Config } config
2627
*/
27-
static initialize(business, config = { autogenerateSession: true }) {
28+
static initialize(business, config) {
2829
this.#config = Configuration.assign(config)
2930
this.#query = new Query()
3031

3132
this.business = new Business(business)
3233
this.forms = new FormCollection()
3334

34-
addEventListener('load', () => {
35-
this.forms.collect()
36-
})
37-
3835
if (this.#query.inPreviewMode) return
3936

4037
if (this.#query.session) {

0 commit comments

Comments
 (0)