2 let blog_post = await _require('/_lib/blog_post.jst')
3 let fa_cloud_upload_alt = await env.site.get_min_svg('/_svg/fa_cloud-upload-alt.svg')
4 let fa_server = await env.site.get_min_svg('/blog/20220128/fa_server.svg')
5 let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
6 let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
15 'So after gaining considerable momentum to complete the refactoring project of the last 3–4 weeks, I am happy with things and can take a breather. Whilst the site still has the same basic functionality (read static content, sign up, verify email, password reset, sign in/out, change details and password), things are very different under the hood, and there are also visible UX improvements.'
20 a (href="../20220114/index.html") {
23 ' database has been in a useable state for about 2 weeks and since then I have been changing all the flows to store the data (such as user-account data upon sign up) in the central '
25 ' database for the site, rather than individual JSON files. But since this required rewriting almost all code, I decided the rewrite would encompass the following points:'
28 'Change data storage to use '
30 ' (the original goal of the rewrite).'
33 'Partition the logic into API endpoints and pages.'
36 'Rearrange the code into a sensible directory structure.'
39 'UX improvements, such as custom forms and fewer pop-ups.'
42 'I will discuss each point in further detail next.'
46 'Change data storage to use '
51 'The experience with '
57 'After some weeks of using '
59 ' for the backend storage, the experience has been very good. Compared to the traditional 2-dimensional structure of a relational database (SQL), the tree-like structure of '
61 ' proves to be quite convenient.'
65 'The JSON structure of the database at the moment looks like '
78 ' stores miscellaneous information such as the site’s title and copyright message, '
80 ' stores information about each browser session connected to the site (including sign-in status), '
82 ' stores information about signed-up users, '
84 ' stores the navigation tree of the site (basically a mapping from directory names to page titles, in a hierarchical structure), and '
86 ' stores information about mail accounts and servers which we use for verifying users’ email addresses and similar purposes.'
94 ' JSON Objects are keyed by the user’s session or account identifier respectively, and lead to a JSON Object per session or account. These latter do not have a predefined structure, as any part of the business logic is able to add a new key to the session or account as required. This is useful because normally this kind of addition would require a SQL structure change or a new table, both of which are inconvenient and time-consuming to implement.'
98 'Of course, it is also easy to add new top-level keys to the database for new functionality that doesn’t reside in the session or account. The general approach I have taken is that any code accessing the database and expecting a particular key should add that key if it does not exist. Ultimately we might want to have a schema for the database, and predefined objects with predefined field sets. This would also work well, but would require a bit more effort upfront.'
102 'From a historical perspective, my understanding was that IBM was making object-oriented databases in the 70s and 80s before Oracle popularized the relational paradigm with SQL in the late 80s and 90s. Having taught the relational algebra at undergraduate level, I appreciate there are many good things about it, and I would not want to dismiss it out of hand. But I still think that the idea of object databases deserves reconsideration, hence my experiment here.'
112 'In my initial attempt at the conversion to '
114 ', I was trying to keep the number of transactions to a minimum, particularly considering that all parent objects to a changed object must be rewritten to the log every transaction.'
118 'So each API endpoint would, for example, invoke the code to generate a new session cookie and enter or locate a session object in the database, after having opened the transaction it would use to perform its own operation. This avoided having an extra transaction just to deal with the session cookie, but also made the logic for each API endpoint more complex. A similar issue arose when serving pages and dealing with analytics or sign in/out status for the navbar.'
122 'A particularly annoying thing about this ‘optimization’ was that transactions which were inherently read-only were becoming read-write in general, since they might write a new session even if the rest of the transaction was only reading. This added considerable complexity to the read-only transactions such as ‘get’ API endpoints and the navbar template.'
126 'Therefore, I eventually realized that this was not really an optimization, and so I changed it so that when you hit the server, it will usually run 3 or 4 smaller database transactions throughout the process, e.g. for the session cookie, the analytics, the navbar, and whatever operation is needed by the page itself. Some of these are read-only, which will improve concurrency. A cost is that the log grows slightly faster, as there are multiple write transactions too.'
130 'Potential issues with '
136 'It must be admitted that the '
138 ' object is not scalable as things stand, since for example when I create a management interface for users, I will want to display something like 20 users per page sorted by email, and at present you would have to extract the entire set of keys from the '
140 ' object simultaneously and sort them to make any sense of them.'
144 'Therefore, I plan to create a B-Tree index of email addresses in sorted order, which will be maintained as accounts are added and removed. This B-Tree will be encoded into JSON and stored in the database as well. I will do this before attempting to create the management interface.'
148 'Another scalability issue is that '
150 ' generally writes out an entire JSON Object (plus all its parent objects) when it changes. Although it does not write out the child objects that have not changed, an object with many keys will still place a strain on the system (since all keys and pointers to their corresponding child objects must be written out even if only one key or pointer has changed). I see two possible solutions to this issue:'
157 ' to only write out changed keys.'
159 ' Later on we will need to track the deltas (changed keys) of each transaction precisely in order to improve concurrency (needed in turn if the load on the webserver is high). If we write the transactions to the log in delta-format, then they will be quicker to write. But conversely, they will be slower to read.'
164 'Modify database clients to avoid writing large objects.'
166 ' We could use a hashing approach to implement this. Suppose the user’s account key was '
168 ', we could hash this to find a hex value of say '
170 ', and then we break up the '
172 ' object by storing under the hash first, e.g.'
182 "jane@doe.com": {...},
193 'and this way the top 2 levels of the '
195 ' table would have at most 256 entries each (easy to write out each time they change) and the next level would have about 1/65536 the number of entries as the original table. This would scale to at least hundreds of thousands of accounts. An adaptive scheme might also be considered.'
199 'A cheap way to achieve the second goal might be simply to store the data in B-Trees (implemented via JSON Arrays of some fixed maximum size corresponding to the B-Tree block size) rather than JSON Objects directly. It would give the advantage of being able to extract data in order as well as by key. Even if we didn’t use this latter feature, the nature of the B-Tree itself provides an adaptive way of keeping the index in manageably sized chunks.'
203 'Final comments on the '
209 'I also fixed several minor bugs in '
211 ' over the last few weeks and it appears to be stable at this point. I must admit I have not constructed a large-scale programmatic test (add and remove thousands of items at different hierarchical levels, perhaps concurrently, and test that everything is stored correctly throughout the process) but I will do so eventually. I’ll also make a similar test to simulate heavy load on the website and exercise the database in the process.'
215 'Overall, I’m satisfied with the progress of the '
217 ' experiment, though I haven’t implemented the planned B-Tree layer yet. (I have been doing some research into B-Trees in preparation and have found some MIT-licensed sample code, although it’s not perfect for my requirement).'
221 'Partition the logic into API endpoints and pages'
225 'Under the new scheme, API endpoints use POST requests and return machine-readable responses, whereas pages use GET requests and return human-readable markup. This helps to separate business logic from presentation logic. Whilst I’m not generally a huge fan of model/view/controller type schemes, I must admit that my previous code was a bit messy when both logics were intermixed, and it was hard to see what was happening when you returned to the code later on.'
229 'In the previous way, with the business logic encoded into the particular pages it was used on, the page would generally check for a POST request and do something different in such case, for instance it might save something in the database and return a thank-you page.'
233 'I did have a few machine-readable API endpoints for special cases such as the sign-in, and some interesting API endpoints uch as the sign-out which would return status text to be displayed in the page. But as there was no clear separation between API endpoints and pages, these endpoints simply appeared as pages that the user would not normally read directly.'
237 'The new API-based way is much better from the UX perspective too, as we do not have to return a new page to the user each time they interact with the site, and given that APIs return machine-readable responses it is easy to indicate the progress of an operation with ticks, crosses and the like. And we can make the API more granular and present a running status to the user during a sequence of several API calls, which is easier to implement and better for the UX.'
241 'Interestingly, I can still use some text-based status messages (where the API endpoint returns text to be embedded in the page, like it used to) because I have standardized all API errors to return an '
242 tt {'application/problem+json'}
243 ' object mostly following IETF standards, and this contains a '
245 ' field where I can describe an error message for the user. However, in the success case, any message for the user is generated by client-side code rather than being returned from the API as in the old way.'
249 'The new API endpoints are very easy to use, since I have the utility class '
251 ' and utility function '
253 ' which take care of marshalling the call across the client/server boundary. At the server side, each endpoint such as '
254 tt {'/api/account/sign_in.json'}
255 ' is held in a corresponding source file such as '
256 tt {'/api/account/sign_in.json.jst'}
257 ' and the marshalling is done by a subroutine '
258 tt {'post_request()'}
260 tt {'/_lib/post_request.jst'}
261 ', which also avoids a lot of duplication compared to before. To create a new endpoint, you simply copy a similar endpoint source file to the new endpoint name.'
265 'Rearrange the code into a sensible directory structure'
269 'Basically under the new system, things are placed in different directories from the root according to their function, similar to how in a '
277 ' and so on, and a package will divide up its files between these directories. Whilst there is some argument about this (would it not be better to have each package keep all its files in the same place?), there are also advantages to separating things out by function.'
281 'So under the new scheme, API endpoints are under '
283 ', utility code and templates are under '
285 ', and pages are under the original directory structure reflecting the navigation tree of the site.'
289 'In the previous way, it mainly followed the navigation tree of the site, which was not very flexible, since the navigation tree does not necessarily reflect the structure we need for the logic, and also was quite messy as API, utility and template code occurred at various levels of the hierarchy among the code that generated the pages.'
293 'The previous theory was that if for example I wanted to add a blog functionality to some other website, I’d just copy the '
295 ' directory and all business logic and templates would automatically be installed as well. But I have decided that mixing everything together was too impractical, so I might have to write an installation procedure at some point if I want a component system for sites. For the time being I’ll just install things in the needed places manually, which is not really much of a hassle.'
299 'UX improvements, such as custom forms and fewer pop-ups'
303 'Old style form filling'
307 'In the old way we were simply using plain old HTML forms with a ‘Submit’ button, and the browser would encode them into something like JSON (I am not entirely clear how this encoding works) and submit them with a POST request.'
311 'I created a little in-browser simulation to demonstrate how this the old style of form worked. Please have a play with it, and then we will demonstrate some UX improvements. Note that for example purposes, you can trigger a server-side error by entering the name ‘Jane Doe’.'
317 div.card-body(style="height: 400px;") {
319 a#old-refresh(href='#') {'Refresh'}
324 'Please enter your details to receive a greeting message!'
331 label.form-label(for="old-given-names") {'Given names *'}
332 input.form-control#old-given-names(type="text" placeholder="Your given names" required maxlength=256) {}
337 label.form-label(for="old-family-name") {'Family name *'}
338 input.form-control#old-family-name(type="text" placeholder="Your family name" required maxlength=256) {}
343 button.btn.btn-success#old-submit(type="button") {'Submit'}
344 p.text-muted.'mt-3'.mb-0 {'* This field is required.'}
347 p.mb-0#old-message(hidden) {}
351 div.'col-md-4'.align-self-center {
353 svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512") {
354 circle#old-pip0(cx="64" cy="256" r="16") {}
355 circle#old-pip1(cx="128" cy="256" r="16") {}
356 circle#old-pip2(cx="192" cy="256" r="16") {}
357 circle#old-pip3(cx="256" cy="256" r="16") {}
358 circle#old-pip4(cx="320" cy="256" r="16") {}
359 circle#old-pip5(cx="384" cy="256" r="16") {}
360 circle#old-pip6(cx="448" cy="256" r="16") {}
363 div.icon.'ml-3'#old-server {
366 pre.wrap#old-details(style="height: 250px;") {
374 'In the above demonstration form, we had followed one good UX practice:'
376 div.alert.alert-success {
377 div.icon24-outer.mr-2 {
378 div.icon24-inner {_out.push(icon_tick)}
380 'Tell the user why we need this information and what we will do with it.'
384 'However, we had also broken another recommended UX practice:'
386 div.alert.alert-danger {
387 div.icon24-outer.mr-2 {
388 div.icon24-inner {_out.push(icon_cross)}
390 'Don’t repeat the field name in the placeholder text.'
392 div.alert.alert-success {
393 div.icon24-outer.mr-2 {
394 div.icon24-inner {_out.push(icon_tick)}
396 'Use a real person’s name or details for example purposes, as this will speed data entry.'
400 'We are using the browser’s built-in validation style above, which isn’t consistent across browsers. On Chromium, if you do not fill in a required field, a tooltip appears saying ‘Please fill in this field’. I don’t think this text can be changed, and more annoyingly, only the first error is indicted. Whilst this doesn’t specifically violate any UX guidelines, we can do better:'
402 div.alert.alert-success {
403 div.icon24-outer.mr-2 {
404 div.icon24-inner {_out.push(icon_tick)}
406 'Use a custom validation style to highlight errors with colour and provide useful hints for the expected input.'
410 'Another UX issue that is demonstrated in the above example is:'
412 div.alert.alert-danger {
413 div.icon24-outer.mr-2 {
414 div.icon24-inner {_out.push(icon_cross)}
416 'Don’t throw away the user’s partial input if they have to refresh the page!'
418 div.alert.alert-success {
419 div.icon24-outer.mr-2 {
420 div.icon24-inner {_out.push(icon_tick)}
422 'Save a draft to the server during data entry, and pre-populate the form on refresh.'
426 'Form filling was one of the killer applications for the early web, and the old way of doing things worked well for its time. Whilst the traditional way is often like it is for a reason, and I do not agree with change for the sake of change, there are definitely improvements to be made in this case. We will look at the improved form UX next.'
430 'New style form filling'
434 'In the new way we use an API endpoint, or several, to receive the user’s input. This allows us to stay on the same page and provide interactive feedback to the user, e.g. if something goes wrong. We can also implement advanced features, such as the saving of drafts, etc.'
438 'Here is an in-browser simulation to demonstrate the improved form-filling UX. Please have a play with it, and recall that for example purposes, you can trigger a server-side error by entering the name ‘Jane Doe’.'
444 div.card-body(style="height: 450px;") {
446 a#new-refresh(href='#') {'Refresh'}
451 'Please enter your details to receive a greeting message!'
458 label.form-label(for="new-given-names") {'Given names *'}
459 input.form-control#new-given-names(type="text" placeholder="Jane" required maxlength=256) {}
460 div.invalid-feedback {'Please enter a name we can address you by.'}
465 label.form-label(for="new-family-name") {'Family name *'}
466 input.form-control#new-family-name(type="text" placeholder="Doe" required maxlength=256) {}
467 div.invalid-feedback {'Please enter something. You can enter ‘X’ if you do not have a family name.'}
472 button.btn.btn-success#new-submit(type="button" disabled) {
473 div.icon24-outer.mr-2#new-icon {
474 div.icon24-inner {_out.push(fa_cloud_upload_alt)}
476 div.icon24-outer.mr-2#new-tick(hidden) {
477 div.icon24-inner {_out.push(icon_tick)}
479 div.icon24-outer.mr-2#new-cross(hidden) {
480 div.icon24-inner {_out.push(icon_cross)}
482 div.icon24-outer.mr-2#new-spinner(hidden) {
484 div.spinner-border.spinner-border-sm(role="status") {}
489 p.text-muted.'mt-3'.mb-0 {'* This field is required.'}
492 div.alert.mb-0#new-message(hidden) {}
496 div.'col-md-4'.align-self-center {
498 svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512") {
499 circle#new-pip0(cx="64" cy="256" r="16") {}
500 circle#new-pip1(cx="128" cy="256" r="16") {}
501 circle#new-pip2(cx="192" cy="256" r="16") {}
502 circle#new-pip3(cx="256" cy="256" r="16") {}
503 circle#new-pip4(cx="320" cy="256" r="16") {}
504 circle#new-pip5(cx="384" cy="256" r="16") {}
505 circle#new-pip6(cx="448" cy="256" r="16") {}
508 div.icon.'ml-3'#new-server {
511 pre.wrap#new-details(style="height: 250px;") {
512 `draft_details = undefined
520 'In this simulation we are able to see all communication with the server, including the saving of the draft which is normally silent from the user’s viewpoint. The draft is saved every 3 seconds (plus the round-trip time) whilst editing is taking place. The simulation correctly shows also that you can trick the system by refreshing the page very quickly after an edit.'
524 'Note that the draft can be saved '
526 ' the form is submitted, which is probably redundant but harmless. It might be good to clear the form once submitted, and the corresponding draft, but I have not formed a clear policy on this. For the time being, I’m relying on stale drafts expiring at the server after a timeout of 1 day.'
530 'Some other good UX practices that we are adopting in the improved form are:'
532 div.alert.alert-success {
533 div.icon24-outer.mr-2 {
534 div.icon24-inner {_out.push(icon_tick)}
536 'Provide an icon in every button, this speeds entry and also assists non-English speaking users.'
538 div.alert.alert-success {
539 div.icon24-outer.mr-2 {
540 div.icon24-inner {_out.push(icon_tick)}
542 'Provide strong visual indication of completion, success or failure by means of icons and colours.'
544 div.alert.alert-success {
545 div.icon24-outer.mr-2 {
546 div.icon24-inner {_out.push(icon_tick)}
548 'Show a pacifier when undertaking an operation that takes time, this confirms that the last click was received and also makes the operation seem quicker.'
550 div.alert.alert-success {
551 div.icon24-outer.mr-2 {
552 div.icon24-inner {_out.push(icon_tick)}
554 'Disable UI elements such as buttons when it doesn’t make sense to interact with them yet.'
558 'Final comments on form filling'
565 'Whilst the old way was sufficient, it was only that, sufficient. The new way provides a more vibrant experience for the user, while capturing the data in a more efficient way by guiding the user through the process. We are also striving to reduce user frustration that could result in them leaving the form (and lost sales/conversions in a commercial context).'
569 'Sadly I have not had time to make the form accessible yet, and that will probably be the subject of a future article. Apart from that, the experiment with providing a modern form-filling UX has been very successful. Although it has taken a week or so of experimentation, I consider this time well spent, as I learned something about modern UX design.'
577 'The new internals of the site are much more pleasant to work on, as things are much more organized, and the '
579 ' database allows plenty of flexibility. Thus we can quickly implement UX features such as the saving of drafts. Moreover, the new UX (implemented during the database upgrade) is much more visually pleasing and easier to use.'
584 //script(src="/js/utils.js") {}
587 document.addEventListener(
590 let id_old_details = document.getElementById('old-details')
591 let id_old_family_name = document.getElementById('old-family-name')
592 let id_old_form = document.getElementById('old-form')
593 let id_old_given_names = document.getElementById('old-given-names')
594 let id_old_main = document.getElementById('old-main')
595 let id_old_message = document.getElementById('old-message')
597 document.getElementById('old-pip0'),
598 document.getElementById('old-pip1'),
599 document.getElementById('old-pip2'),
600 document.getElementById('old-pip3'),
601 document.getElementById('old-pip4'),
602 document.getElementById('old-pip5'),
603 document.getElementById('old-pip6')
605 let id_old_refresh = document.getElementById('old-refresh')
606 let id_old_server = document.getElementById('old-server')
607 let id_old_submit = document.getElementById('old-submit')
609 let id_new_cross = document.getElementById('new-cross')
610 let id_new_details = document.getElementById('new-details')
611 let id_new_family_name = document.getElementById('new-family-name')
612 let id_new_form = document.getElementById('new-form')
613 let id_new_given_names = document.getElementById('new-given-names')
614 let id_new_icon = document.getElementById('new-icon')
615 let id_new_main = document.getElementById('new-main')
616 let id_new_message = document.getElementById('new-message')
618 document.getElementById('new-pip0'),
619 document.getElementById('new-pip1'),
620 document.getElementById('new-pip2'),
621 document.getElementById('new-pip3'),
622 document.getElementById('new-pip4'),
623 document.getElementById('new-pip5'),
624 document.getElementById('new-pip6')
626 let id_new_refresh = document.getElementById('new-refresh')
627 let id_new_server = document.getElementById('new-server')
628 let id_new_spinner = document.getElementById('new-spinner')
629 let id_new_submit = document.getElementById('new-submit')
630 let id_new_tick = document.getElementById('new-tick')
633 let old_pips_to_server = async () => {
634 for (let i = 0; i < 7; ++i) {
635 id_old_pips[i].setAttribute('r', 32)
636 await new Promise(resolve => setTimeout(resolve, 100))
637 id_old_pips[i].setAttribute('r', 16)
640 let old_pips_at_server = async success => {
641 id_old_server.classList.add(
642 success ? 'text-success' : 'text-danger'
644 await new Promise(resolve => setTimeout(resolve, 100))
645 id_old_server.classList.remove(
646 success ? 'text-success' : 'text-danger'
649 let old_pips_from_server = async () => {
650 for (let i = 6; i >= 0; --i) {
651 id_old_pips[i].setAttribute('r', 32)
652 await new Promise(resolve => setTimeout(resolve, 100))
653 id_old_pips[i].setAttribute('r', 16)
657 id_old_refresh.addEventListener(
660 event.preventDefault()
661 id_old_main.hidden = true
662 id_old_message.hidden = true
663 await old_pips_to_server()
664 id_old_details.textContent =
667 await old_pips_at_server(true)
668 await old_pips_from_server()
669 id_old_given_names.value = ''
670 id_old_family_name.value = ''
671 id_old_main.hidden = false
674 id_old_submit.addEventListener(
677 if (!id_old_form.reportValidity())
680 given_names: id_old_given_names.value.slice(0, 256),
681 family_name: id_old_family_name.value.slice(0, 256)
683 await old_pips_to_server()
684 id_old_details.textContent =
685 `details = ${JSON.stringify(details, null, 2)}
688 details.given_names.toLowerCase() !== 'jane' ||
689 details.family_name.toLowerCase() !== 'doe'
690 await old_pips_at_server(success)
691 await old_pips_from_server()
693 //id_old_message.classList.remove('text-danger')
694 id_old_message.textContent = `Hello, ${details.given_names} ${details.family_name}!`
697 //id_old_message.classList.add('text-danger')
698 id_old_message.textContent = 'Our server has determined that you are not a real person!'
700 id_old_main.hidden = true
701 id_old_message.hidden = false
706 let new_pips_to_server = async () => {
707 for (let i = 0; i < 7; ++i) {
708 id_new_pips[i].setAttribute('r', 32)
709 await new Promise(resolve => setTimeout(resolve, 100))
710 id_new_pips[i].setAttribute('r', 16)
713 let new_pips_at_server = async success => {
714 id_new_server.classList.add(
715 success ? 'text-success' : 'text-danger'
717 await new Promise(resolve => setTimeout(resolve, 100))
718 id_new_server.classList.remove(
719 success ? 'text-success' : 'text-danger'
722 let new_pips_from_server = async () => {
723 for (let i = 6; i >= 0; --i) {
724 id_new_pips[i].setAttribute('r', 32)
725 await new Promise(resolve => setTimeout(resolve, 100))
726 id_new_pips[i].setAttribute('r', 16)
730 let new_details, new_draft_details
731 let new_refresh_details = () => {
732 id_new_details.textContent =
733 `draft_details = ${JSON.stringify(new_draft_details, null, 2)}
734 details = ${JSON.stringify(new_details, null, 2)}
738 let new_input_semaphore = new BinarySemaphore(false)
742 await new_input_semaphore.acquire()
743 await new Promise(resolve => setTimeout(resolve, 3000))
744 new_input_semaphore.try_acquire()
746 id_new_given_names.value.length === 0 &&
747 id_new_family_name.value.length === 0 ?
750 given_names: id_new_given_names.value.slice(0, 256),
751 family_name: id_new_family_name.value.slice(0, 256)
753 await new_pips_to_server()
754 new_draft_details = draft_details
755 new_refresh_details()
756 await new_pips_at_server(true)
757 await new_pips_from_server()
760 )() // ignore returned promise (start thread)
762 let new_edited = () => {
763 new_input_semaphore.release()
765 id_new_submit.disabled =
766 id_new_given_names.value.length === 0 &&
767 id_new_family_name.value.length === 0
768 id_new_icon.hidden = false
769 id_new_tick.hidden = true
770 id_new_cross.hidden = true
771 id_new_spinner.hidden = true
772 id_new_message.hidden = true
775 id_new_given_names.addEventListener('input', new_edited)
776 id_new_family_name.addEventListener('input', new_edited)
778 id_new_refresh.addEventListener(
781 event.preventDefault()
782 id_new_main.hidden = true
783 id_new_message.hidden = true
784 await new_pips_to_server()
785 await new_pips_at_server(true)
786 let draft_details = new_draft_details
787 await new_pips_from_server()
788 id_new_given_names.value =
789 draft_details ? draft_details.given_names : ''
790 id_new_family_name.value =
791 draft_details ? draft_details.family_name : ''
792 id_new_submit.disabled =
793 id_new_given_names.value.length === 0 &&
794 id_new_family_name.value.length === 0
795 id_new_main.hidden = false
798 id_new_submit.addEventListener(
801 id_new_icon.hidden = false
802 id_new_tick.hidden = true
803 id_new_cross.hidden = true
804 id_new_spinner.hidden = true
805 // the below causes an ugly flicker, so just keep the message
806 //id_new_message.hidden = true
808 if (!id_new_form.checkValidity()) {
809 id_new_form.classList.add('was-validated');
811 id_new_icon.hidden = true
812 id_new_cross.hidden = false
815 id_new_form.classList.remove('was-validated');
818 given_names: id_new_given_names.value.slice(0, 256),
819 family_name: id_new_family_name.value.slice(0, 256)
822 id_new_icon.hidden = true
823 id_new_spinner.hidden = false
824 await new_pips_to_server()
825 new_details = details
826 new_refresh_details()
828 details.given_names.toLowerCase() !== 'jane' ||
829 details.family_name.toLowerCase() !== 'doe'
830 await new_pips_at_server(success)
831 await new_pips_from_server()
833 id_new_spinner.hidden = true
835 id_new_tick.hidden = false
837 id_new_message.classList.add('alert-success')
838 id_new_message.classList.remove('alert-danger')
839 id_new_message.textContent = `Hello, ${details.given_names} ${details.family_name}!`
842 id_new_cross.hidden = false
844 id_new_message.classList.remove('alert-success')
845 id_new_message.classList.add('alert-danger')
846 id_new_message.textContent = 'Our server has determined that you are not a real person!'
848 id_new_message.hidden = false
857 white-space: pre-wrap;
858 word-wrap: break-word;
862 display: inline-block;
863 vertical-align: center;
867 stroke: currentColor;