As a front-end developer, I have seen that there is a lack of formal patterns and practices in the work that we do. This often leads to our projects taking longer and being more confusing than they should be as we have to learn on the fly and hack things together. I believe that building solutions to address this problem comes with experience and so, I would like to share some of what I've learnt to start paving a path towards a standard that we can all follow.
Front-end developers donāt have a common language to discuss and understand the building blocks
Iām a Senior Software Engineer and Iāve being doing development across the stack for 11 years. A few years ago, I embarked on a quest to āownā front-end development at my company, in particular web development as we assume today.
Front-end developers spend most of their time focusing on what is called a āuser interfaceā, which can be broken down into various elements, or ābuilding blocksā, known as controls or components.
A user interface is exactly what the name suggests: a way for users to interact with an application or some piece of software. Its primary purpose is to display information to a user, capture information from a user and submit a request to some other code or service that will perform some action on the userās behalf.
A couple of months ago, a colleague, new to front-end development, reached out to me for help with a particular feature for the platform we work on. When I looked at the code for the implementation of a simple dropdown box, it became clear to me that we were lacking standard patterns and practices for UI development.
How I would have implemented the dropdown box was completely different to how my peer, who was having an issue with the way the dropdown interacted with the rest of the feature, did it. As the mechanism in question was quite simple and there were a number of different ways to implement it, I realised that thinking about and discussing front-end patterns and practices, as we do on the back-end, could help developers build front-ends more easily and efficiently build. It could also provide developers with a common language when discussing front-ends, thus speeding up the process of communicating ideas and solving issues.
Across the board, there has been more emphasis on higher quality back-ends. We havenāt had the time to settle on solid front-end practices because our industry is so dynamic. As someone who has worked in this field for a long time, however, I would argue that we need to change this. Before we can start getting fancy with the work we do, we need to build a foundation that we all agree works well.
We need something that is maintainable, reusable, and easy to test and understand. Once we have achieved this, we will be able to build software that serves and adds value for users in a quicker, more efficient way.
In this article, I have attempted to put down some initial steps towards building foundational principles. My goal is to start a discussion within our community and collaborate on adding to and improving these ideas.
Disclaimer: Accompanying this article is a simple demo application written in Vue, React and Angular. The demo attempts to demonstrate each of the patterns or practices discussed in a āclose toā real world environment, as this is where we need to consider the usage of these patterns.
We often see documentation and examples that focus on how to use a control in isolation from the rest of the application but, rarely do we see them working together, which is what this article focuses on.
The article itself contains excerpts from either the Vue or React sample code but doesnāt go too deep into the explanation of the code itself. This is because the emphasis is more on what the pattern or practice is about, its benefits and how it can help you. The sample code that follows is based on a password manager application, Azkaban, for which you can find the source code here.
Basic control best practices
Letās begin with basic controls. These are the simplest components you get and include things like buttons, text-fields and checkboxes (an image component could possibly fall into this category as well). These components typically consist of a single value, such as a person's name for a text field or a function that gets called when a button is clicked. They donāt do much individually, but together they are very useful as they are used to build forms and other features.
Though these controls are simple to begin with, we should build them with the idea that they will grow in complexity. Having good practices that everyone on the team understands and follows helps to manage the complexity as time goes on.
Letās have a look at some standards that we can apply to keep things as simple and easy to follow as possible when working with controls.
Another disclaimer: In the example, Iāve used these components to build a sign-in page, a feature that uses all of these components together without too much repetitiveness. Iāve used the basic controls to capture the username and password from the user, who can then click on a button to submit their credentials to a server that will perform the necessary validation and respond with, usually, a JSON Web Token (JWT) to authenticate any subsequent requests. A checkbox control is used to allow the user to optionally specify whether to be remembered, which means that if they close the web browser, the next time they open the web page, they will be signed in.
Naming conventions
A naming convention is as simple as it sounds: It is an agreed-upon system for naming code and other files that teams work on.
When there is no standard system for naming files, it is common for people to quickly feel overwhelmed by the idea of trying to figure out how everything works and fits together. Having good names makes it easier to understand what the view is doing and how it works in relation to the underlying code.
Tip: Keep your field names clear, concise and visible. Decide on what your naming convention should be with your team and try to keep it consistent with the underlying model.
The following code snippet is from the 'Sign In' page of our application (Vue demo), where we have two inputs for the email address and password, a checkbox for a 'Remember Me' option and a button that initiates the sign in process.
<v-form>
<v-text-field v-model="email" label="Email Address" prepend-icon="email"></v-text-field>
<v-text-field v-model="password" label="Password" prepend-icon="lock" type="password"></v-text-field>
<v-checkbox v-model="rememberMe" label="Remember Me" color="primary"></v-checkbox>
</v-form>
<v-btn @click="signIn" color="primary">
<v-icon left>security</v-icon>Sign In
</v-btn>
The names used in our data-bindings tie the view to the underlying JavaScript, which is normally somewhere else, out of sight.
Having good names makes it easier to understand the relationship between the view (HTML) and the view-model (JavaScript), and, as a result, it is easier to detect potential data-binding issues between the view-model and the view. Unit tests are great for this too but we can save time by detecting the issues a lot sooner through easy-to-see and easy-to-recognise naming conventions.
Attribute Ordering
Attribute ordering is another convention and has to do with the placement of markup attributes in your code.
When attributes are ordered consistently and relevantly you can easily and quickly understand the code that you are reading.
Tip: Order attributes by relevance. Decide with your team what is most relevant to the team as a whole. Hereās an example of how Iāve ordered attributes in our example code:
<v-text-field v-model="email" label="Email Address" prepend-icon="email"></v-text-field>
In this code, Iāve placed the v-model attribute first, as this ties the text field to the model and this is what is most important to me and my team. The label attribute is placed second as it determines what the user sees, which is also very important but, since they arenāt looking at the code, not as important. The prepend-icon field is placed last as it is the least relevant.
This practice is another step in making the code more readable and thus, understandable. Iāve found that, as a team, when we have worked with larger forms and components, it helps when things are lined up in the same way and you know where to look.
Formatting
Formatting deals with the way code is displayed and laid out (usually automatically) in your editor. Line length, indentation and 'tabs versus spaces' are elements of formatting.
Standardising the formatting of code, especially HTML, helps to keep code consistent across team members and tools. If you are used to a particular type of formatting and you view code that is not in that format, it takes more time to understand and become familiar with it. This is especially true for HTML (or JSX) where the code can be heavily nested and laden with attributes.
Tip: Adopt a formatting style, especially if you are using different IDEās or tools on your team. Make use of your IDEās āformat on saveā option to format code whenever you save it. Another great way to ensure that code is always formatted in the correct way across your team is to implement a git hook that formats code whenever it is committed.
Tip: Know your CSS/UI framework. If youāre working with a popular CSS/UI framework such as Vuetify (Vue), Material UI (React) or Angular Material (Angular), it helps to get familiar with the basic controls and to know what they are capable of and how best to use them, before diving into the code. If you are using custom CSS and components, get familiar with it so that you are sure that it's working for you, not you for it.
Having a standard code format in my team, especially since we use different IDEās, has made it much easier to read and understand each other's code. As a bonus, it has also made it easier to merge changes.
Now that we have looked at some of the basic control best practices, letās look at some other patterns and practices in more detail. Iāll talk you through my suggestion for approaching:
- Select/dropdown patterns
- Layout best practice
- Progress patterns (view state pattern in general)
- The Create, Read, Update, Delete (CRUD) UI pattern
Select/dropdown pattern
This pattern demonstrates a good practice for implementing a selection or dropdown control and highlights some points to consider when implementing one.
A selection control is deceptively simple to implement but can lead to a headache. Itās important to clearly differentiate between what is currently selected and what can be selected. This is because what is currently selected is usually used elsewhere to drive some other behaviour.
You can almost always use this pattern when you have a select or dropdown control. In the demo application, I am using a selection control to provide a way for the user to filter passwords by category. The selection control displays the available categories and, when a specific one is selected, the list of passwords below is updated to reflect only passwords in the selected category.
Typically, youāll have a list of objects that you want to show in the select or dropdown control. These are normally complex objects, so they might consist of various properties.
In our example, where Iāve used categories to demonstrate this pattern, a single category consists of an ID and a name. Sometimes, a category will have a parent category in order to model sub-categories. This is a good example of how youāre often not just setting up a select on an array of strings. Each select option is rendered with the value being the objects ID and is using the name property for the display text. The value of the select itself is bound to a 'selected' value field in the state.
When using React in our example, we see this pattern using a category selection control, whereby a user can select a category to filter the list of passwords displayed.
The select
control is rendered with the value set to selectedCategory
from the state. This also allows us to specify a default category, as demonstrated in the second snippet.
The selects onChange
event is handled by a method called handleChange
, which simply updates the state using the value from the onChange
event. A MenuItem
component is used to render an option for each category in the categories array. The value of the category option is the category object itself and the text is the category.name property.
<Select
value={selectedCategory}
onChange={this.handleChange}>
{AzkabanService.categories.map(category => (
<MenuItem key={category.id} value={category}>
{category.name}
</MenuItem>
))}
</Select>
This is the example state showing the selectedCategory
state, which is used to render the select controls currently selected value.
class PasswordManager extends Component {
state = {
selectedCategory: AzkabanService.categories[0],
isLoadingPasswords: false
};
...
};
You could use category.id as the value property and, in many cases, this is enough, but for complex objects such as a property, I prefer to use the whole object when setting my selectedCategory
. The reason for this is that I may need extra information about the currently selected category, such as the name or the parent category. If I just stored the ID, I would need to do extra work to get the information I wanted and display it.
Tip: If you do decide to use the object's ID as your value, be explicit about your variable name. For example, use selectedCategoryId
, as this makes it clear that the ID is stored and not the whole object.
This is a simple pattern to implement but it might not be obvious to everyone. Having a pattern like this that can be used almost always, helps to keep code consistent and ensures that all team members can easily see how a particular select or dropdown control is working.
Layout best practice
Layout is a cornerstone of front-end development and deals with how a component, page or feature is structured at a high level.
Iāve found that it sets a good precedence for how to componentise a feature, or break a feature down into its individual components, in a way that leads to good separation of concerns and with the right amount of modularisation. This is especially applicable when an application needs to be responsive.
Grids
As a front-end engineer, the grid system is one of the tools that I am most reliant on. Grids form the foundation for everything I build, from a web page to a simple component and itās one of the first things I get to know when learning a new UI/CSS framework. Not all grid systems are made equal and itās a good idea to find a good one that works for you as it will save you a lot of time and trouble if you can use it well.
Most modern grid systems are built on top of CSS Flex and, while you donāt have to, it really does help to understand how CSS Flex works.
All three of the sample applications use a CSS Flex based grid system, which typically comprises of a container, layout and flex.
In our sample application, once signed in, you are presented with a page where you can view, edit or delete all your saved passwords, filter them based on a category and add new ones.
The following image highlights how this page has been defined in a grid system.
The main thing to note about this page is that we have nested grids ā i.e. grids within grids. This is a key take away for this section: You will typically have many levels of nesting inside a page or component, depending on how complex it is.
In this page, the red blocks are layouts and the blue block are flex elements (Containers encapsulate layouts adding extra padding and are not always used). The main layout has four flex elements that have 100% width.
The first two flex elements again contain layouts and, these layouts have more flex elements. The first layout element in the first 'row' defines a header for the page. It consists of two flex elements: one for the icon and one for the text. Vertically aligning the two flex elements to the center and horizontally aligning them to the left (justify-start) is handy for achieving the desired effect.
Tip: Get intimate with your CSS/UI framework grid system. It will usually provide components to help you define layouts more easily.
Tip: Understand and learn how to use CSS Flex. If you donāt want to depend too heavily on your CSS/UI framework or when itās too much for what you need, you can use CSS Flex.
Well-defined layouts, especially responsive ones, often seem to be overlooked. Poorly designed layouts can cause problems at a later stage, so, investing time in your grid system and getting your team onboard will make the development of pages and components much easier and quicker.
Progress pattern (view state pattern in general)
The progress pattern, or the view state pattern in general, is a great way to change whatās represented on screen depending on the current state of something.
This is especially effective when dealing with progress, but it is just as applicable elsewhere, such as a wizard or some kind of multi-step process.
I once implemented a multi-step registration process using this pattern where you could navigate back and forth between various forms that needed to be filled out. I also implemented an 'action button', similar to the example which we will get into below, which contains various states and updates the display of the button based on those states.
The component was generic but an example of how it was used was for saving a user's profile details. The button displayed with the text 'Save', the colour green and showed the save icon (Ready Sate). When you clicked on it, the text disappeared and a spinning circle icon was shown to indicate that the request to update was running (In Progress).
When the information had been updated successfully, the text changed to 'Done', the colour changed to green and the icon showed as a tick (Success). After about one second, the state reverted back to 'Ready' and the text and icon changed accordingly. If there was an error, the text changed to 'Error', the colour to red and an error icon was shown (Error). More details were displayed about the error in a tooltip if you hoverd over the button.
This pattern worked by using a view state to control how the component was represented. The component could have multiple states and was represented differently for each state.
This pattern is great for representing more than two states on a component, but even when you only have two states, itās still a good pattern to use as it makes adding new states a breeze.
Components representing various states can easily be extended and modified when using this pattern. When we haven't used this patter, my team has found that the code controlling which parts of the view get rendered and which donāt can quickly get ugly and out of hand. This makes it very difficult to understand what a component is doing and how it is behaving.
If we take, for example, the progress of the sign-in page, there are generally four different states that the page may be in. These are,
- Ready - We are waiting for the user to enter their username and password and click the 'Sign In' button.
- In progress - The user has clicked the 'Sign In button', weāve sent the request to the backend server and are awaiting a response.
- Success - The request has completed and a successful response from the backend server has been received.
- Error - The sign-in request has completed with errors, typically to do with an incorrect username or password.
We could use in-line if statements to control the visibility of elements, using React as an example:
{(inProgress && !isComplete && !error) && (<div>Doing something...</div>)}
{(!inProgress && isComplete && !error) && (<div>Completed Successfully</div>)}
{(!inProgress && isComplete && error) && (<div>Failed with error: { error }</div>)}
This quickly gets out of hand though, becoming difficult to read (especially in React), difficult to maintain and prone to errors.
A better way to do this could be with a state variable, for example:
{(state === 'IN_PROGRESS') && (<div>Doing something...</div>)}
{(state === 'SUCCESS') && (<div>Completed Successfully</div>)}
{(state === 'ERROR') && (<div>Failed with error: { error }</div>)}
Much better! In our sample app (React), we use the sign in page to demonstrate this in a real world scenario. By default, we have a signInState
that is in the SIGN_IN_STATE_READY
state:
class SignIn extends Component {
state = {
email: '',
password: '',
rememberMe: false,
signInState: SIGN_IN_STATE_READY,
error: ''
};
...
When we click the 'Sign In' button we call the signIn method
, which changes the state to SIGN_IN_STATE_IN_PROGRESS
. This changes the view to show a loading indicator. We then call into our service and, if successful, we update the state to SIGN_IN_STATE_SUCCESS
and, if not, we update it to SIGN_IN_STATE_ERROR
and set the error message that is then displayed.
signIn = () => {
this.setState({ signInState: SIGN_IN_STATE_IN_PROGRESS });
AzkabanService.signIn(this.state.email, this.state.password)
.then(() => {
this.setState({ signInState: SIGN_IN_STATE_SUCCESS });
setTimeout(() => {
this.props.history.push('/passwords');
}, 750);
})
.catch(error => {
this.setState({ signInState: SIGN_IN_STATE_ERROR, error: error });
});
};
{signInState === SIGN_IN_STATE_IN_PROGRESS && (
<Fragment>
<Grid item xs={12}>
<CircularProgress />
</Grid>
<Grid item xs={12}>
<Typography component="h3" variant="title">
You are being signed in.
</Typography>
<Typography component="h4" variant="subheading">
Please wait...
</Typography>
</Grid>
</Fragment>
)}
{signInState === SIGN_IN_STATE_ERROR && (
<Fragment>
<Grid item xs={12}>
<Icon color="error" fontSize="large" style={{ marginRight: 10 }}>
error
</Icon>
</Grid>
<Grid item xs={12}>
<Typography component="h3" variant="title">
{error}
</Typography>
<Typography component="h4" variant="subheading">
Please try again or <a href="#reset">reset your password</a>.
</Typography>
<Button
variant="contained"
color="secondary"
style={{ marginTop: 25, marginBottom: 50 }}
onClick={() =>
this.setState({ signInState: SIGN_IN_STATE_READY })
}>
Try again
</Button>
</Grid>
</Fragment>
)}
{signInState === SIGN_IN_STATE_SUCCESS && (
<Fragment>
<Grid item xs={12}>
<Icon
color="primary"
fontSize="large"
style={{ marginRight: 10 }}
>
check_circle_outline
</Icon>
</Grid>
<Grid item xs={12}>
<Typography component="h3" variant="title">
You have signed in successfully.
</Typography>
<Typography component="h4" variant="subheading">
You're on the way to managing your passwords.
</Typography>
</Grid>
</Fragment>
)}
Tip: Use TypeScript for enums. Instead of defining a set of constants such as this,
const SIGN_IN_STATE_READY = 'READY';
const SIGN_IN_STATE_IN_PROGRESS = 'IN_PROGRESS';
const SIGN_IN_STATE_ERROR = 'ERROR';
const SIGN_IN_STATE_SUCCESS = 'SUCCESS';
You can declare an enumeration such as this using TypeScript,
enum SignInState {
READY = 'READY',
IN_PROGRESS = 'IN_PROGRESS',
ERROR = 'ERROR',
SUCCESS = 'SUCCESS'
}
Create, Read, Update, Delete UI pattern (CRUD)
This pattern is also referred to as the master/detail pattern and you can use it, typically, whenever you need to do CRUD. This usually takes the form of a list or table that lists all existing entries, allows you to add new ones and edit and delete existing ones.
For example, our sample app, Azkaban, allows you to add a new password to a list of passwords. When adding a new password, you capture the application name, url, username and password, which will then be listed once saved. You can edit this password, change its details, and delete it if you donāt use the app any more.
Usually developers struggle with this because it deals with the transfer of information from one component to another. Part of the state pattern is also introduced in this problem, as there might be a natural tendency to have two separate components, one for creating a new record and one for editing an existing record.
We can use an element of the state pattern to represent the details component differently based on whether the user is creating a new record or editing an existing record.
To implement this in a way that takes into consideration all the caveats, and results in a set of components that is easy to maintain going forward, there are two questions to consider before implementing this pattern:
- Do I have all (or almost all) of the information about a record in memory from when it was listed?
- What is the likelihood that the set of information listed will, at some time in the future, be less than the information provided in the details screen?
Answering these two questions will allow you to decide whether you can pass an object from your list to the details screen, or whether you need to query all the details about a record in the details page.
Getting the record from master screen to the detail screen can be done in many ways, some of which are:
- Using a router - sending a record ID by way of a route or query parameter.
- Using component composition - binding an ID or object into a component using the components props or binding attributes.
- Using a state store such as Vuex or Redux with a router.
I wonāt go into the details of all the above approaches as that would take us beyond the scope of UI patterns and practices. I will only go into more details on the first way, which is to transfer information using a router.
Transferring information using a router
In our sample app, we use a router and send in the record ID on a route parameter. The password record is queried from the service using the ID. We can then perform updates on the record using the ID. Using Vue again, the following snippet demonstrates how we are using the router.
export default {
methods: {
addNewPassword() {
this.$router.push('/password');
},
editExistingPassword(id) {
this.$router.push(`/password/${id}`);
},
...
The key to implementing the master/detail pattern successfully is reusability. You might be tempted to implement different screens for adding, editing and viewing a record. Your requirements may force you to do this, but you should try to use a single screen to handle all three scenarios, as most often you will find that they differ only slightly.
For our sample app, we use isNew
to change the display and behaviour of the detail screen depending on whether we are adding or editing.
The code below highlights the methods that are used in details component and shows how the isNew
field is being calculated based on whether an ID parameter was transferred to the component. If it was transferred then isNew
is calculated as false, meaning that this is an existing record. If it was not transferred, then isNew
is calculated as true, meaning that we are creating a new record that doesnāt exist yet.
Youāll notice that the behaviour of the save method changes based on whether isNew
is true or false, and the text in the view also changes depending on whether isNew
is true or false.
...
methods: {
save() {
this.isSaving = true;
if (this.isNew) {
AzkabanService.addPassword(this.password).then(() => {
this.isSaving = false;
this.navigateToPasswords();
});
} else {
AzkabanService.updatePassword(this.getId(), this.password).then(() => {
this.isSaving = false;
this.navigateToPasswords();
});
}
},
...
getId() {
if (this.$route) {
return this.$route.params.id;
}
},
getIsNew() {
return this.getId() === undefined;
}
},
mounted() {
if (!this.isNew) {
this.password = AzkabanService.getPassword(this.getId());
}
},
data() {
return {
isNew: this.getIsNew(),
isSaving: false,
password: { category: 1 },
categories: AzkabanService.categories.filter(
category => category.id !== 0
)
};
}
...
<v-layout row justify-start>
<v-flex shrink mr-3>
<v-icon large>shield</v-icon>
</v-flex>
<v-flex>
<div class="title">{{ isNew ? 'Add new' : 'Edit' }} password</div>
<div class="subheading">
{{ isNew
? 'Add a new password for an app or website.'
: 'Change the details for your existing password.' }}
</div>
</v-flex>
</v-layout>
...
<v-flex xs12>
<div class="text-xs-right">
<v-btn @click="cancel">Cancel</v-btn>
<v-btn
@click="save"
:disabled="isSaving"
:loading="isSaving"
color="primary"
>{{ isNew ? 'Add' : 'Save'}}</v-btn>
</div>
</v-flex>
Since using a single convention for implementing this scenario, my team has been able to keep their code consistent and easier to maintain. We have also standardised a single way of moving information between components, for a specific scenario, and we have minimised the amount of redundant code that is produced. This has been super useful, as the code base has grown and evolved, to keep it understandable and a pleasure to work with, not to mention reliable.
David Purkiss has 11 years of experience in the software development industry. He has worked on everything from financial projects to mineral processing solutions. He specialises in facilitating the software development lifecycle across the stack, from the database and back-end to the front-end and DevOps and even to the processes and people. He has a keen sense of systems and products, as well as a holistic insight into technology-driven business.