DEV Community

Cover image for NavBar
jacobwicks
jacobwicks

Posted on

NavBar

In this post we will make the NavBar. In the next post we will make the Writing component, where the user can write new cards. The NavBar will let the user switch between Answering cards and Writing cards.

User Story

  • The user thinks of a new card. The user opens the card editor. The user clicks the button to create a new card. The user writes in the card subject, question prompt, and an answer to the question. The user saves their new card.

This user story has a lot of things going on. To make this user story possible we will need to make new component where the user can write cards. The Writing component will be a new 'scene' in the application. We will also need to give the user a way to get to the Writing scene.

Let's make a NavBar (Navigation Bar) component to give the user a way to choose between the two scenes. The two scenes will be the Writing scene and the Answering scene. The NavBar will give the user a button to go to the Writing scene. The NavBar will also give the user a button to go to the Answering scene.

We have not made the NavBar and the Writing scene yet. The App just shows the Answering scene all the time. The NavBar will go inside the App. The Writing scene will also go inside the App. The App will keep track of what to show the user. The NavBar will tell the App when the user wants to see a different scene.

In this post, we will

  • Make a placeholder for the Writing component
  • Write a typescript enum for the different scenes
  • Change the App component to keep track of what scene to show the user
  • Make the NavBar component
  • Show the NavBar component to the user

By the end of this post the NavBar component will show up on the screen and let the user choose between looking at the Answering component and the Writing component. In the next post we will actually make the real Writing component.

Here's the NavBar in action:
NavBar2

Placeholder for the Writing component

File: src/scenes/Writing/index.tsx
Will Match: src/scenes/Writing/complete/index-1.tsx

We haven't made Writing yet. But we need to have something to show on the screen when we select Writing. So we are going to make a placeholder component. This will just be a div with the word 'writing' in it. Because this is a placeholder we aren't going to take the time to write tests first.

The Writing component is one of our 'scenes.' So its folder is src/scenes/Writing.

import React from 'react';

const Writing = () => <div>Writing</div>

export default Writing;

That's it!

Make the sceneTypes type

File: src/types.ts
Will Match: src/complete/types-5.ts

Add a new enum named 'SceneTypes' in src/types.ts:

//defines the scenes that the user can navigate to
export enum SceneTypes {

    //where the user answers questions
    answering = "answering",

    //where the user writes questions
    writing = "writing"
};

Making the App Keep Track of the Scenes

Right now the App just shows the Answering scene all the time. But to make the user story possible we need to let the user choose the Writing scene. We need to keep track of what scene the user is looking at. We are going to keep track of what scene the user is looking at inside the App component. We'll keep track of what scene the user is looking at with useState.

Features

  • There is a NavBar

Choose Components

We'll use the custom NavBar that we'll write later in this post

Decide What to Test

Let's test whether the NavBar shows up.

App Test 1: Has the NavBar

File: src/App.test.tsx
Will Match: src/complete/test-5.tsx

Add a test that checks for the NavBar. The NavBar will have a Header with the text 'Flashcard App.'

//shows the NavBar
it('shows the NavBar', () => {
  const { getByText } = render(<App/>);

  //the navbar has a header with the words "Flashcard App" in it
  const navBar = getByText(/flashcard app/i);

  //if we find the header text, we know the NavBar is showing up
  expect(navBar).toBeInTheDocument();
});

Pass App Test 1: Has the NavBar

File: src/App.tsx
Will Match: src/complete/app-5.tsx

The App component will keep track of which scene to show. We will use the useState() hook from React to keep track of which scene to show. The NavBar component will let the user choose the scene. The App won't pass the test for showing the NavBar until later in this post, after we have written the NavBar and imported it into the App.

Import the useState hook from React.

import React, { useState } from 'react';

Import the SceneTypes enum from types.

import { SceneTypes } from './types/';

Import the Writing component.

import Writing from './scenes/Writing';

We haven't made the NavBar yet, so we won't import it. After we make the NavBar, we will come back to the App and add the NavBar to it.

Change the App to this:

const App: React.FC = () => {

const [showScene, setShowScene] = useState(SceneTypes.answering);

  return (
    <CardProvider>
      <StatsProvider>
        {showScene === SceneTypes.answering && <Answering />}
        {showScene === SceneTypes.writing && <Writing/>}
      </StatsProvider>
    </CardProvider>
  )};

Here's why the code for the App component looks so different now.

Curly Brackets and return

Before these changes the App function just returned JSX. The App had a 'concise body.' A function with a concise body only has an expression that gives the return value. But now we have added an expression before the expression that gives the return value. The new expression sets up useState to track what scene to show. Because we have added an expression besides the return value to the function, we have to add curly brackets so the compiler knows to look for expressions and not just a return value. This is called a function with a 'block body.'

return()

This is the return method of your function. This tells the function to return the value inside the parentheses. The parentheses are not required. But if you don't have the parentheses, you have to start your JSX on the same line. So it would look like this:

//this would work
return <CardProvider>
      <StatsProvider>
        {showScene === SceneTypes.answering && <Answering />}
        {showScene === SceneTypes.writing && <Writing/>}
      </StatsProvider>
    </CardProvider>;

But if you don't have parentheses, starting your JSX return value on the next line will not work.

//this won't work
return 
    <CardProvider>
      <StatsProvider>
        {showScene === SceneTypes.answering && <Answering />}
        {showScene === SceneTypes.writing && <Writing />}
      </StatsProvider>
    </CardProvider>;

I think it is easier to read with the return value starting on the next line. So I put parentheses around the return value.

UseState

The useState hook gives us a place to keep a variable, and a function to change it.

const [showScene, setShowScene] = useState(SceneTypes.answering);

useState(SceneTypes.answering) is the call to the useState hook. SceneTypes.answering is the starting value. TypeScript can figure out from this that the type of the variable showScene will be SceneTypes. You can also explicitly declare that you are using a type. Explicit declaration of a type on useState looks like this:

useState<SceneTypes>(SceneTypes.answering);

const [showScene, setShowScene] is the declaration of two const variables, showScene and setShowScene.

showScene is a variable of type SceneTypes. So showScene will either be SceneTypes.answering or SceneTypes.writing. Remember when we wrote the enum SceneTypes earlier? SceneTypes.answering is the string 'answering' and SceneTypes.writing is the string 'writing'. The variable showScene can only equal one of those two strings.

setShowScene is a function. It takes one argument. The argument that setShowScene takes is of the type SceneTypes. So you can only invoke setShowScene with SceneTypes.answering or SceneTypes.writing. After you invoke setShowScene, the value of showScene will be set to the value that you passed to setShowScene.

We will pass the function setShowScene to the NavBar. Nothing calls setShowScene yet. But after we make the NavBar, we will import it into the App. Then we will pass the setShowScene function to the NavBar. The Navbar will use setShowScene to change the value of showScene in App. When the value of showScene changes, App will change what scene it shows to the user.

Conditional Rendering of Answering and Writing

Conditional Rendering is how you tell React that if some condition is true, you want to show this component to the user. Rendering a component means showing it to the user.

        {showScene === SceneTypes.answering && <Answering />}

{}: The curly brackets tell the compiler that this is an expression. The compiler will evaluate the expression to figure out what value it has before rendering it to the screen.

showScene === SceneTypes.answering: this is an expression that will return a boolean value. It will return true or it will return false.

&&: This is the logical AND operator. It tells the compiler that if the condition to the left of it is true, it should evaluate and return the expression to the right.

&& <Answering/>: The logical && operator followed by the JSX for the Answering component means 'if the condition to the left of && is true, show the Answering component on the screen.'

There is one conditional rendering expression for each scene.

        {showScene === SceneTypes.answering && <Answering />}
        {showScene === SceneTypes.writing && <Writing/>}

This code means if showScene is 'answering' show the Answering component, and if showScene is 'writing' show the Writing component.

You are done with the App for now. The App won't pass the test for the NavBar until later in this post, after we have written the NavBar and imported it into the App.

The NavBar

Now we are ready to make the NavBar. Once we have written the NavBar, we will import it into the App so it shows up on screen and lets the user choose which scene they want to see.

Features

  • The user can click a button to go to the Writing scene
  • The user can click a button to go to the Answering scene

Choose Components

The NavBar is a menu, so we will use the Menu component from Semantic UI React.

Decide What to Test

  • menu
  • header
  • button loads Answering
  • button loads Writing

Write the tests

File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-1.tsx

Write a comment for each test.

//has a menu component
//has a header
//has a menu item button that loads the answering scene
//clicking answer invokes setShowScene
//has a menu item button that loads the writing scene
//clicking edit invokes setShowScene

Imports and afterEach.

import React from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import NavBar from './index';
import { SceneTypes } from '../../types';

afterEach(cleanup);

Write a helper function to render the NavBar. The helper function takes an optional prop function setShowScene. We'll use this prop to make sure that the NavBar calls the function setShowScene when the user clicks the buttons.

const renderNavBar = (setShowScene?: (scene: SceneTypes) => void) => render(
    <NavBar 
    showScene={SceneTypes.answering} 
    setShowScene={setShowScene ? setShowScene : (scene: SceneTypes) => undefined}
    />);

NavBar Test 1: Has a Menu

File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-1.tsx

NavBar takes two props. setShowScene is a function that accepts a SceneType as a parameter. showScene is the SceneType that is currently being shown.

Clicking the Menu Items will invoke setShowScene with the appropriate SceneType.

import React from 'react';
import { Menu } from 'semantic-ui-react';
import { SceneTypes } from '../../types';

const NavBar = ({
    setShowScene,
    showScene
}:{
    setShowScene: (scene: SceneTypes) => void,
    showScene: SceneTypes
}) => <Menu data-testid='menu'/>

export default NavBar;

Now NavBar has a menu.

Has a Menu

NavBar Test 2: Has a Header

File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-2.tsx

If this weren't a tutorial, and you were designing the NavBar yourself, maybe you wouldn't test if NavBar has a header. You might decide that the header on the NavBar is not an important enough feature to test. The reason we are testing for the header is that the App's test checks for the NavBar by finding its header. So we want to be sure when we test NavBar that it has a header, so that when we add it to the App the tests will pass.

//has a header
it('has a header', () => {
    const { getByText } = renderNavBar();
    const header = getByText(/flashcard app/i);
    expect(header).toBeInTheDocument();
});

Pass NavBar Test 2: Has a Header

File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-2.tsx

Add the Menu.Item header.

    <Menu data-testid='menu'>
        <Menu.Item header content='Flashcard App'/>
    </Menu>

Header Passes

NavBar Test 3: Answering Button

File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-3.tsx

//has a menu item button that loads the answering scene
it('has a button to get you to the answering scene', () => {
    const { getByText } = renderNavBar();
    const answering = getByText(/answer/i)
    expect(answering).toBeInTheDocument();
});

Pass NavBar Test 3: Answering Button

File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-3.tsx

The active prop will highlight the Menu Item when the expression evaluates to true. This Menu Item will be active when the showScene prop is SceneTypes.answering.

    <Menu data-testid='menu'>
        <Menu.Item header content='Flashcard App'/>
        <Menu.Item content='Answer Flashcards' 
            active={showScene === SceneTypes.answering}/>
    </Menu>

Answer Button

NavBar Test 4: Clicking Answering Button

File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-4.tsx

//clicking answer invokes setShowScene
it('clicking answer invokes setShowScene', () => {
    const setShowScene = jest.fn();
    const { getByText } = renderNavBar(setShowScene);
    const answering = getByText(/answer/i)

    fireEvent.click(answering);
    expect(setShowScene).toHaveBeenLastCalledWith(SceneTypes.answering);
});

Pass NavBar Test 4: Clicking Answering Button

File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-4.tsx

Add the onClick function to the Answering button.

 <Menu.Item content='Answer Flashcards' 
            active={showScene === SceneTypes.answering}
            onClick={() => setShowScene(SceneTypes.answering)}/>

onClick

NavBar Tests 5-6: Writing Button

File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-5.tsx

//has a menu item button that loads the writing scene
it('has a button to get you to the writing scene', () => {
    const { getByText } = renderNavBar();
    const writing = getByText(/edit/i)
    expect(writing).toBeInTheDocument();
});

//clicking edit invokes setShowScene
it('clicking edit invokes setShowScene', () => {
    const setShowScene = jest.fn();
    const { getByText } = renderNavBar(setShowScene);
    const writing = getByText(/edit/i)

    fireEvent.click(writing);
    expect(setShowScene).toHaveBeenLastCalledWith(SceneTypes.writing);
});

Pass NavBar Tests 5-6: Writing Button

File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-5.tsx

    <Menu data-testid='menu'>
        <Menu.Item header content='Flashcard App'/>
        <Menu.Item content='Answer Flashcards' 
            active={showScene === SceneTypes.answering}
            onClick={() => setShowScene(SceneTypes.answering)}/>
        <Menu.Item content='Edit Flashcards'
            active={showScene === SceneTypes.writing}
            onClick={() => setShowScene(SceneTypes.writing)}/>
    </Menu>

Alt Text

Ok, now we have a NavBar that passes all the tests! Let's import it into the App and show it to the user.

Import NavBar into App

File: src/App.tsx
Will Match: src/complete/app-6.tsx

Now let's import the NavBar into the App. This will make App pass the tests we wrote earlier. It will also make the NavBar show up on screen. Once the user can see the NavBar, they will be able to switch between the two scenes. The user will be able to look at the Answering scene. The user will also be able to look at the Writing scene. The Writing scene that the user can see will be the placeholder that you wrote earlier in this post. In the next post we will make the actual Writing component.

import NavBar from './components/NavBar';

Add the NavBar component into the App.

//rest of app component stays the same
  return (
    <CardProvider>
      <StatsProvider>
//add the NavBar here
        <NavBar setShowScene={setShowScene} showScene={showScene} />
        {showScene === SceneTypes.answering && <Answering />}
        {showScene === SceneTypes.writing && <Writing/>}
      </StatsProvider>
    </CardProvider>
  )};

Save the App. Most of the tests will pass, but the snapshot test will fail because you have changed what shows up on the screen. Update the snapshot by pressing 'u'. Now all tests should pass.

Run the app with npm start. You will see the Answering scene with the NavBar above it.

NavBar1

Click on 'Edit Flashcards'. You will see the placeholder Writing scene.

NavBar2
Great job!

Next Post

In the next post we will make the actual Writing component.

Top comments (0)