So far our app can display the notes given in the dashboard component’s state.
Let’s implement a UI for adding notes.
Material-UI also provides icons. Icons are present in the @material-ui/icons package. Install it via npm using this command.
npm install @material-ui/icons --save
Let’s implement the UI for adding notes.
../src/components/Dashboard.js
import React, { Component } from 'react';import Container from './Container';import Grid from '@material-ui/core/Grid';import AppBar from '@material-ui/core/AppBar';import Toolbar from '@material-ui/core/Toolbar';import Button from '@material-ui/core/Button';import AddIcon from '@material-ui/icons/Add';import Dialog from '@material-ui/core/Dialog';import DialogActions from '@material-ui/core/DialogActions';import DialogContent from '@material-ui/core/DialogContent';import DialogContentText from '@material-ui/core/DialogContentText';import DialogTitle from '@material-ui/core/DialogTitle';import Slide from '@material-ui/core/Slide';import TextField from '@material-ui/core/TextField';import PropTypes from 'prop-types';import { withStyles } from '@material-ui/core/styles';function Transition(props) {return <Slide direction="up" {...props} />;}const styles = theme => ({textField: {marginLeft: theme.spacing.unit,marginRight: theme.spacing.unit,width: 500,overflow: 'hidden',},});class Dashboard extends Component {constructor(props) {super(props);this.state = {notes: [{title: 'Hello, World',timestamp: '11 June 2018',content: "This is your first note",},{title: 'Hey There',timestamp: '22 May 2016',content: "This is your second note",},],open: false,};}addNotetoDB = () => {}handleClickOpen = () => {this.setState({open: true,});};handleClose = () => {this.setState({open: false,title: '',content: '',});};render () {const { classes } = this.props;const AddButtonStyle = {position: 'fixed',right: '4%',bottom: '6%',};return (<div className="Dashboard"><Grid container spacing={0}><Grid item xs={12} className="NavBar"><AppBar className="AppBar" position="fixed"><Toolbar><h1>My Notes</h1></Toolbar></AppBar></Grid><br /><br /><br /><Grid item xs={12} className="Notes" style={{ marginTop:'20px' }}>{ this.state.notes.map(note => <Container note={ note } /> )}<br /><br /></Grid></Grid><Button className='AddButton' onClick={this.handleClickOpen} style={ AddButtonStyle } variant="fab" color="primary" aria-label="Add"><AddIcon /></Button><Dialogopen={this.state.open}TransitionComponent={Transition}keepMountedonClose={this.handleClose}aria-labelledby="alert-dialog-slide-title"aria-describedby="alert-dialog-slide-description"><DialogTitle id="alert-dialog-slide-title"><TextFieldid="title"label="Title"placeholder="Note Title"className={classes.textField}onChange={this.handleChange('title')value={this.state.title}margin="normal"/></DialogTitle><DialogContent><DialogContentText id="alert-dialog-slide-description"><TextFieldid="content"label="Content"placeholder="Add your note here"multilinevalue={this.state.content}onChange={this.handleChange('content')}className={classes.textField}margin="normal"/></DialogContentText></DialogContent><DialogActions><Button onClick={this.handleClose} color="secondary">Cancel</Button><Button onClick={this.addNotetoDB} color="primary">Add</Button></DialogActions></Dialog></div>);}}Dashboard.propTypes = {classes: PropTypes.object.isRequired,};export default withStyles(styles)(Dashboard);
After the change in dashboard component, our UI looks like this
Dashboard:
When you click on the add button at the bottom:
For our UI, we need the following components from Material-UI
- Add Button: This is implemented by using Floating Action Buttons. Attributes: onClick: the specified by onClick is called whenever this button is clicked by the user. Here the handleClickOpen function is called which changes the value of open inside of state to true which triggers the dialog box to open which we will be covering soon. style: this props is used to make changes to the default material-UI components.
- Dialog: This is implemented using the Slide in Alert Dialog.This component requires DIalogActions, DialogContent, DialogContentText, DialogTitle to format the dialog UI and Slide to add the slide-in feature. A function Transition which will be passed to the Dialog component’s Transition Component props to add the sliding action when the dialog box opens. The opening and closing of the dialog box is determined by the bool open inside of the component’s state. When the user clicks the + button, it calls the handleOpen function which triggers the dialog box to open. The handleClose function triggers the dialog box to close.
- Text Field: The textfield is where the user enters the text for title and content. One of the two text fields is a multiline textfield. When the textfield values change, a function handleChange is called which sets the state values for title and content.
The functions handleChange, handleOpen, handleClose are Javascript’s ES6 functions.
We have added a styles function which takes in themes and outputs an object. This object is used to modify the default theme of the MaterialUI components.
withStyles links the styles object with a component. It does not modify the component passed to it. Instead, it returns a new component with a classes property. This classes object contains the name of the class names injected in the DOM. To ensure that props is passed to Dashboard component, we use PropTypes. So, if Dashboard component is mounted without classes prop, React raises an error.
withStyles(styles)(Dashboard) is actually a function which calls another function. withStyles takes in the styles object and returns a function which in turn returns a modified React component with the styles applied.
Now let’s add the functionality for adding notes to the state of the component. We have created an addNote function which gets invoked when the user clicks on the add button in the note dialog box. The addNote function generates the current date. It fetches the title and content from the component’s state and creates a note object. It then appends the note object to the notes object in the component’s state. In the next section we will be passing the note object to the firebase database.
import React, { Component } from 'react';import Container from './Container';import Grid from '@material-ui/core/Grid';import AppBar from '@material-ui/core/AppBar';import Toolbar from '@material-ui/core/Toolbar';import Button from '@material-ui/core/Button';import AddIcon from '@material-ui/icons/Add';import Dialog from '@material-ui/core/Dialog';import DialogActions from '@material-ui/core/DialogActions';import DialogContent from '@material-ui/core/DialogContent';import DialogContentText from '@material-ui/core/DialogContentText';import DialogTitle from '@material-ui/core/DialogTitle';import Slide from '@material-ui/core/Slide';import TextField from '@material-ui/core/TextField';import PropTypes from 'prop-types';import { withStyles } from '@material-ui/core/styles';function Transition(props) {return <Slide direction="up" {...props} />;}const styles = theme => ({textField: {marginLeft: theme.spacing.unit,marginRight: theme.spacing.unit,width: 500,overflow: 'hidden',},});class Dashboard extends Component {constructor(props) {super(props);this.state = {notes: [{title: 'Hello, World',timestamp: '11 June 2018',content: "This is your first note",},{title: 'Hey There',timestamp: '22 May 2016',content: "This is your second note",},],open: false,title: null,content: null,};}addNote = () => {var date = new Date();var timestamp = date.getDate() + '/' + (date.getMonth() + 1) + ' | ' + date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();console.log(timestamp);var note = {};note['title'] = this.state.title;note['timestamp'] = timestamp;note['content'] = this.state.content;this.state.notes.push(note);console.log(this.state);this.handleClose();}handleClickOpen = () => {this.setState({open: true,});};handleClose = () => {this.setState({open: false,title: '',content: '',});};handleChange = name => event => {this.setState({[name]: event.target.value,});};render () {const { classes } = this.props;const AddButtonStyle = {position: 'fixed',right: '4%',bottom: '6%',};return (<div className="Dashboard"><Grid container spacing={0}><Grid item xs={12} className="NavBar"><AppBar className="AppBar" position="fixed"><Toolbar><h1>My Notes</h1></Toolbar></AppBar></Grid><br /><br /><br /><Grid item xs={12} className="Notes" style={{ marginTop:'20px' }}>{ this.state.notes.map(note => <Container note={ note } /> )}<br /><br /></Grid></Grid><Button className='AddButton' onClick={this.handleClickOpen} style={ AddButtonStyle } variant="fab" color="primary" aria-label="Add"><AddIcon /></Button><Dialogopen={this.state.open}TransitionComponent={Transition}keepMountedonClose={this.handleClose}aria-labelledby="alert-dialog-slide-title"aria-describedby="alert-dialog-slide-description"><DialogTitle id="alert-dialog-slide-title"><TextFieldid="itle"label="Title"placeholder="Note Title"className={classes.textField}value={this.state.title}onChange={this.handleChange('title')}margin="normal"/></DialogTitle><DialogContent><DialogContentText id="alert-dialog-slide-description"><TextFieldid="content"label="Content"placeholder="Add your note here"multilinevalue={this.state.content}className={classes.textField}onChange={this.handleChange('content')}margin="normal"/></DialogContentText></DialogContent><DialogActions><Button onClick={this.handleClose} color="secondary">Cancel</Button><Button onClick={this.addNote} color="primary">Add</Button></DialogActions></Dialog></div>);}}Dashboard.propTypes = {classes: PropTypes.object.isRequired,};export default withStyles(styles)(Dashboard);
Since we have the function to add notes, let’s remove the default notes from the component’s state.
Our React app can now store notes inside its state. The notes will disappear if you close or refresh the tab. So we need a way to store these notes in the cloud. For that, we will use the Firebase realtime database.
Add notes to Firebase Realtime Database
Let’s create a function to add our notes to the Firebase database.
In the ../src/firebase/firebase.js file, we create the following function
export const addToDB = (note) => {const NoteRef = firebase.database().ref('notes/').push({title: note.title,timestamp: note.timestamp,content : note.content});return NoteRef.key;}
export keyword is used to export the function so other files could import this function. The function firebase.database().ref('notes/').push() is used to push an object to the database at the node pointed to by the string in the ref() function.
The ref function points to our main node in the database. We create a new field ‘notes’ inside of our main node. Then we push our object in the database using the push() function.
After we push the object into the notes/ node in our database, the database returns the key for the object stored in the database. Firebase stores all the objects under a unique key. We will use this key as to uniquely identify each note in our database.
Remove notes from Firebase Realtime Database
export const removeFromDB = (noteKey) => {firebase.database().ref('notes/' + noteKey).remove();}
To remove the note stored in the database we remove the field with key noteKey. This traverses to the field using the ref function and removes the field using the remove function.
For all the read-write functions provided by Firebase, refer to: Firebase Realtime Database Read-Write
In dashboard.js, we pass the removeNote function to the Container component as removeNote function.
../src/components/Dashboard.js
import React, { Component } from 'react';import Container from './Container';import Grid from '@material-ui/core/Grid';import AppBar from '@material-ui/core/AppBar';import Toolbar from '@material-ui/core/Toolbar';import Button from '@material-ui/core/Button';import AddIcon from '@material-ui/icons/Add';import Dialog from '@material-ui/core/Dialog';import DialogActions from '@material-ui/core/DialogActions';import DialogContent from '@material-ui/core/DialogContent';import DialogContentText from '@material-ui/core/DialogContentText';import DialogTitle from '@material-ui/core/DialogTitle';import Slide from '@material-ui/core/Slide';import TextField from '@material-ui/core/TextField';import PropTypes from 'prop-types';import { withStyles } from '@material-ui/core/styles';import { addToDB, removeFromDB } from '../firebase/firebase';function Transition(props) {return <Slide direction="up" {...props} />;}const styles = theme => ({textField: {marginLeft: theme.spacing.unit,marginRight: theme.spacing.unit,width: 500,overflow: 'hidden',},});class Dashboard extends Component {constructor(props) {super(props);this.state = {notes: [],open: false,title: null,content: null,};}addNote = () => {var date = new Date();var timestamp = date.getDate() + '/' + (date.getMonth() + 1) + ' | ' + date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();console.log(timestamp);var note = {};note['title'] = this.state.title;note['timestamp'] = timestamp;note['content'] = this.state.content;var key = addToDB(note);note['key'] = key;var notes = this.state.notes;notes.push(note);this.setState({notes: notes,})this.handleClose();}removeNote = (noteKey) => {var notes = this.state.notes;console.log(noteKey);for(var i = 0; i < Object.keys(notes).length; ++i) {if(notes[i].key === noteKey) {notes.splice(i, 1);removeFromDB(noteKey);this.setState({notes: notes,});return;}}}handleClickOpen = () => {this.setState({open: true,});};handleClose = () => {this.setState({open: false,title: '',content: '',});};handleChange = name => event => {this.setState({[name]: event.target.value,});};render () {const { classes } = this.props;const AddButtonStyle = {position: 'fixed',right: '4%',bottom: '6%',};return (<div className="Dashboard"><Grid container spacing={0}><Grid item xs={12} className="NavBar"><AppBar className="AppBar" position="fixed"><Toolbar><h1>My Notes</h1></Toolbar></AppBar></Grid><br /><br /><br /><Grid item xs={12} className="Notes" style={{ marginTop:'20px' }}>{ this.state.notes.map(note => <Container removeNote={ this.removeNote }note={ note } /> )}<br /><br /></Grid></Grid><Button className='AddButton' onClick={this.handleClickOpen} style={ AddButtonStyle } variant="fab" color="primary" aria-label="Add"><AddIcon /></Button><Dialogopen={this.state.open}TransitionComponent={Transition}keepMountedonClose={this.handleClose}aria-labelledby="alert-dialog-slide-title"aria-describedby="alert-dialog-slide-description"><DialogTitle id="alert-dialog-slide-title"><TextFieldid="itle"label="Title"placeholder="Note Title"className={classes.textField}value={this.state.title}onChange={this.handleChange('title')}margin="normal"/></DialogTitle><DialogContent><DialogContentText id="alert-dialog-slide-description"><TextFieldid="content"label="Content"placeholder="Add your note here"multilinevalue={this.state.content}className={classes.textField}onChange={this.handleChange('content')}margin="normal"/></DialogContentText></DialogContent><DialogActions><Button onClick={this.handleClose} color="secondary">Cancel</Button><Button onClick={this.addNote} color="primary">Add</Button></DialogActions></Dialog></div>);}}Dashboard.propTypes = {classes: PropTypes.object.isRequired,};export default withStyles(styles)(Dashboard);
../src/firebase/firebase.js
import firebase from 'firebase';var config = {apiKey: "AIzaSyA2Ax7WVCKilzq-iCqlswOr90z8FKW4AFU",authDomain: "note-app-89312.firebaseapp.com",databaseURL: "https://note-app-89312.firebaseio.com",projectId: "note-app-89312",storageBucket: "note-app-89312.appspot.com",messagingSenderId: "1073394984097"};firebase.initializeApp(config);export const addToDB = (note) => {const NoteRef = firebase.database().ref('notes/').push({title: note.title,timestamp: note.timestamp,content : note.content});return NoteRef.key;}export const removeFromDB = (noteKey) => {firebase.database().ref('notes/' + noteKey).remove();}export default firebase;
../src/components/Container.js
import React, { Component } from 'react';import '../css/Container.css';import Paper from '@material-ui/core/Paper';import DeleteForeverSharpIcon from '@material-ui/icons/DeleteForeverSharp';class Container extends Component {render() {return(<div className="Note"><Paper className="NotePaper" style={{ background:'lightgreen'}}><br /><span id='title' > { this.props.note.title } </span> <span id="delete"> <DeleteForeverSharpIcon onClick={ () => this.props.removeNote(this.props.note.key) } style={{fontSize: '50px'}}/> </span> <br /><span id='timestamp' > { this.props.note.timestamp } </span> <br /><br /><span id='content' > { this.props.note.content } </span><br /><br /></Paper></div>);}}export default Container;
We import DeleteForeverSharp Icon from Material-UI. We pass the function removeNotes from the component’s props to the onClick event handler. The function this.props.removeNotes gets executed with the note’s key parameter when the delete button is clicked. We increase the size of the icon by passing the style object.
{ this.props.note.key }
This is react’s JSX feature of embedding Javascript inside of render function.
We can mix custom css with React components to achieve even more control over how our app looks.
../src/css/Container.css
.Note {padding-top: 20px;padding-left: 20px;padding-right: 20px;}.NotePaper {box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);min-height: 100px;}.NotePaper:hover {box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);}#title {padding: 30px 20px 0px 30px;font-weight: bold;font-size: 30px;}#content {font-size: 20px;padding: 0px 20px 20px 30px;}#timestamp {font-size: 10px;padding: 0px 20px 20px 30px;}#delete {position: fixed;right: 100px;padding-top: 12px;}
Now let’s add a function to fetch the notes from database. So our notes will be stored even when we close or refresh the tab.
In our ../src/firebase/firebase.js file, we add a new function
export const fetchFromDB = () => {return new Promise((resolve, reject) => {firebase.database().ref('notes/').once('value').then( function(snapshot) {if(snapshot.val()) {var notes = [];Object.keys(snapshot.val()).forEach(key => {var note = {};note = {...snapshot.val()[key]};note['key'] = key;notes.push(note);});resolve(notes);}else {resolve([]);}});})
We use once('value') function to receive data from the database. This function returns a promise, so we use then() function to get the response. In case you're not sure what Promises are, here's a quick overview:
An short explanation of Javascript Promises
Javascript is single-threaded. Two pieces of scripts cannot run at the same time. This approach is called synchronous programming, where each instruction gets fired only after the previous one has finished executing.
It usually takes a few moments for the data from server to reach our app. If we use a synchronous approach to get the data from the server, then our app would get stuck trying to receive data from the server. Our app would be come responsive only after the data has been received. To avoid this we use another type of programming approach known as asynchronous programming. An asynchronous function waits for the data from the server in the background while our app still remains responsive. On receiving the data from the server, the asynchronous function would call a function which we would specify. This function is called callback. Promises are a way to implement this asynchronous behavior in Javascript. If a function implemented as a promise is called, initially it returns a promise. After the function performs its instruction, the promise resolves into a callback function which returns the data from the server.
Basic syntax of a promise:
var promise1 = new Promise(function(resolve, reject) {setTimeout(resolve, 100, 'foo');});promise1.then(function(value) {console.log(value);// expected output: "foo"});console.log(promise1);// expected output: [object Promise]
Output:
> [object Promise]> "foo"
From the order of execution, we can see that promise.then() is asynchronous.
We need to call this function once our component is loaded to get all the notes.
componentWillMount() {fetchFromDB().then(notes => {this.setState({notes: notes,});})
componentWillMount() is a React LifeCycle function. This function would run when component is going to mount. fetchFromDB() fetches notes from database and updates the state.
So far we can add notes and view the notes fetched from the database. We can also delete the notes. When we make a note, it’s possible for anyone to view and delete our note. To avoid that, lets create an authentication system where only authenticated users can go to the dashboard and view their own notes. So, users can store their notes without worrying about other people being able to access it. For authentication, we will be using Google auth. This method of signup requires the user to login using his/her Google account. Firebase also supports other methods of authentication like Twitter, Email based, etc