Other Data Types
date
HTML
<div class="field date"><span class="required" id="dob-required">★</span><input type="text" id="dob-day" name="dob-day" class="day" placeholder="DD" /><input type="text" id="dob-month" name="dob-month" class="month" placeholder="MM" /><input type="text" id="dob-year" name="dob-year" class="year" placeholder="YYYY" /><span class="status" id="dob-status"></span><span class="issue" id="dob-issue"></span><input type="submit" hidden /></div>
Constructor
class DateInput extends StringInput {constructor(id, remoteState) {super(id, remoteState);this.dayInputEl = document.getElementById(id + '-day');this.monthInputEl = document.getElementById(id + '-month');this.yearInputEl = document.getElementById(id + '-year');this.inputEls = [this.dayInputEl, this.monthInputEl, this.yearInputEl];}}
The main part to note here is the updating of the this.inputEls
defined in the
StringInput
constructor. This abstracts over the input fields used by
DateInput
, which allows the methods defined in StringInput
to handle all
input fields uniformly.
Overrides
Here we override reset
and validatedValue
to handle the multiple input
fields used by DateInput
:
class DateInput extends StringInput {// ...reset() {this.dayInputEl.value = this.remoteState.value.day;this.monthInputEl.value = this.remoteState.value.month;this.yearInputEl.value = this.remoteState.value.year;}validatedValue() {const remoteValue = this.remoteState.value;const value = this.value();if (objectsEqual(remoteValue, value)) {return {isUnchanged: true};}const {day, month, year} = value;if (day === 0 && month === 0 && year === 0) {return {value};}if (day === 0 || month === 0 || year === 0) {return {isUnchanged: true};}const validation = this.remoteState.validation;if (validation.max_day && day > validation.max_day) {return {error: `day can't be after ${validation.max_day}`};} else if (validation.min_day && day < validation.min_day) {return {error: `day can't be before ${validation.min_day}`};} else if (validation.max_month && month > validation.max_month) {return {error: `month can't be after ${validation.max_month}`};} else if (validation.min_month && month < validation.min_month) {return {error: `month can't be before ${validation.min_month}`};} else if (validation.min_year && year < validation.min_year) {return {error: `year can't be before ${validation.min_year}`};} else if (validation.max_year && year > validation.max_year) {return {error: `year can't be after ${validation.max_year}`};}return {value};}value() {return this.newDate(this.dayInputEl.value,this.monthInputEl.value,this.yearInputEl.value,);}newDate(day, month, year) {return {day: parseIntOrZero(day),month: parseIntOrZero(month),year: parseIntOrZero(year),};}function objectsEqual(a, b) {for (const [k, v] of Object.entries(a)) {if (v !== b[k]) {return false;}}for (const [k, v] of Object.entries(b)) {if (v !== a[k]) {return false;}}return true;}
phone
HTML
<div class="field phone"><span class="required" id="phone-required">★</span><select id="phone-dial-code" name="phone-dial-code"><option value="default">Select Country</option><option value="us">USA (+1)</option><option value="ca">Canada (+1)</option><option value="pr1">Puerto Rico (+1 787)</option><option value="pr2">Puerto Rico (+1 939)</option></select><input type="text" id="phone-number" name="phone-number" /><span class="status" id="phone-status"></span><span class="issue" id="phone-issue"></span><input type="submit" hidden /></div>
The Trustap API takes a value of the following form for the phone
field:
{"dial_code": string,"dial_code_country": string,"number": string,}
The HTML we present above uses a sample of countries to highlight the use of the
dial_code_country
property.
Constructor
class PhoneInput extends StringInput {constructor(id, remoteState) {super(id, remoteState);this.dialCodeInputEl = document.getElementById(id + '-dial-code');this.numberInputEl = document.getElementById(id + '-number');this.inputEls = [this.dialCodeInputEl, this.numberInputEl];}}
Overrides
reset
class PhoneInput extends StringInput {// ...reset() {let dcc = this.remoteState.value.dial_code_country;if (dialCodeCountries[dcc]) {dcc = dialCodeCountries[dcc][this.remoteState.value.dial_code];}this.dialCodeInputEl.value = dcc;this.numberInputEl.value = this.remoteState.value.number;}}const dialCodeCountries = {pr: {"1787": "pr1","1939": "pr2",},};
When resetting the value of this component we use the remote value of
dial_code_country
to calculate the combination of dial code and country to be
shown to the user. In the case where a single country supports multiple dial
codes (as in the case of Puerto Rico, shown above), the dial_code
field is
also used in rendering the component.
validatedValue
class PhoneInput extends StringInput {// ...validatedValue() {const remoteValue = this.remoteState.value;const value = this.value();if (objectsEqual(remoteValue, value)) {return {isUnchanged: true};}let {dial_code: dialCode, number} = value;if (number === '') {if (remoteValue.number === '') {return {isUnchanged: true};}} else if (!dialCode) {return {error: `dial code must be provided`};}return {value};}value() {return {...dialCodes[this.dialCodeInputEl.value],number: this.numberInputEl.value,};}}const dialCodes = {'': { dial_code_country: "", dial_code: "" },us: { dial_code_country: "us", dial_code: "1" },ca: { dial_code_country: "ca", dial_code: "1" },pr1: { dial_code_country: "pr", dial_code: "1787" },pr2: { dial_code_country: "pr", dial_code: "1939" },};
We map the option
s of our select
input to distinct
dial_code_country
/dial_code
combinations.
id_number
HTML
<div class="field string"><!-- ... --><label id="ssn-label" for="ssn">SSN</label><!-- ... --></div>
An IdNumberInput
functions very similarly to a StringInput
, and the HTML
that we use for both kinds of component is almost the same. However, we also add
an id
to the label
element, which we will update if the input field allows a
different type of input.
Constructor
class IdNumberInput extends StringInput {constructor(id, remoteState) {super(id, remoteState);this.labelEl = document.getElementById(id + "-label");}}
Here we get a reference to the component label defined in the previous section.
reset
class IdNumberInput extends StringInput {// ...reset() {this.inputEl.value = "";if (this.remoteState.value.full_provided) {this.inputEl.placeholder = "********";} else if (this.remoteState.value.last_4_provided) {this.inputEl.placeholder = "----****";}if (this.requireFull()) {this.labelEl.innerText = "SSN (full)";} else {this.labelEl.innerText = "SSN (last 4)";}}requireFull() {return this.remoteState.value.full_provided ||!this.remoteState.allow_last_4;}}
In the Trustap API, ID numbers are "write-only" fields - after they're submitted, the value won't be returned to the user. Instead, the API will only indicate whether the field is set or not.
To highlight this, this guide makes use of placeholder
s to highlight whether
an ID number has been set or not. The user won't be able to see the previous
value, and any new input will entirely overwrite the previously submitted value.
Finally, in some cases the user isn't required to provide a full ID number, just
the last 4 characters of the ID number. If this is allowed (based on the
allow_last_4
property) then we update the component's label to highlight this.
validatedValue
validatedValue() {let value = {last_4: this.inputEl.value};if (this.requireFull()) {value = {full: this.inputEl.value};}const isEmpty = f => f === undefined || f.length === 0;if (isEmpty(value.last_4) && isEmpty(value.full)) {return {isUnchanged: true};}const validation = this.remoteState.validation;if (value.last4) {const last4 = value.last4;if (validation.last_4_min_length && last4.length < validation.last_4_min_length) {return {error: `must be at least ${validation.last_4_min_length} characters`};} else if (validation.last_4_max_length && last4.length > validation.last_4_max_length) {return {error: `can't be more than ${validation.last_4_max_length} characters`};}}if (value.full) {const full = value.full;if (validation.full_min_length && full.length < validation.full_min_length) {return {error: `must be at least ${validation.full_min_length} characters`};} else if (validation.full_max_length && full.length > validation.full_max_length) {return {error: `can't be more than ${validation.full_max_length} characters`};}}return {value};}
Usage
With the new components in place we first add a new factory function to select
what constructor to use for a given field based on the type
of that field:
function newField(id, remoteValue) {if (remoteValue.type === 'string') {return new StringInput(id, remoteValue);} else if (remoteValue.type === 'date') {return new DateInput(id, remoteValue);} else if (remoteValue.type === 'id_number') {return new IdNumberInput(id, remoteValue);} else if (remoteValue.type === 'phone') {return new PhoneInput(id, remoteValue);}}
Given this new function, we can now update main
to use our factory function,
and add references to fields utilising the new types:
const fields = {};async function main() {// ...const payoutDetails = await resp.json();for (const [id, key] of Object.entries(payoutDetailFieldIds)) {- const field = new StringInput(id, payoutDetails[key]);+ const field = newField(id, payoutDetails[key]);field.setup();fields[key] = field;}// ...}const payoutDetailFieldIds = {// ...'address-state': 'address_state',+ 'dob': 'dob',+ 'phone': 'phone',+ 'ssn': 'id_number',};