Objective
Create a PasswordInput
component that can be used to determine a password's strength.
Description
We're all quite familiar with password input fields on the web, right? They are meant to hold...well..passwords, which are perhaps one of the most sensitive pieces of digital information.
But let's not get into so much digital detail and keep the discussion rudimentary.
Let's suppose we have a password field along with a button that reads 'Show':
When this button is clicked, the password's literal characters are displayed, with the button's own text replaced with 'Hide':
(By the way, having such simple passwords as 'Hello123?' is seriously problematic. Always choose passwords with a reasonably high entropy!)
When the button is clicked again, the previous configuration is restored.
In other words, the button is basically just show/hide toggle for the password.
Besides this, also suppose that there is a strength indicator bar beneath the password field to indicate the strength level of the password, based on a set criteria, along with a piece of text to explain that strength level.
This criteria is described as follows, assuming that the password isn't empty (i.e. not an empty string):
- The password has at least one lowercase letter (a - z).
- The password has at least one uppercase letter (A - Z).
- The password has at least one digit (0 - 9).
- The password has at least one symbol (a character none of the above).
- The length of the password is greater than or equal to 13.
These five requirements each increase the password's strength level by 1. The level begins at 0, where the password is empty (i.e. it's an empty string).
For instance, if a given password has one lowercase letter and one digit, its level would be 2. Similarly, if a given password is 18 characters long and has 2 symbols in it with the rest of the characters being lowercase letters, its level would be 3.
Each level has an explanatory text associated with it, as follows:
- '-' (Level 0)
- 'Weak' (Level 1)
- 'Average' (Level 2)
- 'Good' (Level 3)
- 'Very Good' (Level 4)
- 'Ultimate' (Level 5)
Starting from level 1, each subsequent level serves to fill up as many strength bar segments (shown above) as the level number, with a color associated with that level.
Here's an example of a password with level 1 strength:
Similarly, here's an example of a password with a level 3 strength:
To give you a headstart on them, following are the colors (in CSS) you could use for each of the levels from 1 onwards, in the given order: red
, orange
, greenyellow
, green
, purple
. For level 0, no segment is filled likewise no special color is needed.
As data is input into the password field, the entered password is checked against the aforementioned criteria, its strength determined — the level and its corresponding text — and this information displayed in situ in the respective location.
Now, let's get to the crux of the exercise.
In this exercise, you have to create a PasswordInput
component that works as detailed below.
An optional, Boolean prop interactive
specifies whether the password input should contain a strength indicator along with it or not. By default, interactive
is false
, hence there is no strength indicator.
Besides this, a value
prop specifies the initial value to put inside the password field to begin with.
Note that the entire set of elements representing the password field, the show/hide button, strength bars, etc. MUST be wrapped inside a <div>
block.
Shown below are two instances of using PasswordInput
.
First, with the interactive
attribute provided, and a given value
to begin with:
function App() {
return (
<PasswordInput value="Hello123?" interactive />
);
}
Now, without interactive
:
function App() {
return (
<PasswordInput value="Hello123?" />
);
}
Note that when interactive
is falsey, it means that the interactive nature of the password field, in live-presenting a strength indicator, should be ceased completely. That is, the entered password MUST NOT be tested against the given criteria at all.
You're free to approach the logic of filling up the individual strength bar segments as you wish. You're also free to use CSS any way you like.
The main point of the exercise is to get the overall logic and strength indication right — exactly how the indication is presented is all up to your imagination.
New file
Inside the directory you created for this course on React, create a new folder called Exercise-14-Password-Strength and put the .js solution files for this exercise within it.
Solution
First, let's get done with the general structure of PasswordInput
before proceeding forward.
By this point in this course, we should be more than comfortable with setting up basic, controlled input fields.
Here's the code:
import { useState } from 'react';
function PasswordInput({
value: initialValue = '',
interactive = false
}) {
const [value, setValue] = useState(initialValue);
return (
<div>
<input type="password" value={value} onChange={e => setValue(e.target.value)} />
<button>Show</button>
</div>
);
}
Perfect! Let's now dive into the real business.
There are essentially two parts to implementing the PasswordInput
component:
- The password show/hide toggler.
- The strength indicator, to be shown only when the
interactive
prop istrue
.
We'll begin with the first part which is fairly easy to code.
Start off by laying out an <input>
field whose type
is determined conditionally, depending on the state value shown
. This state value is initially false
(the password's characters are initially hidden), and is udpated — or better to say, toggled — by the given button.
Following is the code we get in the end:
import { useState } from 'react';
function PasswordInput({
value: initialValue = '',
interactive = false
}) {
const [value, setValue] = useState(initialValue);
const [shown, setShown] = useState(false);
return (
<div>
<input type={shown ? 'text' : 'password'} value={value} onChange={e => setValue(e.target.value)} />
<button>{shown ? 'Hide' : 'Show'}</button>
</div>
);
}
Now over to implementing the second part, which is slightly more involved to implement.
But before we code, let's spare a few minutes in thinking about how to approach creating the strength indicator.
The five strength level segments could easily be created by mapping over a dummy 5-element array, to prevent repetition and make the segment-filling logic a little bit easier (we'll see how).
The entire <div>
element containing these segments could be called .pwd-strength
, with an additional modifier class added to it to denote a particular level; let's call this .pwd-strength--{level}
, where {level}
is the strength level.
In that respect, each strength level segment therein could be called .pwd-strength_bar
(being a <span>
element). When a segment is filled, it would have a child (<span>
) element in it called .pwd-strength_bar_filled
.
In the following CSS code, we implement the styles of all these classes:
.pwd-strength_bar,
.pwd-strength_bar_filled {
background-color: lightgrey;
width: 20px;
height: 4px;
}
.pwd-strength_bar {
margin-right: 3px;
display: inline-block;
}
.pwd-strength_bar_filled {
margin-right: 0;
display: block;
}
.pwd-strength--1 .pwd-strength_bar_filled {
background-color: red;
}
.pwd-strength--2 .pwd-strength_bar_filled {
background-color: orange;
}
.pwd-strength--3 .pwd-strength_bar_filled {
background-color: greenyellow;
}
.pwd-strength--4 .pwd-strength_bar_filled {
background-color: green;
}
.pwd-strength--5 .pwd-strength_bar_filled {
background-color: purple;
}
This completes our thought process for the design and structure of the strength indicator; we still have to crack its logic.
That is, how do we determine the strength level and its corresponding text?
Well, let's think, think and think... After all, programming is mainly thinking and less so coding.
The solution that we come up with is to employ a custom hook to compute all the necessary information of a given password value, i.e. its strength level (as a number) and the corresponding text (as a string), and then return that information.
Let's call this hook useStrength()
.
Its first argument would be the interactive
prop (to determine whether we even need to process the given password value for its strength or not) while its second argument would be value
.
To keep the discussion short, let's first see the definition of useStrength()
and then reason about the code:
function useStrength(interactive, value) {
if (!interactive) {
return [];
}
const strengthText = ['-', 'Weak', 'Average', 'Good', 'Very good', 'Ultimate'];
const patterns = [/[a-z]/, /[A-Z]/, /\d/, /[^a-zA-Z0-9]/, /.{15,}/];
let level = 0;
for (var pattern of patterns) {
if (pattern.test(value)) {
level++;
}
}
return [level, strengthText[level]];
}
The main points to note here are as follows:
- When
interactive
isfalse
, no further processing is done; the function immediately returns an empty array. An array is returned because the return value ofuseStrength()
is destructured inPasswordInput
— if we don't return an array above, the destructuring operation would fail. strengthText
provides an array of strings representing the text that corresponds to each strength level (as mentioned in the description above).patterns
provides an array of regular expressions to use to test for each of the given criteria (as mentioned in the description above).level
specifies the strength level, starting at0
.- A
for...of
loop iterates over each pattern inpatterns
, and tests the given password,value
, against it. If the pattern is found,level
is incremented.
With this custom hook made, now we just ought to go back to our PasswordInput
component and code it to completion, for we now have all the information we need to complete the component.
Consider the following code:
import { useState } from 'react';
function useStrength(interactive, value) { /* ... */ }
function PasswordInput({
value: initialValue = '',
interactive = false
}) {
const [value, setValue] = useState(initialValue);
const [shown, setShown] = useState(false);
const [level, text] = useStrength(interactive, value);
return (
<div>
<input type={shown ? 'text' : 'password'} value={value} onChange={e => setValue(e.target.value)} />
<button>{shown ? 'Hide' : 'Show'}</button>
{interactive && (
<div className={'pwd-strength pwd-strength--' + level}>
{new Array(5).fill(0).map((_, i) => (
<span className="pwd-strength_bar">
{i <= (level - 1) && <span className="pwd-strength_bar_filled"></span>}
</span>
))}
{text}
</div>
)}
</div>
);
}
Let's quickly see what's happening here:
- The entire password strength indicator section is conditionally rendered, when
interactive
istrue
. new Array(5).fill(0)
creates a dummy array of five elements. This is done purely to allow us to usemap()
to produce a collection of five<span>
elements, corresponding to the five strength level segments.i <= (level - 1)
decides whether the current segment should be filled. The filling is performed by adding a.pwd-strength_bar_filled
element (which is also a<span>
) inside the current segment.- Right after this sequence of 5 segments, we render
text
(recall that it holds the text corresponding to the current strength level of the password).
And this is pretty much everything we need in order to completely and successfully implement PasswordInput
.
No doubt that this exercise was quite an involved one but it was surely worth the effort. We got to learn a diverse set of concepts, including the crucially important concept of custom hooks in React.
Custom hooks is a really powerful pattern — in fact, a desirable pattern — to use in React apps. Many React libraries heavily use custom hooks to simplify complex logic and to provide developers with a wide range of functionality to hook their components into.