|
| 1 | +# 09 Sidebar |
| 2 | + |
| 3 | +In this example we are going to add a sidebar to our application, we will start with a specific |
| 4 | +implementation, then we will make it generic. |
| 5 | + |
| 6 | +# Steps |
| 7 | + |
| 8 | +- We will take as starting point sample _08 ColorPickerRefactor_, let's copy the content |
| 9 | + from that file and execute _npm install_. |
| 10 | + |
| 11 | +```bash |
| 12 | +npm install |
| 13 | +``` |
| 14 | + |
| 15 | +- Create a file called _src/sidebar.css_ and add the following styles (http://www.w3schools.com/howto/howto_js_sidenav.asp): |
| 16 | + |
| 17 | +_./src/components/sidebar.css_ |
| 18 | + |
| 19 | +```css |
| 20 | +/* The side navigation menu */ |
| 21 | +.sidenav { |
| 22 | + height: 100%; /* 100% Full-height */ |
| 23 | + width: 0; /* 0 width - change this with JavaScript */ |
| 24 | + position: fixed; /* Stay in place */ |
| 25 | + z-index: 1; /* Stay on top */ |
| 26 | + top: 0; |
| 27 | + left: 0; |
| 28 | + background-color: #808080; /* Gray*/ |
| 29 | + overflow-x: hidden; /* Disable horizontal scroll */ |
| 30 | + padding-top: 60px; /* Place content 60px from the top */ |
| 31 | + transition: 0.5s; /* 0.5 second transition effect to slide in the sidenav */ |
| 32 | +} |
| 33 | + |
| 34 | +/* Position and style the close button (top right corner) */ |
| 35 | +.sidenav .closebtn { |
| 36 | + position: absolute; |
| 37 | + top: 0; |
| 38 | + right: 25px; |
| 39 | + font-size: 36px; |
| 40 | + margin-left: 50px; |
| 41 | +} |
| 42 | + |
| 43 | +/* Style page content - use this if you want to push the page content to the right when you open the side navigation */ |
| 44 | +#main { |
| 45 | + transition: margin-left 0.5s; |
| 46 | + padding: 20px; |
| 47 | +} |
| 48 | + |
| 49 | +/* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font size) */ |
| 50 | +@media screen and (max-height: 450px) { |
| 51 | + .sidenav { |
| 52 | + padding-top: 15px; |
| 53 | + } |
| 54 | + .sidenav a { |
| 55 | + font-size: 18px; |
| 56 | + } |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +- We are going to use CSS Modules, so let's configure it. |
| 61 | + |
| 62 | +_./webpack.config.js_ |
| 63 | + |
| 64 | +```diff |
| 65 | + module.exports = { |
| 66 | + context: path.join(basePath, "src"), |
| 67 | + resolve: { |
| 68 | +- extensions: ['.js', '.ts', '.tsx'] |
| 69 | ++ extensions: ['.js', '.ts', '.tsx', '.css'] |
| 70 | + }, |
| 71 | +``` |
| 72 | + |
| 73 | +- We will only use CSS Modules for custom app stylesheets. We will not use CSS Modules for other CSS files, like Bootstrap (folder node_modules). |
| 74 | + |
| 75 | +_./webpack.config.js_ |
| 76 | + |
| 77 | +```diff |
| 78 | + { |
| 79 | + test: /\.css$/, |
| 80 | ++ include: /node_modules/, |
| 81 | + use: [MiniCssExtractPlugin.loader, "css-loader"] |
| 82 | + }, |
| 83 | ++ // Use CSS modules for custom stylesheets |
| 84 | ++ { |
| 85 | ++ test: /\.css$/, |
| 86 | ++ exclude: /node_modules/, |
| 87 | ++ use: [ |
| 88 | ++ MiniCssExtractPlugin.loader, |
| 89 | ++ { |
| 90 | ++ loader: 'css-loader', |
| 91 | ++ options: { |
| 92 | ++ modules: true, |
| 93 | ++ localIdentName: '[name]__[local]___[hash:base64:5]', |
| 94 | ++ camelCase: true, |
| 95 | ++ }, |
| 96 | ++ }, |
| 97 | ++ ] |
| 98 | ++ }, |
| 99 | ++ // Do not use CSS modules in node_modules folder |
| 100 | + |
| 101 | +``` |
| 102 | + |
| 103 | +- We are going to create now a sidebar component, _src/sidebar.tsx_. Right now we will create just |
| 104 | + a rectangle and we will interact with the animation. |
| 105 | + |
| 106 | +We need to install node typings, since we are goin to make use of _require_ to import from |
| 107 | +the _css_. |
| 108 | + |
| 109 | +```bash |
| 110 | +npm install @types/node --save-dev |
| 111 | +``` |
| 112 | + |
| 113 | +_./src/components/sidebar.tsx_ |
| 114 | + |
| 115 | +```jsx |
| 116 | +import * as React from "react"; |
| 117 | + |
| 118 | +const classNames = require("./sidebar.css"); |
| 119 | + |
| 120 | +export const SidebarComponent = () => ( |
| 121 | + <div id="mySidenav" className={classNames.sidenav}> |
| 122 | + <span>Basic side bar, first steps</span> |
| 123 | + </div> |
| 124 | +); |
| 125 | +``` |
| 126 | + |
| 127 | +- Let's add this component to the _index_ barrel. |
| 128 | + |
| 129 | +_./src/components/index.ts_ |
| 130 | + |
| 131 | +```diff |
| 132 | +export * from "./hello"; |
| 133 | +export * from "./nameEdit"; |
| 134 | +export * from "./colorBrowser"; |
| 135 | +export * from "./colorPicker"; |
| 136 | ++ export * from "./sidebar"; |
| 137 | +``` |
| 138 | + |
| 139 | +- We are going to add a known id to the body section of _src/index.html_ page |
| 140 | + |
| 141 | +_./src/index.html_ |
| 142 | + |
| 143 | +```diff |
| 144 | +- <body> |
| 145 | ++ <body id="main"> |
| 146 | +``` |
| 147 | + |
| 148 | +- Let's place the component adding it into the `app.tsx`: |
| 149 | + |
| 150 | +_./src/app.tsx_ |
| 151 | + |
| 152 | +```diff |
| 153 | +import * as React from "react"; |
| 154 | +- import { HelloComponent, NameEditComponent, ColorBrowser, ColorPicker } from "./components"; |
| 155 | ++ import { HelloComponent, NameEditComponent, ColorBrowser, ColorPicker, SidebarComponent } from "./components"; |
| 156 | +import { Color } from "./model/color"; |
| 157 | +``` |
| 158 | + |
| 159 | +_./src/app.tsx_ |
| 160 | + |
| 161 | +```diff |
| 162 | + return ( |
| 163 | + <> |
| 164 | ++ <SidebarComponent> |
| 165 | + <ColorBrowser color={color} /> |
| 166 | +``` |
| 167 | + |
| 168 | +- Let's start with the interesting part of this implementation, let's add a flag to show/hide the |
| 169 | + sidebar _sidebar.tsx_. |
| 170 | + |
| 171 | +_./src/components/sidebar.tsx_ |
| 172 | + |
| 173 | +```diff |
| 174 | +import * as React from 'react'; |
| 175 | + |
| 176 | +const classNames = require('./sidebar.css'); |
| 177 | + |
| 178 | ++ interface Props { |
| 179 | ++ isVisible: boolean; |
| 180 | ++ } |
| 181 | + |
| 182 | +- export const SidebarComponent = () => |
| 183 | ++ export const SidebarComponent = (props: Props) => |
| 184 | + <div id="mySidenav" className={classNames.sidenav}> |
| 185 | + <span>Basic sidebar, first steps</span> |
| 186 | + </div> |
| 187 | +``` |
| 188 | + |
| 189 | +- Now let's add some logic to show / hide the sidebar in case the flag gets updated |
| 190 | + |
| 191 | +_./src/sidebar.tsx_ |
| 192 | + |
| 193 | +```diff |
| 194 | +import * as React from 'react'; |
| 195 | + |
| 196 | +const classNames = require('./sidebar.css'); |
| 197 | + |
| 198 | +interface Props { |
| 199 | + isVisible: boolean; |
| 200 | +}; |
| 201 | + |
| 202 | ++ const divStyle = (props: Props): React.CSSProperties => ({ |
| 203 | ++ width: (props.isVisible) ? '23rem' : '0rem' |
| 204 | ++ }); |
| 205 | + |
| 206 | +export const SidebarComponent = (props: Props) => |
| 207 | +- <div id="mySidenav" className={classNames.sidenav}> |
| 208 | ++ <div id="mySidenav" className={classNames.sidenav} |
| 209 | ++ style={divStyle(props)} |
| 210 | ++ > |
| 211 | + <span>Basic sidebar, first steps</span> |
| 212 | + </div> |
| 213 | +``` |
| 214 | + |
| 215 | +- Let's make a quick test we will show always the side bar: |
| 216 | + |
| 217 | +_./src/app.tsx_ |
| 218 | + |
| 219 | +```diff |
| 220 | + return ( |
| 221 | + <> |
| 222 | +- <SidebarComponent /> |
| 223 | ++ <SidebarComponent isVisible={true}/> |
| 224 | + <ColorBrowser color={color} /> |
| 225 | +``` |
| 226 | + |
| 227 | +- If we start the project we should now see the sidebar that we have created (a gray rectangle). |
| 228 | + |
| 229 | +```bash |
| 230 | +npm start |
| 231 | +``` |
| 232 | + |
| 233 | +_What if I cannot see the sidebar?_ Check that your styles and webpackconfig has been applied, |
| 234 | +you may need to start and top webpack-dev-sever (relaunch _npm \_start_), check with dev tools |
| 235 | +that you are loading the CSS styles. |
| 236 | + |
| 237 | +- Now at app level we can remember the visible status and add a button to toggle the |
| 238 | + visibility of the sidebar. |
| 239 | + |
| 240 | +_./src/app.tsx_ |
| 241 | + |
| 242 | +```diff |
| 243 | +export const App = () => { |
| 244 | + const [name, setName] = React.useState("defaultUserName"); |
| 245 | + const [editingName, setEditingName] = React.useState("defaultUserName"); |
| 246 | + const [color, setColor] = React.useState<Color>({ |
| 247 | + red: 20, |
| 248 | + green: 40, |
| 249 | + blue: 180 |
| 250 | + }); |
| 251 | ++ const[isVisible, setVisible] = React.useState(false); |
| 252 | +``` |
| 253 | + |
| 254 | +_./src/app.tsx_ |
| 255 | + |
| 256 | +```diff |
| 257 | + return ( |
| 258 | + <> |
| 259 | +- <SidebarComponent isVisible={true} /> |
| 260 | ++ <SidebarComponent isVisible={isVisible} /> |
| 261 | + <ColorBrowser color={color} /> |
| 262 | + <ColorPicker color={color} onColorUpdated={setColor} /> |
| 263 | + <HelloComponent userName={name} /> |
| 264 | + <NameEditComponent |
| 265 | + initialUserName={name} |
| 266 | + editingName={editingName} |
| 267 | + onNameUpdated={setUsernameState} |
| 268 | + onEditingNameUpdated={setEditingName} |
| 269 | + disabled={editingName === "" || editingName === name} |
| 270 | + /> |
| 271 | ++ <div style={{float: 'right'}}> |
| 272 | ++ <button |
| 273 | ++ onClick={() => setVisible(!isVisible)}> |
| 274 | ++ Toggle Sidebar |
| 275 | ++ </button> |
| 276 | ++ </div> |
| 277 | + </> |
| 278 | +``` |
| 279 | + |
| 280 | +- Let's start the application to check how it behaves: |
| 281 | + |
| 282 | +```bash |
| 283 | +npm start |
| 284 | +``` |
| 285 | + |
| 286 | +> Excercise: the inline call to the function in _onClick_ is not considered a |
| 287 | +> good pratice (on each render the function will be recreated), let's refactor this in two |
| 288 | +> steps: |
| 289 | +
|
| 290 | +- First we will extract this logic to a function, we will call it _toggleSidebarVisbility_. |
| 291 | +- Then let's wrap visibility + toggleSidebarVisibility in a custom hook. |
| 292 | + |
| 293 | +* So far so good, but what happens if we want to make this sidebar a reusable component? We could just show the frame but the content should be dynamic. |
| 294 | + |
| 295 | +* Let's start by adding some content when instantiating the sidebar (_app.tsx_). |
| 296 | + |
| 297 | +_./src/app.tsx_ |
| 298 | + |
| 299 | +```diff |
| 300 | + <> |
| 301 | +- <SidebarComponent isVisible={isVisible} /> |
| 302 | ++ <SidebarComponent isVisible={isVisible}> |
| 303 | ++ <h1>Cool Scfi movies</h1> |
| 304 | ++ <ul> |
| 305 | ++ <li><a href="https://www.imdb.com/title/tt0816692/">Interstellar</a></li> |
| 306 | ++ <li><a href="https://www.imdb.com/title/tt0083658/">Blade Runner</a></li> |
| 307 | ++ <li><a href="https://www.imdb.com/title/tt0062622/">2001: a space odyssey</a></li> |
| 308 | ++ </ul> |
| 309 | ++ </SidebarComponent> |
| 310 | + <ColorBrowser color={color} /> |
| 311 | +``` |
| 312 | + |
| 313 | +> We got an error, _children_ is not defined, that's something we are going to fix in the |
| 314 | +> next step... |
| 315 | +
|
| 316 | +- Now in the _sidebar.tsx_ let's dump this content by using {this.props.children} |
| 317 | + |
| 318 | +_./src/components/sidebar.tsx_ |
| 319 | + |
| 320 | +```diff |
| 321 | +- export const SidebarComponent = (props: Props) => ( |
| 322 | ++ export const SidebarComponent: React.StatelessComponent<Props> = (props) => ( |
| 323 | + |
| 324 | + <div id="mySidenav" className={classNames.sidenav} style={divStyle(props)}> |
| 325 | +- <span>Basic side bar, first steps</span> |
| 326 | ++ {props.children} |
| 327 | + </div> |
| 328 | +); |
| 329 | +``` |
| 330 | + |
| 331 | +- Let's try the sample |
| 332 | + |
| 333 | +``` |
| 334 | +npm start |
| 335 | +``` |
0 commit comments