Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forms #15

Merged
merged 12 commits into from
Oct 14, 2024
Merged
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ Hellotext.initialize('HELLOTEXT_BUSINESS_ID', configurationOptions)

#### Configuration Options

| Property | Description | Type | Default |
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|---------|
| 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 |
| autoGenerateSession | Whether the library should automatically generate a session when no session is found in the query or the cookies | Boolean | true |
| autoMountForms | Whether the library should automatically mount forms collected or not | Boolean | true |
| Property | Description | Type | Default |
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|-------------------------------------------|
| 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 |
| autoGenerateSession | Whether the library should automatically generate a session when no session is found in the query or the cookies | Boolean | true |
| 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 changes: 30 additions & 0 deletions __tests__/core/configuration/forms_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Forms } from '../../../src/core/configuration/forms'

describe('Forms', () => {
describe('.autoMount', () => {
it('is true by default', () => {
expect(Forms.autoMount).toEqual(true)
})

it('can be set to false', () => {
Forms.autoMount = false
expect(Forms.autoMount).toEqual(false)
})
})

describe('.successMessage', () => {
it('is true by default', () => {
expect(Forms.successMessage).toEqual(true)
})

it('can be set to false', () => {
Forms.successMessage = false
expect(Forms.successMessage).toEqual(false)
})

it('can be set to a string', () => {
Forms.successMessage = 'Thank you for your submission'
expect(Forms.successMessage).toEqual('Thank you for your submission')
})
})
})
28 changes: 28 additions & 0 deletions __tests__/core/configuration_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Configuration } from '../../src/core'

describe('Configuration', () => {
describe('.autoGenerateSession', () => {
it('is true by default', () => {
expect(Configuration.autoGenerateSession).toEqual(true)
})

it('can be set to false', () => {
Configuration.autoGenerateSession = false
expect(Configuration.autoGenerateSession).toEqual(false)
})
})

describe('.forms', () => {
it('has default values', () => {
expect(Configuration.forms.autoMount).toEqual(true)
expect(Configuration.forms.successMessage).toEqual(true)
});

it('can be modified', () => {
Configuration.assign({ forms: { autoMount: false, successMessage: false } })

expect(Configuration.forms.autoMount).toEqual(false)
expect(Configuration.forms.successMessage).toEqual(false)
})
})
})
4 changes: 3 additions & 1 deletion __tests__/models/form_collection_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ beforeEach(() => {

Hellotext.initialize('M01az53K', {
autoGenerateSession: false,
autoMountForms: false
forms: {
autoMount: false,
}
})
})

Expand Down
50 changes: 50 additions & 0 deletions __tests__/models/form_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,53 @@ describe('markAsCompleted', () => {
expect(emit).toHaveBeenCalledWith('form:completed', { id: 1 })
})
})

describe('localeAuthKey', () => {
it('is email when the first step has an email input', () => {
const form = new Form({
steps: [
{
inputs: [{ kind: 'email' }]
}
]
})

expect(form.localeAuthKey).toBe('email')
})

it('is phone when the first step has a phone input', () => {
const form = new Form({
steps: [
{
inputs: [{ kind: 'phone' }]
}
]
})

expect(form.localeAuthKey).toBe('phone')
})

it('is phone_and_email when the first step has both email and phone inputs', () => {
const form = new Form({
steps: [
{
inputs: [{ kind: 'email' }, { kind: 'phone' }]
}
]
})

expect(form.localeAuthKey).toBe('phone_and_email')
})

it('is none when the first step has neither email nor phone inputs', () => {
const form = new Form({
steps: [
{
inputs: [{ kind: 'first_name' }]
}
]
})

expect(form.localeAuthKey).toBe('none')
})
})
2 changes: 1 addition & 1 deletion dist/hellotext.js

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions docs/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ Then let Hellotext handle building the form, collecting, validating and authenti

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

### Configuration

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:

- `autoMount`: Automatically mount forms to the DOM when a `form` element with the `data-hello-form` attribute is found. Default is `true`.
- `successMessage`: Display a contextual success message when a form is submitted successfully. Default is `true`.
You can turn this off by setting it to `false`, or provide your custom success message by setting it to a string.

```javascript
Hellotext.initialize('HELLOTEXT_BUSINESS_ID', {
forms: {
autoMount: true,
successMessage: 'Thank you for submitting the form'
}
})
```

### Collection Phase

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.
Expand Down
10 changes: 9 additions & 1 deletion lib/controllers/form_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var _stimulus = require("@hotwired/stimulus");
var _hellotext = _interopRequireDefault(require("../hellotext"));
var _models = require("../models");
var _forms = _interopRequireDefault(require("../api/forms"));
var _core = require("../core");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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); } }
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); }); }; }
Expand Down Expand Up @@ -92,7 +93,14 @@ var _default = /*#__PURE__*/function (_Controller) {
key: "completed",
value: function completed() {
this.form.markAsCompleted(this.formData);
this.element.remove();
if (!_core.Configuration.forms.shouldShowSuccessMessage) {
return this.element.remove();
}
if (typeof _core.Configuration.forms.successMessage === 'string') {
this.element.innerHTML = _core.Configuration.forms.successMessage;
} else {
this.element.innerHTML = _hellotext.default.business.locale.forms[this.form.localeAuthKey];
}
}

// private
Expand Down
30 changes: 26 additions & 4 deletions lib/core/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,44 @@ Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Configuration = void 0;
var _forms = require("./configuration/forms");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
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); }
/**
* @class Configuration
* @classdesc
* Configuration for Hellotext
* @property {Boolean} [autoGenerateSession=true] - whether to auto generate session or not
* @property {String} [session] - session id
* @property {Forms} [forms] - form configuration
*/
var Configuration = /*#__PURE__*/function () {
function Configuration() {
_classCallCheck(this, Configuration);
}
_createClass(Configuration, null, [{
key: "assign",
value: function assign(props) {
value:
/**
*
* @param props
* @param {Boolean} [props.autoGenerateSession=true] - whether to auto generate session or not
* @param {String} [props.session] - session id
* @param {Object} [props.forms] - form configuration
* @returns {Configuration}
*/
function assign(props) {
if (props) {
Object.entries(props).forEach(_ref => {
var [key, value] = _ref;
this[key] = value;
if (key === 'forms') {
this.forms = _forms.Forms.assign(value);
} else {
this[key] = value;
}
});
}
return this;
Expand All @@ -35,5 +57,5 @@ var Configuration = /*#__PURE__*/function () {
exports.Configuration = Configuration;
Configuration.apiRoot = 'https://api.hellotext.com/v1';
Configuration.autoGenerateSession = true;
Configuration.autoMountForms = true;
Configuration.session = null;
Configuration.session = null;
Configuration.forms = _forms.Forms;
50 changes: 50 additions & 0 deletions lib/core/configuration/forms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Forms = void 0;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
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); }
/**
* @class Forms
* @classdesc
* Configuration for forms
* @property {Boolean} autoMount - whether to auto mount forms
* @property {Boolean|String} successMessage - whether to show success message after form completion or not
*/
var Forms = /*#__PURE__*/function () {
function Forms() {
_classCallCheck(this, Forms);
}
_createClass(Forms, null, [{
key: "assign",
value:
/**
* @param {Object} props
* @param {Boolean} [props.autoMount=true] - whether to auto mount forms
* @param {Boolean|String} [props.successMessage=true] - whether to show success message after form completion or not
*/
function assign(props) {
if (props) {
Object.entries(props).forEach(_ref => {
var [key, value] = _ref;
this[key] = value;
});
}
return this;
}
}, {
key: "shouldShowSuccessMessage",
get: function get() {
return this.successMessage;
}
}]);
return Forms;
}();
exports.Forms = Forms;
Forms.autoMount = true;
Forms.successMessage = true;
7 changes: 1 addition & 6 deletions lib/hellotext.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + n
var _session = /*#__PURE__*/_classPrivateFieldLooseKey("session");
var _query = /*#__PURE__*/_classPrivateFieldLooseKey("query");
var _mintAnonymousSession = /*#__PURE__*/_classPrivateFieldLooseKey("mintAnonymousSession");
/**
* @typedef {Object} Config
* @property {Boolean} autoGenerateSession
* @property {Boolean} autoMountForms
*/
var Hellotext = /*#__PURE__*/function () {
function Hellotext() {
_classCallCheck(this, Hellotext);
Expand All @@ -40,7 +35,7 @@ var Hellotext = /*#__PURE__*/function () {
/**
* initialize the module.
* @param business public business id
* @param { Config } config
* @param { Configuration } config
*/
function initialize(business, config) {
_core.Configuration.assign(config);
Expand Down
6 changes: 6 additions & 0 deletions lib/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ var _default = {
errors: {
parameter_not_unique: 'This value is taken.',
blank: 'This field is required.'
},
forms: {
phone: 'Click the link sent via SMS to verify your submission.',
email: 'Click the link sent via email to verify your submission.',
phone_and_email: 'Click the links sent via SMS and email to verify your submission.',
none: 'Your submission has been received.'
}
};
exports.default = _default;
6 changes: 6 additions & 0 deletions lib/locales/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ var _default = {
errors: {
parameter_not_unique: 'Este valor ya está en uso.',
blank: 'Este campo es obligatorio.'
},
forms: {
phone: 'Haga clic en el enlace enviado por SMS para verificar su envío.',
email: 'Haga clic en el enlace enviado por e-mail para verificar su envío.',
phone_and_email: 'Haga clic en los enlaces enviados por SMS y e-mail para verificar su envío.',
none: 'Su envío ha sido recibido.'
}
};
exports.default = _default;
14 changes: 14 additions & 0 deletions lib/models/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ var Form = /*#__PURE__*/function () {
get: function get() {
return this.data.id;
}
}, {
key: "localeAuthKey",
get: function get() {
var firstStep = this.data.steps[0];
if (firstStep.inputs.some(input => input.kind === 'email') && firstStep.inputs.some(input => input.kind === 'phone')) {
return 'phone_and_email';
} else if (firstStep.inputs.some(input => input.kind === 'email')) {
return 'email';
} else if (firstStep.inputs.some(input => input.kind === 'phone')) {
return 'phone';
} else {
return 'none';
}
}
}, {
key: "elementAttributes",
get: function get() {
Expand Down
4 changes: 2 additions & 2 deletions lib/models/form_collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var FormCollection = /*#__PURE__*/function () {
var mutation = mutations.find(mutation => mutation.type === 'childList' && mutation.addedNodes.length > 0);
if (!mutation) return;
var forms = Array.from(document.querySelectorAll('[data-hello-form]'));
if (forms && _core.Configuration.autoMountForms) {
if (forms && _core.Configuration.forms.autoMount) {
this.collect();
}
}
Expand All @@ -68,7 +68,7 @@ var FormCollection = /*#__PURE__*/function () {
}
this.fetching = true;
yield Promise.all(promises).then(forms => forms.forEach(this.add)).then(() => _hellotext.default.eventEmitter.dispatch('forms:collected', this)).then(() => this.fetching = false);
if (_core.Configuration.autoMountForms) {
if (_core.Configuration.forms.autoMount) {
this.forms.forEach(form => form.mount());
}
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hellotext/hellotext",
"version": "1.7.9",
"version": "1.8.0",
"description": "Hellotext JavaScript Client",
"source": "src/index.js",
"main": "lib/index.js",
Expand Down
Loading
Loading