In Part 1, we discussed the basics of atomic design and theming library, and how using them correctly can play a crucial role in product development, no matter what kind of project you are working on.
In this article, we will explore how we can use atoms to build molecules with basic functionality and event handling mechanisms.
We will use typography atoms of three different variants and create a list item molecule. We will also start looking into how to use data models for data coming from the back-end (or stored locally) to populate these values.
We will start adding padding, shadows, border radius, etc., but no specific styling.
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Box from "@material-ui/core/Box";
import Typography from "components/atoms/Typography";
const useStyle = makeStyles((theme) => ({
root: {
backgroundColor: theme.container.secondary.color,
borderRadius: theme.container.secondary.borderRadius,
padding: theme.container.secondary.padding,
},
}));
const EventListItemComponent = (props) => {
const { eventData } = props;
const style = useStyle();
return (
<React.Fragment>
<Box component={Container} boxShadow={2} className={style.root}>
<Typography variant="header">{eventData.date}</Typography>
<Typography variant="title">{eventData.title}</Typography>
<Typography variant="subtitle">{eventData.type}</Typography>
</Box>
</React.Fragment>
);
};
EventListItemComponent.propTypes = {
eventData: PropTypes.shape({
date: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
}),
};
EventListItemComponent.defaultProps = {};
export default EventListItemComponent;
EventListItemComponent.js hosted with ❤ by GitHub
import { createMuiTheme } from "@material-ui/core/styles";
import { grey } from "@material-ui/core/colors";
const defaultTheme = createMuiTheme();
const baseTheme = createMuiTheme({
container: {
secondary: {
color: grey[300],
borderRadius: defaultTheme.spacing(1),
padding: defaultTheme.spacing(2),
},
},
});
export default baseTheme;
Themes.js hosted with ❤ by GitHub
In the above code, we used a special component from Material User Interface (UI) called Box. This component acts as a wrapper to handle most of the (Cascading Style Sheet) CSS styling like BoxShadow as part of the theme.
Since the code from here on out is going to only get larger, we’re going to give the overview of only one organism. For the other organism, check out the full project here.
Add an event organism that has some input fields, buttons, and validation logic. The TextField component is updated to accommodate the date and time.
import React, { useCallback, useState } from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Box from "@material-ui/core/Box";
import Grid from "@material-ui/core/Grid";
import Typography from "components/atoms/Typography";
import TextField from "components/atoms/TextField";
import Button from "components/atoms/Button";
const useStyle = makeStyles((theme) => ({}));
const AddEventBanner = (props) => {
const validate = { props };
const style = useStyle();
const [fieldToValidate, setFieldValidation] = useState({
title: false,
date: false,
type: false,
});
const [inputFieldValue, setFieldValue] = useState({
title: "",
description: "",
date: "",
type: "",
});
const validateField = useCallback(
(field) => {
const input = inputFieldValue[field];
const currentValidationState = fieldToValidate;
if (input === null || input === "") {
currentValidationState[field] = true;
} else {
currentValidationState[field] = false;
}
setFieldValidation({ ...currentValidationState });
return !currentValidationState[field];
},
[inputFieldValue, fieldToValidate, setFieldValidation]
);
const isValid = useCallback(() => {
let validationStatus = true;
Object.keys(fieldToValidate).map((key, value) => {
validationStatus = validationStatus && validateField(key);
console.log(validationStatus);
});
return validationStatus;
}, [fieldToValidate, validateField]);
const handleFieldChange = useCallback(
(field, value) => {
const currentValidationState = fieldToValidate;
currentValidationState[field] = false;
setFieldValidation({ ...currentValidationState });
const currentFieldValues = inputFieldValue;
currentFieldValues[field] = value;
setFieldValue({ ...currentFieldValues });
},
[fieldToValidate, inputFieldValue, setFieldValidation, setFieldValue]
);
const handleSubmit = useCallback(() => {
if (!props.validate || (props.validate && isValid())) {
props.onSubmit(inputFieldValue);
}
}, [isValid]);
return (
<Box component={Container}>
<Grid container spacing={2}>
<Grid item>
<Typography variant="header">Add New Event</Typography>
</Grid>
<Grid item xs={12}>
<TextField
required
label="Title"
onChange={(event) => handleFieldChange("title", event.target.value)}
error={fieldToValidate.title}
onBlur={() => validate && validateField("title")}
/>
</Grid>
<Grid item xs={12}>
<TextField
label="Description"
onChange={(event) =>
handleFieldChange("description", event.target.value)
}
/>
</Grid>
<Grid item xs={6}>
<TextField
required
label="Date and Time"
onChange={(event) => handleFieldChange("date", event.target.value)}
type="datetime-local"
error={fieldToValidate.date}
onBlur={() => validate && validateField("date")}
/>
</Grid>
<Grid item xs={6}>
<TextField
required
label="Event Type"
onChange={(event) => handleFieldChange("type", event.target.value)}
error={fieldToValidate.type}
onBlur={() => validate && validateField("type")}
/>
</Grid>
<Grid item container xs={12} justify="center">
<Button title="Add Event" onClick={handleSubmit} />
</Grid>
</Grid>
</Box>
);
};
AddEventBanner.propTypes = {
validate: PropTypes.bool,
onSubmit: PropTypes.func,
};
AddEventBanner.defaultProps = {
validate: false,
};
export default AddEventBanner;
AddEvent.js hosted with ❤ by GitHub
Some basic validations and listeners, but no business logic yet. This is very important to create a boundary of what your component should do and how much it should control itself. For example, irrespective of where we add this component to the home screen or create a dedicated screen for events, it will be able to validate (or not validate) the required fields, and return the data to the main page for further processing. There are no network calls being made or any interactions with any data source.
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
const useStyle = makeStyles((theme) => ({
root: {
flexGrow: 1,
backgroundColor: "#f00",
},
paper: {
padding: theme.spacing(2),
},
container: {
height: "100vh",
},
sideNav: {},
banner: {},
content: {},
rightPanel: {},
}));
const HomeTemplate = (props) => {
const style = useStyle();
return (
<div className={style.root}>
<Paper className={style.paper}>
<Grid container className={style.container} spacing={2}>
<Grid item xs={2} className={style.sideNav}>
{props.sideNav}
</Grid>
<Grid item container xs alignContent="flex-start" spacing={2}>
<Grid item xs={12} className={style.banner}>
{props.banner}
</Grid>
<Grid item xs={12} className={style.content}>
{props.content}
</Grid>
</Grid>
<Grid item xs={3} className={style.rightPanel}>
{props.rightPanel}
</Grid>
</Grid>
</Paper>
</div>
);
};
HomeTemplate.propTypes = {
sideNav: PropTypes.element.isRequired,
rightPanel: PropTypes.element.isRequired,
banner: PropTypes.element.isRequired,
content: PropTypes.element.isRequired,
};
HomeTemplate.defaultProps = {};
export default HomeTemplate;
HomeTemplate.js hosted with ❤ by GitHub
While many developers jump into divs for building the UI, we use Grids. They allow flexibility and rigidness at the same time. We can build an amazing responsive UI with minimal effort.
import React, { useEffect, useCallback, useState } from "react";
import HomeTemplate from "components/templates/HomeTemplate";
import NextEventBanner from "components/organisms/NextEventBanner";
import EventList from "components/organisms/EventList";
import AddEvent from "components/organisms/AddEvent";
import { fetchEventData } from "services/EventDataService";
const DummyView = () => (
<div style={{ backgroundColor: "#adadad", height: "100vh" }}></div>
);
const HomePage = () => {
const [eventDataList, setEventDataList] = useState([]);
const fetchEventList = useCallback(async () => {
const eventData = await fetchEventData();
setEventDataList([...eventData]);
}, [setEventDataList]);
const handleAddEvent = useCallback((eventData) => {
console.log("New Event Added", eventData);
}, []);
useEffect(() => {
fetchEventList();
}, [fetchEventList]);
return (
<HomeTemplate
sideNav={<DummyView />}
banner={
<NextEventBanner
date={eventDataList.length > 0 ? eventDataList[0].date : ""}
title={eventDataList.length > 0 ? eventDataList[0].title : ""}
description={
eventDataList.length > 0 ? eventDataList[0].description : ""
}
type={eventDataList.length > 0 ? eventDataList[0].type : ""}
/>
}
rightPanel={
<EventList title="Upcoming Event" eventDataList={eventDataList} />
}
content={<AddEvent validate onSubmit={handleAddEvent} />}
/>
);
};
HomePage.propTypes = {};
HomePage.defaultProps = {};
export default HomePage;
HomePage.js hosted with ❤ by GitHub
Once all the setup is done, creating a page is really fast. Check out the code below.
In the above image, we are using a service to fetch and update the data.
import { fetchEventData } from "services/EventDataService";
const fetchEventList = useCallback(async () => {
const eventData = await fetchEventData();
setEventDataList([...eventData]);
}, [setEventDataList]);
Atomic design or not, this recommended practice to isolate data fetching, data processing, and data display keeps the app clean.
Let us pick some beautiful colors and set up the palette for the application. One best place to begin Theming the application is to use Google’s Material Color Tool
Here I picked the following
● Primary : cyan[200]
● Secondary: orange[500]
Material Color Tool offers a variety of previews and customizations to pick from.
Adding color to our application:
const baseTheme = createMuiTheme({
...
palette: {
primary: {
main: cyan[200],
},
secondary: {
main: orange[500],
},
},
});
typography: {
fontSize: 12,
},
When done right, the whole application’s appearance can be controlled from one file. Even if your application has 1000’s of pages, as long as you define some components, stick to a basic theme, and link it to a universal base theme: updates, upgrades, and modifications to your product appearance will be easy.
You can learn more about deploying the application using Firebase here.
Firebase: https://mohammed-atif.medium.com/deploy-react-application-to-firebase-using-github-actions-2c7514fba386
Let us make our page dark theme compatible
palette: {
type: "dark",
primary: {
main: cyan[200],
},
secondary: {
main: orange[500],
},
containerPrimary: {
main: cyan[200],
light: "#b4ffff",
dark: "#4bacb8",
},
containerSecondary: {
main: grey[400],
light: "#f5f5f5",
dark: "#373737",
},
},
That's it, just add type:dark, some minor tweaks and we are up and running.
An earlier version of this blog post was published by the author on Medium.
In Part 1, we discussed the basics of atomic design and theming library, and how using them correctly can play a crucial role in product development, no matter what kind of project you are working on.
In this article, we will explore how we can use atoms to build molecules with basic functionality and event handling mechanisms.
We will use typography atoms of three different variants and create a list item molecule. We will also start looking into how to use data models for data coming from the back-end (or stored locally) to populate these values.
We will start adding padding, shadows, border radius, etc., but no specific styling.
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Box from "@material-ui/core/Box";
import Typography from "components/atoms/Typography";
const useStyle = makeStyles((theme) => ({
root: {
backgroundColor: theme.container.secondary.color,
borderRadius: theme.container.secondary.borderRadius,
padding: theme.container.secondary.padding,
},
}));
const EventListItemComponent = (props) => {
const { eventData } = props;
const style = useStyle();
return (
<React.Fragment>
<Box component={Container} boxShadow={2} className={style.root}>
<Typography variant="header">{eventData.date}</Typography>
<Typography variant="title">{eventData.title}</Typography>
<Typography variant="subtitle">{eventData.type}</Typography>
</Box>
</React.Fragment>
);
};
EventListItemComponent.propTypes = {
eventData: PropTypes.shape({
date: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
}),
};
EventListItemComponent.defaultProps = {};
export default EventListItemComponent;
EventListItemComponent.js hosted with ❤ by GitHub
import { createMuiTheme } from "@material-ui/core/styles";
import { grey } from "@material-ui/core/colors";
const defaultTheme = createMuiTheme();
const baseTheme = createMuiTheme({
container: {
secondary: {
color: grey[300],
borderRadius: defaultTheme.spacing(1),
padding: defaultTheme.spacing(2),
},
},
});
export default baseTheme;
Themes.js hosted with ❤ by GitHub
In the above code, we used a special component from Material User Interface (UI) called Box. This component acts as a wrapper to handle most of the (Cascading Style Sheet) CSS styling like BoxShadow as part of the theme.
Since the code from here on out is going to only get larger, we’re going to give the overview of only one organism. For the other organism, check out the full project here.
Add an event organism that has some input fields, buttons, and validation logic. The TextField component is updated to accommodate the date and time.
import React, { useCallback, useState } from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Box from "@material-ui/core/Box";
import Grid from "@material-ui/core/Grid";
import Typography from "components/atoms/Typography";
import TextField from "components/atoms/TextField";
import Button from "components/atoms/Button";
const useStyle = makeStyles((theme) => ({}));
const AddEventBanner = (props) => {
const validate = { props };
const style = useStyle();
const [fieldToValidate, setFieldValidation] = useState({
title: false,
date: false,
type: false,
});
const [inputFieldValue, setFieldValue] = useState({
title: "",
description: "",
date: "",
type: "",
});
const validateField = useCallback(
(field) => {
const input = inputFieldValue[field];
const currentValidationState = fieldToValidate;
if (input === null || input === "") {
currentValidationState[field] = true;
} else {
currentValidationState[field] = false;
}
setFieldValidation({ ...currentValidationState });
return !currentValidationState[field];
},
[inputFieldValue, fieldToValidate, setFieldValidation]
);
const isValid = useCallback(() => {
let validationStatus = true;
Object.keys(fieldToValidate).map((key, value) => {
validationStatus = validationStatus && validateField(key);
console.log(validationStatus);
});
return validationStatus;
}, [fieldToValidate, validateField]);
const handleFieldChange = useCallback(
(field, value) => {
const currentValidationState = fieldToValidate;
currentValidationState[field] = false;
setFieldValidation({ ...currentValidationState });
const currentFieldValues = inputFieldValue;
currentFieldValues[field] = value;
setFieldValue({ ...currentFieldValues });
},
[fieldToValidate, inputFieldValue, setFieldValidation, setFieldValue]
);
const handleSubmit = useCallback(() => {
if (!props.validate || (props.validate && isValid())) {
props.onSubmit(inputFieldValue);
}
}, [isValid]);
return (
<Box component={Container}>
<Grid container spacing={2}>
<Grid item>
<Typography variant="header">Add New Event</Typography>
</Grid>
<Grid item xs={12}>
<TextField
required
label="Title"
onChange={(event) => handleFieldChange("title", event.target.value)}
error={fieldToValidate.title}
onBlur={() => validate && validateField("title")}
/>
</Grid>
<Grid item xs={12}>
<TextField
label="Description"
onChange={(event) =>
handleFieldChange("description", event.target.value)
}
/>
</Grid>
<Grid item xs={6}>
<TextField
required
label="Date and Time"
onChange={(event) => handleFieldChange("date", event.target.value)}
type="datetime-local"
error={fieldToValidate.date}
onBlur={() => validate && validateField("date")}
/>
</Grid>
<Grid item xs={6}>
<TextField
required
label="Event Type"
onChange={(event) => handleFieldChange("type", event.target.value)}
error={fieldToValidate.type}
onBlur={() => validate && validateField("type")}
/>
</Grid>
<Grid item container xs={12} justify="center">
<Button title="Add Event" onClick={handleSubmit} />
</Grid>
</Grid>
</Box>
);
};
AddEventBanner.propTypes = {
validate: PropTypes.bool,
onSubmit: PropTypes.func,
};
AddEventBanner.defaultProps = {
validate: false,
};
export default AddEventBanner;
AddEvent.js hosted with ❤ by GitHub
Some basic validations and listeners, but no business logic yet. This is very important to create a boundary of what your component should do and how much it should control itself. For example, irrespective of where we add this component to the home screen or create a dedicated screen for events, it will be able to validate (or not validate) the required fields, and return the data to the main page for further processing. There are no network calls being made or any interactions with any data source.
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
const useStyle = makeStyles((theme) => ({
root: {
flexGrow: 1,
backgroundColor: "#f00",
},
paper: {
padding: theme.spacing(2),
},
container: {
height: "100vh",
},
sideNav: {},
banner: {},
content: {},
rightPanel: {},
}));
const HomeTemplate = (props) => {
const style = useStyle();
return (
<div className={style.root}>
<Paper className={style.paper}>
<Grid container className={style.container} spacing={2}>
<Grid item xs={2} className={style.sideNav}>
{props.sideNav}
</Grid>
<Grid item container xs alignContent="flex-start" spacing={2}>
<Grid item xs={12} className={style.banner}>
{props.banner}
</Grid>
<Grid item xs={12} className={style.content}>
{props.content}
</Grid>
</Grid>
<Grid item xs={3} className={style.rightPanel}>
{props.rightPanel}
</Grid>
</Grid>
</Paper>
</div>
);
};
HomeTemplate.propTypes = {
sideNav: PropTypes.element.isRequired,
rightPanel: PropTypes.element.isRequired,
banner: PropTypes.element.isRequired,
content: PropTypes.element.isRequired,
};
HomeTemplate.defaultProps = {};
export default HomeTemplate;
HomeTemplate.js hosted with ❤ by GitHub
While many developers jump into divs for building the UI, we use Grids. They allow flexibility and rigidness at the same time. We can build an amazing responsive UI with minimal effort.
import React, { useEffect, useCallback, useState } from "react";
import HomeTemplate from "components/templates/HomeTemplate";
import NextEventBanner from "components/organisms/NextEventBanner";
import EventList from "components/organisms/EventList";
import AddEvent from "components/organisms/AddEvent";
import { fetchEventData } from "services/EventDataService";
const DummyView = () => (
<div style={{ backgroundColor: "#adadad", height: "100vh" }}></div>
);
const HomePage = () => {
const [eventDataList, setEventDataList] = useState([]);
const fetchEventList = useCallback(async () => {
const eventData = await fetchEventData();
setEventDataList([...eventData]);
}, [setEventDataList]);
const handleAddEvent = useCallback((eventData) => {
console.log("New Event Added", eventData);
}, []);
useEffect(() => {
fetchEventList();
}, [fetchEventList]);
return (
<HomeTemplate
sideNav={<DummyView />}
banner={
<NextEventBanner
date={eventDataList.length > 0 ? eventDataList[0].date : ""}
title={eventDataList.length > 0 ? eventDataList[0].title : ""}
description={
eventDataList.length > 0 ? eventDataList[0].description : ""
}
type={eventDataList.length > 0 ? eventDataList[0].type : ""}
/>
}
rightPanel={
<EventList title="Upcoming Event" eventDataList={eventDataList} />
}
content={<AddEvent validate onSubmit={handleAddEvent} />}
/>
);
};
HomePage.propTypes = {};
HomePage.defaultProps = {};
export default HomePage;
HomePage.js hosted with ❤ by GitHub
Once all the setup is done, creating a page is really fast. Check out the code below.
In the above image, we are using a service to fetch and update the data.
import { fetchEventData } from "services/EventDataService";
const fetchEventList = useCallback(async () => {
const eventData = await fetchEventData();
setEventDataList([...eventData]);
}, [setEventDataList]);
Atomic design or not, this recommended practice to isolate data fetching, data processing, and data display keeps the app clean.
Let us pick some beautiful colors and set up the palette for the application. One best place to begin Theming the application is to use Google’s Material Color Tool
Here I picked the following
● Primary : cyan[200]
● Secondary: orange[500]
Material Color Tool offers a variety of previews and customizations to pick from.
Adding color to our application:
const baseTheme = createMuiTheme({
...
palette: {
primary: {
main: cyan[200],
},
secondary: {
main: orange[500],
},
},
});
typography: {
fontSize: 12,
},
When done right, the whole application’s appearance can be controlled from one file. Even if your application has 1000’s of pages, as long as you define some components, stick to a basic theme, and link it to a universal base theme: updates, upgrades, and modifications to your product appearance will be easy.
You can learn more about deploying the application using Firebase here.
Firebase: https://mohammed-atif.medium.com/deploy-react-application-to-firebase-using-github-actions-2c7514fba386
Let us make our page dark theme compatible
palette: {
type: "dark",
primary: {
main: cyan[200],
},
secondary: {
main: orange[500],
},
containerPrimary: {
main: cyan[200],
light: "#b4ffff",
dark: "#4bacb8",
},
containerSecondary: {
main: grey[400],
light: "#f5f5f5",
dark: "#373737",
},
},
That's it, just add type:dark, some minor tweaks and we are up and running.
An earlier version of this blog post was published by the author on Medium.
In Part 1, we discussed the basics of atomic design and theming library, and how using them correctly can play a crucial role in product development, no matter what kind of project you are working on.
In this article, we will explore how we can use atoms to build molecules with basic functionality and event handling mechanisms.
We will use typography atoms of three different variants and create a list item molecule. We will also start looking into how to use data models for data coming from the back-end (or stored locally) to populate these values.
We will start adding padding, shadows, border radius, etc., but no specific styling.
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Box from "@material-ui/core/Box";
import Typography from "components/atoms/Typography";
const useStyle = makeStyles((theme) => ({
root: {
backgroundColor: theme.container.secondary.color,
borderRadius: theme.container.secondary.borderRadius,
padding: theme.container.secondary.padding,
},
}));
const EventListItemComponent = (props) => {
const { eventData } = props;
const style = useStyle();
return (
<React.Fragment>
<Box component={Container} boxShadow={2} className={style.root}>
<Typography variant="header">{eventData.date}</Typography>
<Typography variant="title">{eventData.title}</Typography>
<Typography variant="subtitle">{eventData.type}</Typography>
</Box>
</React.Fragment>
);
};
EventListItemComponent.propTypes = {
eventData: PropTypes.shape({
date: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
}),
};
EventListItemComponent.defaultProps = {};
export default EventListItemComponent;
EventListItemComponent.js hosted with ❤ by GitHub
import { createMuiTheme } from "@material-ui/core/styles";
import { grey } from "@material-ui/core/colors";
const defaultTheme = createMuiTheme();
const baseTheme = createMuiTheme({
container: {
secondary: {
color: grey[300],
borderRadius: defaultTheme.spacing(1),
padding: defaultTheme.spacing(2),
},
},
});
export default baseTheme;
Themes.js hosted with ❤ by GitHub
In the above code, we used a special component from Material User Interface (UI) called Box. This component acts as a wrapper to handle most of the (Cascading Style Sheet) CSS styling like BoxShadow as part of the theme.
Since the code from here on out is going to only get larger, we’re going to give the overview of only one organism. For the other organism, check out the full project here.
Add an event organism that has some input fields, buttons, and validation logic. The TextField component is updated to accommodate the date and time.
import React, { useCallback, useState } from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Box from "@material-ui/core/Box";
import Grid from "@material-ui/core/Grid";
import Typography from "components/atoms/Typography";
import TextField from "components/atoms/TextField";
import Button from "components/atoms/Button";
const useStyle = makeStyles((theme) => ({}));
const AddEventBanner = (props) => {
const validate = { props };
const style = useStyle();
const [fieldToValidate, setFieldValidation] = useState({
title: false,
date: false,
type: false,
});
const [inputFieldValue, setFieldValue] = useState({
title: "",
description: "",
date: "",
type: "",
});
const validateField = useCallback(
(field) => {
const input = inputFieldValue[field];
const currentValidationState = fieldToValidate;
if (input === null || input === "") {
currentValidationState[field] = true;
} else {
currentValidationState[field] = false;
}
setFieldValidation({ ...currentValidationState });
return !currentValidationState[field];
},
[inputFieldValue, fieldToValidate, setFieldValidation]
);
const isValid = useCallback(() => {
let validationStatus = true;
Object.keys(fieldToValidate).map((key, value) => {
validationStatus = validationStatus && validateField(key);
console.log(validationStatus);
});
return validationStatus;
}, [fieldToValidate, validateField]);
const handleFieldChange = useCallback(
(field, value) => {
const currentValidationState = fieldToValidate;
currentValidationState[field] = false;
setFieldValidation({ ...currentValidationState });
const currentFieldValues = inputFieldValue;
currentFieldValues[field] = value;
setFieldValue({ ...currentFieldValues });
},
[fieldToValidate, inputFieldValue, setFieldValidation, setFieldValue]
);
const handleSubmit = useCallback(() => {
if (!props.validate || (props.validate && isValid())) {
props.onSubmit(inputFieldValue);
}
}, [isValid]);
return (
<Box component={Container}>
<Grid container spacing={2}>
<Grid item>
<Typography variant="header">Add New Event</Typography>
</Grid>
<Grid item xs={12}>
<TextField
required
label="Title"
onChange={(event) => handleFieldChange("title", event.target.value)}
error={fieldToValidate.title}
onBlur={() => validate && validateField("title")}
/>
</Grid>
<Grid item xs={12}>
<TextField
label="Description"
onChange={(event) =>
handleFieldChange("description", event.target.value)
}
/>
</Grid>
<Grid item xs={6}>
<TextField
required
label="Date and Time"
onChange={(event) => handleFieldChange("date", event.target.value)}
type="datetime-local"
error={fieldToValidate.date}
onBlur={() => validate && validateField("date")}
/>
</Grid>
<Grid item xs={6}>
<TextField
required
label="Event Type"
onChange={(event) => handleFieldChange("type", event.target.value)}
error={fieldToValidate.type}
onBlur={() => validate && validateField("type")}
/>
</Grid>
<Grid item container xs={12} justify="center">
<Button title="Add Event" onClick={handleSubmit} />
</Grid>
</Grid>
</Box>
);
};
AddEventBanner.propTypes = {
validate: PropTypes.bool,
onSubmit: PropTypes.func,
};
AddEventBanner.defaultProps = {
validate: false,
};
export default AddEventBanner;
AddEvent.js hosted with ❤ by GitHub
Some basic validations and listeners, but no business logic yet. This is very important to create a boundary of what your component should do and how much it should control itself. For example, irrespective of where we add this component to the home screen or create a dedicated screen for events, it will be able to validate (or not validate) the required fields, and return the data to the main page for further processing. There are no network calls being made or any interactions with any data source.
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
const useStyle = makeStyles((theme) => ({
root: {
flexGrow: 1,
backgroundColor: "#f00",
},
paper: {
padding: theme.spacing(2),
},
container: {
height: "100vh",
},
sideNav: {},
banner: {},
content: {},
rightPanel: {},
}));
const HomeTemplate = (props) => {
const style = useStyle();
return (
<div className={style.root}>
<Paper className={style.paper}>
<Grid container className={style.container} spacing={2}>
<Grid item xs={2} className={style.sideNav}>
{props.sideNav}
</Grid>
<Grid item container xs alignContent="flex-start" spacing={2}>
<Grid item xs={12} className={style.banner}>
{props.banner}
</Grid>
<Grid item xs={12} className={style.content}>
{props.content}
</Grid>
</Grid>
<Grid item xs={3} className={style.rightPanel}>
{props.rightPanel}
</Grid>
</Grid>
</Paper>
</div>
);
};
HomeTemplate.propTypes = {
sideNav: PropTypes.element.isRequired,
rightPanel: PropTypes.element.isRequired,
banner: PropTypes.element.isRequired,
content: PropTypes.element.isRequired,
};
HomeTemplate.defaultProps = {};
export default HomeTemplate;
HomeTemplate.js hosted with ❤ by GitHub
While many developers jump into divs for building the UI, we use Grids. They allow flexibility and rigidness at the same time. We can build an amazing responsive UI with minimal effort.
import React, { useEffect, useCallback, useState } from "react";
import HomeTemplate from "components/templates/HomeTemplate";
import NextEventBanner from "components/organisms/NextEventBanner";
import EventList from "components/organisms/EventList";
import AddEvent from "components/organisms/AddEvent";
import { fetchEventData } from "services/EventDataService";
const DummyView = () => (
<div style={{ backgroundColor: "#adadad", height: "100vh" }}></div>
);
const HomePage = () => {
const [eventDataList, setEventDataList] = useState([]);
const fetchEventList = useCallback(async () => {
const eventData = await fetchEventData();
setEventDataList([...eventData]);
}, [setEventDataList]);
const handleAddEvent = useCallback((eventData) => {
console.log("New Event Added", eventData);
}, []);
useEffect(() => {
fetchEventList();
}, [fetchEventList]);
return (
<HomeTemplate
sideNav={<DummyView />}
banner={
<NextEventBanner
date={eventDataList.length > 0 ? eventDataList[0].date : ""}
title={eventDataList.length > 0 ? eventDataList[0].title : ""}
description={
eventDataList.length > 0 ? eventDataList[0].description : ""
}
type={eventDataList.length > 0 ? eventDataList[0].type : ""}
/>
}
rightPanel={
<EventList title="Upcoming Event" eventDataList={eventDataList} />
}
content={<AddEvent validate onSubmit={handleAddEvent} />}
/>
);
};
HomePage.propTypes = {};
HomePage.defaultProps = {};
export default HomePage;
HomePage.js hosted with ❤ by GitHub
Once all the setup is done, creating a page is really fast. Check out the code below.
In the above image, we are using a service to fetch and update the data.
import { fetchEventData } from "services/EventDataService";
const fetchEventList = useCallback(async () => {
const eventData = await fetchEventData();
setEventDataList([...eventData]);
}, [setEventDataList]);
Atomic design or not, this recommended practice to isolate data fetching, data processing, and data display keeps the app clean.
Let us pick some beautiful colors and set up the palette for the application. One best place to begin Theming the application is to use Google’s Material Color Tool
Here I picked the following
● Primary : cyan[200]
● Secondary: orange[500]
Material Color Tool offers a variety of previews and customizations to pick from.
Adding color to our application:
const baseTheme = createMuiTheme({
...
palette: {
primary: {
main: cyan[200],
},
secondary: {
main: orange[500],
},
},
});
typography: {
fontSize: 12,
},
When done right, the whole application’s appearance can be controlled from one file. Even if your application has 1000’s of pages, as long as you define some components, stick to a basic theme, and link it to a universal base theme: updates, upgrades, and modifications to your product appearance will be easy.
You can learn more about deploying the application using Firebase here.
Firebase: https://mohammed-atif.medium.com/deploy-react-application-to-firebase-using-github-actions-2c7514fba386
Let us make our page dark theme compatible
palette: {
type: "dark",
primary: {
main: cyan[200],
},
secondary: {
main: orange[500],
},
containerPrimary: {
main: cyan[200],
light: "#b4ffff",
dark: "#4bacb8",
},
containerSecondary: {
main: grey[400],
light: "#f5f5f5",
dark: "#373737",
},
},
That's it, just add type:dark, some minor tweaks and we are up and running.
An earlier version of this blog post was published by the author on Medium.