Introducing Odom: Creating a Reactive Signup Page

Introduction
When developing a signup page, there are a couple of things to consider. On the design sign side, the user must easily figure out what information to enter in each field. If the information must be in a particular format, it is best to give the user a hint on what they are expected to do. On the form handling side, validation ensures that the form is not sent to the server with invalid information. The user can be notified of the validity of the information either while entering or upon submission. When reporting errors, the more specific the error, the earlier the user is going to figure out what went wrong.
Achieving all this involves a lot of work. We can make the implementation process easier by using Odom. Odom is a JavaScript framework for building user interfaces. This is the second part of the series Introducing Odom. In the first episode, The Open UI Framework, the basic features of Odom were looked at. In this episode, we are going to see Odom in action by creating a Single Page Application (SPA). The application will contain a signup section. The signup section will contain a form for signing up. The picture below shows a snapshot of the application (on wide screens).

We are going to focus on the following topics:
The repo for the app is published on GitHub. The live demo is published on Netlify. You can clone or download the repo if you want. However, in this article, I am going to include all the code and give instructions on how to build the project.
File Structure
We are going to use the following file structure for the application:
.
└── odom-signup/
├── src/
│ ├── assets/
│ │ └── index.css
│ ├── components/
│ │ ├── alert-message.js
│ │ ├── app.js
│ │ ├── confirmation-message.js
│ │ ├── form.js
│ │ ├── input-group.js
│ │ └── signup.js
│ └── index.js
└── index.html
Create the file system as shown above using empty files. We will populate all the files later.
Creating Components
For the sake of simplicity, we are going to use a CDN to create the app. Destructuring will be used to get methods from the window
object window.odom
. This will make it easy for you to convert the project if you would rather work with package managers like NPM. The following code demonstrates how you can replace the destructuring assignments with import statements.
We will refer to all the exports in the modules in /odom-signup/src/components
as components because they are self-contained entities. However, some of the modules will be exporting functions that return DOM nodes instead of an instance of the component offered by Odom. The reasons for doing so were given in the first episode.
I will split some of the modules into multiple code snippets to make the code more readable.
App
The root component of our application will be App. This is the component we are going to render to the DOM. We will create the component in the module at/odom-signup/src/components/app.js
. Paste the following code into the file:
In the module, the component Signup is imported. The method createComponent
is obtained from odom
. The markup
contains two elements. The root element is a div
and the inner element has an attribute odom-node
set to Signup
, which refers to the component we have imported. Replace the comment (/* styles */
) in the template literal for the variable styles
with the following CSS code:
The selector :scope
selects the root element of the component. In :scope
, we declare CSS variables for the colour theme and spacers (used for margin, padding and more). The rest of the CSS sets the width, minimum height, font family and the text colour of the component.
Let us look at what each one of the functions in the module does.
App
: The constructor for the component. In Odom, a constructor is a function that returns an instance of a component, DOM node, markup or text. When invoked,App
will return an instance ofComponent
, which is also the return value ofcreateComponent
. Capitalising the name of a constructor is not mandatory. We will use this as a convention. We will also begin the names of the instance of the components with lowercase letters. You can not use thenew
keyword to invoke Odom constructors as you would JavaScript native constructors.createComponent
: Creates the component. The parameter is an object with three properties. Themarkup
will be used to create the DOM element for the component. Thestyles
will be used to style the component. The objectutils
has one property,nodes
, which is used to provide node assets. So, the valueSignup
, for the attributeodom-node
in the markup refers toutils.nodes.Signup
.
Signup
Signup is the only section of App. It will handle the form submission and reporting of failure or success of the submission. Put the following code in the module at /odom-signup/src/components/signup.js
:
In the module, we import the components Form and ConfirmationMessage. We also get the methods createComponent
and replaceNode
from odom
. The value form
of the attribute odom-src
in the markup
refers to an instance of Form
. The value form
is not capitalised because it refers to an instance of a component and not the component itself. The media query in styles
sets a padding on top of the component on windows with widths that are at least 576px
. Notice that we used the variable --spacer-xl
which was declared in App. All components in Odom inherit styles from their ancestors.
Let us look at what each function in the module does:
Signup
: The constructor for the node. Inside the function, we create a component, use it and return a DOM nodesignup.scope
. We return a node instead of a component because the component interface is not need anymore.onvalid
: Sends the form if all inputs are valid. If the form has been successfully sent, it shows a confirmation message. The methodreplaceNode
is used to replace the form with aconfirmationMessage
. If sending of the form fails,form.onSignupFail
is invoked to inform the user that they have not been signed up because of an error.createComponent
: Used to create the component. The propertycomponents
of the parameter provides the component assets referenced in the markup.sendForm
: Sends the form to the server. The function simulates the process of sending to the server. It returns early with a promise that resolves to a mock-up of the response object. Removing the return statement leaves with code you can use to send the form to an actual server.
Notice that we passed onvalid
into Form
as a prop. In the next section, we will see how onvalid
is used in Form
.
Form
Form initiates form submission. It also reports errors from input validation and form submission. Put the following code into the module at /odom-signup/src/components/form.js
:
In the module, we are importing the components AlertMessage
and InputGroup
. Replace the comment in the value for markup
with the following HTML code:
The values for the attribute odom-src
in section
elements refer to the instances of components.
Replace the comment in the value for styles with the following CSS code:
The selector > *:not(footer)
is equivalent to :scope > *:not(footer)
. The media query sets a border and a fixed width on the component on windows that are at least 576px
wide.
Replace the empty object for props
with the following code:
The object props
contains the props for all the instances of InputGroup
. The following are the purposes for each one of the properties for the props of each instance:
type
: The type of the input element.name
: The name of the input element set via the attributename
.label
: The text content for the label of each input group.pattern
: The pattern used to validate the input values.instruction
: An instruction on the format of information expected from the user.errorMessage
: The message displayed if the input is invalid.successMessage
: The message displayed if the input is valid.
Let us look at the purposes of the functions used in the module:
Form
: The constructor for the component.onsubmit
: A listener for the eventsubmit
. It checks if all inputs are valid. If inputs are valid, it initiates the sending process by callingonvalid
, the prop from Signup. It also disables the submit button so that the user does not click on it while the sending is still in progress. It removes the attributename
from the element for confirming password so that the value of the element is not sent with the form to the server. If any of the inputs is invalid, it shows an error message by invokingonerror
.onerror
: Displays an error message by callingalertMessage.show
.onSignupFail
: It displays an error message if there is a signup failure. It also enables the submit button to enable the user to try again.password.addObserver
: Used to add an observer of the input value changes in the componentInputGroup
. Sincepassword
is an instance ofInputGroup
, the method is used to observe the password changes. Notice that thepattern
(inprops
) forconfirmPassword
is the same as that ofpassword
. We want the user to confirm their password by re-entering it. The observer is used to update the pattern forconfirmPassword
so that it matches the password. It also sets the validity ofconfirmPassword
to false to prevent the user from submitting an invalid confirmation password if they edit the password after entering the confirmation password.createComponent
: Used to create the component. The propertyprops
specifies the props of the component. Props specified this way can be referenced in markup and end up on the interface of the component. In this case, the methodonSignupFail
is used by Signup via the interface. The propertyeventListeners
of the parameter maps the CSS selector:scope
to an array containing an event object. The selector is used to select elements we want to apply event listeners to, which in this case is the root element of the component. The array can contain as many event objects as we want. The propertytype
specifies the type of event we want to listen to. The propertylistener
specifies the listener for the event.
AlertMessage
The component AlertMessage handles the display of error messages in Form. Put the following code into the module at /odom-signup/src/components/alert-message.js
:
The markup
contains a root element and its only child. The class hide
is set in order to hide the alert message by default. The selector :scope.hide
in styles
selects the root element when it is has the class hide
. The functions have the following purposes:
AlertMessage
: The constructor for the component.show
: Show the alert message.createComponent
: Creates the component. The propertyprops
in the parameter is used to addshow
to the component interface.
InputGroup
InputGroup gets user input and performs validation. Put the code below in the module at /odom-signup/src/components/input-group.js
:
The markup
contains a section
as the root element. The label
indicates what the user is supposed to enter. The value @props.inputID
refers to the value set in props
, a property of the parameter to createComponent
(more on this later). The attribute odom-text
of the span
inside the label
indicates that the element will be replaced by text. Notice that there are no attributes on the input
element. We will set the attributes later. This is because the input element has several attributes and setting them directly in the markup
would make the markup
hard to read.
Replace the comment in the value for styles
with the following CSS code:
Let us look at what each function in the module does:
InputGroup
: The constructor of the component.updateValue
: Used to update the value of the input in the component. The value is updated every time the user changes the value in the input field.updateValidity
: Updates the validity of the input. The update is performed every time the user changes the input.updateMessage
: Updates the validation message. If the input is invalid, the message will be coloured green. Otherwise, the message will be coloured red.hasValidValue
: Checks whether or not the input is valid. It also triggers the border colour update by invokingupdateInputBorder
.updateInputBorder
: Updates the border of the input field. If the input is invalid, the border will coloured red. Otherwise, it will be coloured grey, the default colour.props.addObserver
: Adds an observer for the input value. Every time the value changes, the observer is invoked with the new value.props.setValidity
: Sets the validity of the input value. This is used on the interface of the component.createComponent
: Creates the component. The functions takesoptions
as its parameter.
The property props
of the parameter options
is used to set properties on the interface and provide values that can be referenced in markup
. In the markup, the value @props.inputID
refers to props.inputID
. The property attributes
is used to set attributes on the input element. The CSS selector input
selects the input filed. The object set to input
contains key-value pairs of the attribute names and values of the input element.
The property utils.data
contains data that can be referenced in markup. The property utils.data.dynamic
contains the data that may change during the execution of the app program. The data can be bound to the DOM, so that the changes made to the DOM are reflected in the component data and vise versa. So, the value ::@data.value
refers to utils.data.dynamic.value.data
. The two colons indicate that there will be a double bind between the DOM updates and the component data. The array utils.data.dynamic.value.updaters
specifies the functions that will be responding to the updates. The property utils.data.dynamic.valid
follows the same pattern, but without the DOM binding.
The property utils.texts
specifies the text asset referenced in the markup
by the attribute odom-text
.
All the dynamic data specified in data
end up in inputGroup.dynamicData
. To update a value like data.dynamic.valid.data
, you set the value directly on inputGroup.dynamicData
. For example, setValidity
sets the value directly using the asignment inputGroup.dynamicData.valid = valid
. Doing so invokes updateValidity
. The function updateValidity
gets the new value as a parameter and returns a Boolean
value after updating the message. Notice that the method updateValue
sets inputGroup.dynamicData.valid
to its parameter value
, which is a string. The function updateValidity
intercepts the value, checks if it is valid and returns a boolean. If the value is not a string, it is assumed to be a Boolean
, thus it is return without modification. The returned value is what inputGroup.dynamicData.valid
will be set to.
ConfirmationMessage
Confirmation of successful signup will be done by ConfirmationMessage. Put the following code in the module at /odom-signup/src/components/confirmation-message.js
:
Replace the comment in the value for styles
with the following code:
The media query sets a border on the component on windows with widths that are at least 576px
. The function ConfirmationMessage
is the constructor. It returns a promise that resolves to a DOM node.
Rendering
In this section, we are going to render the component App to the DOM. Put the following code in the module at /odom-signup/src/index.js
.
In the module, App is imported. In the IIFE, an instance (app
) of the component is created. The method app.render
renders the component to the DOM. The parameter of the method is a CSS selector for the element that is to be replaced by App in the DOM.
In the file at /odom-signup/index.html
, put the following code:
The link
element in the head
element adds styles to the page. In the file at /odom-signup/assets/index.css
, put the following CSS code:
The styles are used to reset some of the default styles. The value for the box-sizing
is reset to border-box
for all elements. The margin
and padding
of the body
element are reset to 0
.
The element div#app
is what is going to be replaced by the component App. The first script
element includes Odom to the page via a CDN. The second script
element adds the main JavaScript file we created earlier to the page.
Save the file. Make sure that all the files created are saved. Open the HTML file (/odom-signup/index.html
) in the browser. You should be able to see the page with the form on it. If anything does not seem right, you can check if you did not follow any step properly, or you can use the files in the repo.
Enter valid values in all fields and submit the form. You should be able to see the confirmation message. Refresh the page and enter an invalid value in any fields and submit the form. You should be able to see an error message just below the heading of the form. Refresh the page and submit the form without entering any value in any field. You should be able to see an error message displayed as before.
Optimisation
Styles
If you inspect the page elements using the development tools in your browser, you should see 8 style
elements in the head
element. The style
elements are for the components App, Signup, Form, Alert and InputGroup (4). All 4 instances of InputGroup use the same styles. This makes 3 of the style
elements redundant. We can get rid of the redundancy by telling Odom that the 4 instances of InputGroup come from the same component. We can do so by setting an ID on InputGroup. In the module for InputGroup (/odom-signup/src/components/input-group.js
) set a property options.id
to odom-"input-group"
or any String
value you like. The code below illustrates how to do so.
Save the file, refresh the page and inspect the elements again. You should see only 5 style
elements this time. Odom was able to prevent the redundancy by not processing and adding styles for all instances after the first one. All instances used the same styles.
DOM Node Nesting
In Odom, avoiding nesting of DOM nodes whenever possible is very important for optimisation. All child components of a given component are build at once. This means that the amount of time it takes to build all child components is approximately equal to the amount of time it takes to build the component that takes longest to build.
Let’s assume that it takes 100 milliseconds to build AlertMessage, and 200 milliseconds to build InputGroup. It would take approximately 200 milliseconds to build all the child components of Form. If we decided to put all the InputGroup elements into a single element like a section
element, the InputGroup instances would be built first. The AlertMessage instance would be built after that. This means that the amount of time it would take to build all the components would be approximately equal to 300 milliseconds (100 + 200). We have already performed this kind of optimisation by making all components are direct descendants of the root element of Form.
Conclusion
Whether it’s adding OAuth, splitting the signup process into multiple steps or anything else, I’d like to see how you can customise the application. If you find time to do so, please share your project and experience in the comment section. Thank you for reading.