Other Data Types

date

HTML

<div class="field date">
<span class="required" id="dob-required">&starf;</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">&starf;</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 options 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 placeholders 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',
};