├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── content └── your-first-choo-app │ ├── README.md │ └── images │ ├── animals.gif │ ├── remix-this.png │ ├── show.png │ ├── starter-blank.png │ ├── starter-delete-animals.gif │ ├── starter-filter.gif │ ├── starter-first-animal.png │ ├── starter-first-route.png │ ├── starter-grass.png │ ├── starter-new-file.png │ ├── starter-random-animals.gif │ ├── starter-readme.png │ ├── starter-second-animal.png │ ├── starter-share.png │ └── starter-third-animal.gif ├── package.json └── src ├── images ├── animals.gif ├── choo-animals-mobile.gif └── choo-animals.gif ├── index.html ├── style.css └── your-first-choo-app ├── images ├── animals.gif ├── remix-this.png ├── show.png ├── starter-blank.png ├── starter-delete-animals.gif ├── starter-filter.gif ├── starter-first-animal.png ├── starter-first-route.png ├── starter-grass.png ├── starter-new-file.png ├── starter-random-animals.gif ├── starter-readme.png ├── starter-second-animal.png ├── starter-share.png └── starter-third-animal.gif └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "6" 3 | - "4" 4 | sudo: false 5 | language: node_js 6 | script: "npm run test" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Yoshua Wuyts 2 | 3 | Permission is hereby granted, free of charge, to any person ob- 4 | taining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without restric- 6 | tion, including without limitation the rights to use, copy, modi- 7 | fy, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is fur- 9 | nished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONIN- 17 | FRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 19 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # choo-handbook 2 | A handbook of guides & tutorials for [choo][choo], a framework for creating 3 | sturdy frontend applications. 4 | 5 | If you want to read the handbook, visit [handbook.choo.io][web]. 6 | 7 | ## Contributing 8 | The handbook is currently a WIP. If you'd like to get involved, 9 | [join the conversation][join]. Pull requests to fix typos and such are welcome. 10 | Please file any issues for any bugs you find. 11 | 12 | ## License 13 | [MIT](https://tldrlegal.com/license/mit-license) 14 | 15 | [choo]: https://github.com/yoshuawuyts/choo 16 | [web]: https://handbook.choo.io 17 | [join]: https://github.com/yoshuawuyts/choo-handbook/issues/10 18 | -------------------------------------------------------------------------------- /content/your-first-choo-app/README.md: -------------------------------------------------------------------------------- 1 | ## Who is this guide for? 2 | If you're comfortable with the basics of JavaScript, HTML & CSS, but you've never built an interactive web app before, this guide is for you. 3 | 4 | If you are a seasoned JavaScript developer with experience in other frameworks, and are looking to try `choo` for the first time, this guide is also for you. 5 | 6 | ## What is choo? 7 | `choo` is a small framework that helps you build web apps with JavaScript. 8 | 9 | JavaScript makes it easy to add fun interactive elements to our HTML pages. As the language has gained in popularity, developers are now choosing to build their websites entirely with JavaScript. This offers many benefits to both users and developers, most notably that websites can now behave similarly to native desktop or mobile applications. 10 | 11 | `choo` provides a small but powerful collection of tools that commonly feature in JavaScript web apps, such as templating, routing, and state management. By the end of this guide, you will understand what these terms mean. Don't be concerned if you don't already know what they are. This is why you are here! 12 | 13 | ## What will we build? 14 | For this guide, we're going to build an interactive animal simulator called `choo-animals`. It will be informative, cute, but most of all, fun! 15 | 16 | This is what it looks like: 17 | 18 | [![screenshot](images/animals.gif)](https://choo-animals.glitch.me) 19 | 20 | The user can click anywhere on the grass to add an animal to the screen. Clicking on an animal will remove it from the screen. The user can also filter which animals they see, using the filter links at the bottom of the page. 21 | 22 | You can try it for yourself here: [https://choo-animals.glitch.me](https://choo-animals.glitch.me) 23 | 24 | ## Let's get started! 25 | For this guide, we will be using an online code editor called [Glitch](https://glitch.com/). 26 | 27 | Glitch lets us write, edit and deploy JavaScript code in the browser. This is useful because we can make changes to our code and see the updated results instantly. 28 | 29 | A starter project is available for you to follow along with this guide: [https://glitch.com/edit/#!/project/choo-animals-starter](https://glitch.com/edit/#!/project/choo-animals-starter) 30 | 31 | When the editor has finished loading, you should make your own copy of the project so you can start coding. To do this, click the `Remix this` button near the top left hand corner of the window: 32 | 33 | ![remix](images/remix-this.png "Screenshot of remix this button") 34 | 35 | Your screen should now looking something like the following: 36 | 37 | ![starter](images/starter-readme.png "Screenshot of starter code") 38 | 39 | ## Starting choo 40 | In our project's left sidebar, click on the `index.js` file to open its code in the code editor. 41 | 42 | You will see the following: 43 | 44 | ```js 45 | // import choo 46 | var choo = require('choo') 47 | 48 | // initialize choo 49 | var app = choo() 50 | 51 | // start app 52 | app.mount('div') 53 | ``` 54 | 55 | At the top of this file, we are importing the `choo` framework into our project using the `require()` function. 56 | 57 | `require()` lets us import other JavaScript files into our code. `require()` does a lot more than this, but if you're not already familiar with its concepts, for this tutorial all you need to remember is that we will use it to import `choo`, and a handful of other files we write ourselves into our application. 58 | 59 | We then need to create an instance of `choo` that we will build our app with. We initialize this instance using `choo()`, and then store it in the `app` variable. From this point onwards, when we interact with `choo`, it will be via `app`. 60 | 61 | Finally, we start our application by appending it to the `
` element of the HTML page our app will run on. We do this using the `app.mount()` method that `choo` conveniently makes available to us. If you open the `index.html` file, you'll see there is a single `
` element inside ``. This is where `app.mount()` attaches our application. 62 | 63 | At this point, we can take a look at what our app currently looks like. To do this, press the `"🕶 Show"` button near the top left hand corner of the window: 64 | 65 | ![show](images/show.png "Screenshot of show button") 66 | 67 | Our application should appear in a new browser tab. However, as you'll see, the screen will be blank: 68 | 69 | ![blank](images/starter-blank.png "Screenshot of blank browser") 70 | 71 | Our app does not yet contain any templates, nor any routes. This means that if we try to run our application, we will only ever see a blank page. 72 | 73 | Let's fix this! 74 | 75 | ## Building a template 76 | In web app development, a template often refers to a piece of code that will help us render some HTML to the screen. Being templates, they will render most of the same information each time, but change slightly depending on the different bits of input they receive. 77 | 78 | Templates are essentially functions: given some input, they will render a new output depending on what happens inside that function. 79 | 80 | `choo` provides another module we can import that will help us build HTML templates with ["template strings"](https://davidwalsh.name/template-literals), one of JavaScript's newer syntax features. 81 | 82 | Let's update the code in our `index.js` file to read the following: 83 | 84 | ```js 85 | // import choo 86 | var choo = require('choo') 87 | 88 | // import choo's template helper 89 | var html = require('choo/html') 90 | 91 | // initialize choo 92 | var app = choo() 93 | 94 | // create a template 95 | var main = function () { 96 | return html`
choo animals
` 97 | } 98 | 99 | // start app 100 | app.mount('div') 101 | ``` 102 | 103 | Underneath our `choo` import near the top of the file, we're now also importing `choo`'s html helper. This becomes accessible via the `html` variable. 104 | 105 | Towards the end of the file, we're creating a function, and assigning it to the `main` variable. Inside this function, we're returning a template that we'll use to render `
choo animals
` to our page. 106 | 107 | This bit of code might be confusing, so let's dissect further. 108 | 109 | The `html` variable references a function that parses template strings containing HTML code (eg. `` html`

choo

` ``). A given template string will be interpreted into a special data structure, that `choo` then renders as HTML on the page. 110 | 111 | ## Creating a route 112 | When a friend links you to a specific page on a website, that URL acts as a route so the server knows where to direct you. 113 | 114 | Many JavaScript web apps use this same concept to figure out which template to show you at a given point within the application. 115 | 116 | This means that to see something on our page, we need to create a route that directs us to the template we just made. Let's add some code underneath our template: 117 | 118 | ```js 119 | // ... 120 | 121 | // create a template 122 | var main = function () { 123 | return html`
choo animals
` 124 | } 125 | 126 | // create a route 127 | app.route('/', main) 128 | 129 | // start app 130 | app.mount('div') 131 | ``` 132 | 133 | The `app.route()` function takes in two arguments, the first being the URL path you want to create (this should be specified as a string), and the second being the template you'd like to show users when they arrive at that path (usually a variable that references the template). 134 | 135 | As this is our first route, we will make it the "index" (or "root") of the entire application. We do this by specifying the route as `'/'`. This route would be the equivalent of visiting something like `https://choo.io` (the home page). Creating a route with the path `/cats` would be the equivalent of visiting `https://choo.io/cats`. 136 | 137 | As we change the code in Glitch's editor, our app's window should automatically update itself. If you open it, you should see the following: 138 | 139 | ![first route](images/starter-first-route.png "Screenshot of first route") 140 | 141 | If your app hasn't updated yet, you can simply refresh the window yourself. 142 | 143 | Now that we can see text on the screen, it means that you successfully got `choo` working. Congratulations! 144 | 145 | We're not finished yet, but let's quickly summarise what we've done so far: 146 | 147 | - First, we initialised `choo`, and mounted it onto the page. 148 | - We then created a template using the `choo/html` template function. 149 | - Finally, we created an index route (`/`), and pointed it to our newly made template. 150 | 151 | ## Modularising our templates 152 | Just like we can `require()` third-party modules like `choo` into our application, we can break our code down into smaller pieces so that the different concerns of our app exist in their own neat little files. 153 | 154 | If we were to modularise what we currently have, it would likely be considered "pre-optimisation", since there's not much code there anyway. Attempts to break it out further would likely just increase complexity, but as we're about to add a lot of new markup to our `main` template in just a moment, let's go ahead and break it out into its own file. 155 | 156 | In Glitch's left sidebar, click the `"+ New File"` button. Specify your new file's path as `templates/main.js`, then click `Add File`: 157 | 158 | ![new file](images/starter-new-file.png "Screenshot of new file") 159 | 160 | First this creates a new folder named `templates`, and then adds a file called `main.js` inside of it. This file should now appear in the sidebar. 161 | 162 | Let's add some code to our new `main.js` file: 163 | 164 | ```js 165 | // import choo's template helper 166 | var html = require('choo/html') 167 | 168 | // export module 169 | module.exports = function () { 170 | // create html template 171 | return html` 172 |
173 |
174 | 175 |
176 |
177 | ` 178 | } 179 | ``` 180 | 181 | What's happening here? 182 | 183 | First, we are importing `choo`'s html helper so that we can build a new template within this file. 184 | 185 | Next, we are assigning a function that returns a template to the variable called `module.exports`. What does this variable do exactly? In the context of this application, when we create JavaScript files that we'd like to import into other JavaScript files, whatever is assigned to `module.exports` is what will be exported out. 186 | 187 | Inside of our template code, we've added some markup that will display an image called `bg.gif` on the page. 188 | 189 | __*(NOTE: There are some asset files this project requires that you won't be able to see in Glitch's left sidebar. This image is an example of one those files. Other assets will crop up later in this tutorial, but I'll be sure to point them out to you.)*__ 190 | 191 | Now let's `require()` this new template into our `index.js` file, and see the results. 192 | 193 | Start by adding this line towards the top of `index.js`: 194 | 195 | ```js 196 | // ... 197 | 198 | // import choo's template helper 199 | var html = require('choo/html') 200 | 201 | // import template 202 | var main = require('./templates/main.js') 203 | 204 | // initialize choo 205 | var app = choo() 206 | 207 | // ... 208 | ``` 209 | 210 | Underneath the line of code where we import `choo`'s html helper, we now import the `main.js` file we just created. Remembering that we exported a function that returns a template, we can now use that as the designated template for our `/` route. 211 | 212 | Let's clean up `index.js`: 213 | 214 | ```js 215 | // import choo 216 | var choo = require('choo') 217 | 218 | // import choo's template helper 219 | var html = require('choo/html') 220 | 221 | // import template 222 | var main = require('./templates/main.js') 223 | 224 | // initialize choo 225 | var app = choo() 226 | 227 | // create a route 228 | app.route('/', main) 229 | 230 | // start app 231 | app.mount('div') 232 | ``` 233 | 234 | If we look at our application, we should now see the following: 235 | 236 | ![grass](images/starter-grass.png "Screenshot of grass") 237 | 238 | Awesome! Let's recap what we just did: 239 | 240 | - First, we created a file called `main.js` inside of a new folder called `templates`. 241 | - Inside `main.js`, we created a function that returns a template, and then exported it using `module.exports`. 242 | - Finally, we imported `main.js` into `index.js`, then plugged it into our `/` route. 243 | 244 | Now that we have a nice grass field to play in, let's start adding some animals. 245 | 246 | ## Adding state to our application 247 | A core behaviour of many modern web applications is that they can be entirely driven and interacted with, without having to refresh or navigate to another static HTML page. 248 | 249 | When you browse a website like [Wikipedia](https://wikipedia.org) or [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/JavaScript), each new page you navigate to, or each piece of functionality you interact with, will require a new page from the server to load in your browser. 250 | 251 | In `choo`, the idea is that when information is changed or new information becomes available, the page will automatically update itself to reflect that, much in the same way that a native desktop or mobile application would. 252 | 253 | To do this, `choo` uses the concept of "application state" to drive the output of its templates. This could mean absolutely nothing to you right now, so it's a good opportunity to dive further into what "state" is and how it works. 254 | 255 | In programming, it's commonplace for a script to have several variables which contain information that you pass into a function. A function will typically do the same thing each time you run it, so when you pass the same variables in as arguments again and again, it will keep returning the same result: 256 | 257 | ```js 258 | // example 259 | var fun = true 260 | var name = 'Alice' 261 | 262 | function sentence() { 263 | if (fun) { 264 | console.log(`Yay! ${name} is having fun!`) 265 | } else { 266 | console.log(`Oh no, ${name} is not having fun.`) 267 | } 268 | } 269 | 270 | sentence() 271 | sentence() 272 | sentence() 273 | 274 | // the following is logged to screen: 275 | // "Yay! Alice is having fun!" 276 | // "Yay! Alice is having fun!" 277 | // "Yay! Alice is having fun!" 278 | ``` 279 | 280 | As mentioned earlier in this guide: 281 | 282 | *"Templates are essentially functions: given some input, they will render a new output depending on what happens inside that function."* 283 | 284 | In `choo`, state is passed down into our templates so that we can change what our templates do depending on what is currently contained in that state. 285 | 286 | Let's demonstrate this concept by adding new code to our `index.js` file: 287 | 288 | ```js 289 | // ... 290 | 291 | // initialize choo 292 | var app = choo() 293 | 294 | app.use(function (state) { 295 | // initialize state 296 | state.animals = {type: 'lion', x: 200, y: 100} 297 | }) 298 | 299 | // declare routes 300 | app.route('/', main) 301 | 302 | // ... 303 | ``` 304 | 305 | Underneath our initialization of `choo`, we are calling `app.use()` and passing a function into it. The function we pass in receives a `state` object which represents the state of our entire application. Although it has a wider scope of usage, for now we can just call `app.use()` when we want to setup the initial parameters of our application's state. 306 | 307 | Inside of `app.use()`, we're creating a new property on the `state` object called `state.animals`. We're then assigning it an object that contains parameters we'll use to place a new animal on our screen: `type` sets which animal we want, and the `x` and `y` properties will dictate the animal's position. 308 | 309 | ## Passing state into our templates 310 | Now that we've set a new property on our state object, we can access this state from any templates that our router calls. 311 | 312 | Let's flip over to `main.js`, and update our template function: 313 | 314 | ```js 315 | // import choo's template helper 316 | var html = require('choo/html') 317 | 318 | // export module 319 | module.exports = function (state) { 320 | var type = state.animals.type 321 | var x = state.animals.x 322 | var y = state.animals.y 323 | 324 | // create html template 325 | return html` 326 |
327 |
328 | 329 | 330 |
331 |
332 | ` 333 | } 334 | ``` 335 | 336 | A few things have changed, so let's briefly dive in to see what's happening: 337 | 338 | First, the function that we're exporting now takes in an argument called `state`. This is the same `state` object from `index.js`, after it has been run through `app.use()`. This means that the `state.animal` property is now available to us inside this template. 339 | 340 | Inside the function, we're initializing a set of variables that point to the different parts of our `state.animals` object. Then, inside of our template string, we've created a new `` element. This `` will represent an animal we place on screen. 341 | 342 | As mentioned earlier, we have several different images located inside an invisible `assets` folder in Glitch. They are: 343 | 344 | - `assets/bg.gif` 345 | - `assets/crocodile.gif` 346 | - `assets/koala.gif` 347 | - `assets/lion.gif` 348 | - `assets/tiger.gif` 349 | - `assets/walrus.gif` 350 | 351 | We can use the `state.animals.type` property that has been passed into our template to point to the correct image we need to render the animal on screen (`src="/assets/${type}.gif"`). 352 | 353 | Then, we can set a `style` property on the same `` element to position the animal using `left` and `top`. These correspond to our `state.animals.x` and `state.animals.y` values. 354 | 355 | You've probably noticed the interesting `${}` syntax being used inside of our template strings. By placing short JavaScript expressions inside these curly braces, the code will be evaluated and replaced with its returned value. In this case, `${type}` will evaluate to `'lion'`, `${x}` will evaluate to `200`, and so on. 356 | 357 | If we look at our application now, you should see something cute like this: 358 | 359 | ![first animal](images/starter-first-animal.png "Screenshot of first animal") 360 | 361 | Nice! A super sweet lion has decided to come and hang out with us while we code. 362 | 363 | That was a lot to digest, so let's quickly go over what we just did: 364 | 365 | - First, we setup `app.use()` in our `index.js` file. 366 | - Inside of `app.use()`, we then initialized our app's `state` object by describing the type and coordinates of an animal. 367 | - Then, we updated our `main.js` file to reflect the information stored in state. 368 | 369 | As a result, an animal is now rendered on the screen. 370 | 371 | It would be cool to have more animals roaming around without copying and pasting additional `` elements into our template. Instead, we should add more animals into our state, then have our template reflect those changes. 372 | 373 | ## Rendering templates with loops 374 | The `state.animals` property currently refers to a single object, representing one animal. If we want to render multiple animals on the screen, we'd need a way of representing multiple animals inside this property. 375 | 376 | Using an array, we can store multiple objects inside of it, each representing an animal that the user should see. 377 | 378 | Let's head to `index.js` and update our initialized state to reflect this change: 379 | 380 | ```js 381 | // ... 382 | 383 | // initialize choo 384 | var app = choo() 385 | 386 | app.use(function (state) { 387 | // initialize state 388 | state.animals = [ 389 | {type: 'lion', x: 200, y: 100}, 390 | {type: 'crocodile', x: 50, y: 300} 391 | ] 392 | }) 393 | 394 | // declare routes 395 | app.route('/', main) 396 | 397 | // ... 398 | ``` 399 | 400 | `state.animals` now refers to both a Lion and a Crocodile, but `main.js` also needs to be updated to handle this restructure. 401 | 402 | Our template should render each animal regardless of how many there are in state. To do this, we can program our template to iterate across `state.animals`, rendering a block of HTML for each animal that exists in the array. 403 | 404 | First, let's create a new file in Glitch with the filepath `templates/animal.js`. This template will represent a single animal, and will be used to iterate over `state.animals` inside of `main.js`. 405 | 406 | Let's wire up `animal.js` with the following code: 407 | 408 | ```js 409 | // import choo's template helper 410 | var html = require('choo/html') 411 | 412 | // export module 413 | module.exports = function (animal) { 414 | var type = animal.type 415 | var x = animal.x 416 | var y = animal.y 417 | 418 | // create html template 419 | return html` 420 | 421 | ` 422 | } 423 | ``` 424 | 425 | This code you've seen before, as it already exists inside of `main.js`. We're breaking this template out into its own file so that we can refer to it each time we iterate over `state.animals`. Let's do that now by updating `main.js`: 426 | 427 | ```js 428 | // import choo's template helper 429 | var html = require('choo/html') 430 | 431 | // import template 432 | var animal = require('./animal.js') 433 | 434 | // export module 435 | module.exports = function (state) { 436 | // create html template 437 | return html` 438 |
439 |
440 | 441 | ${state.animals.map(animal)} 442 |
443 |
444 | ` 445 | } 446 | ``` 447 | 448 | In `main.js`, we've replaced the HTML markup we shifted into `animal.js` with `${state.animals.map(animal)}`. What is happening here? 449 | 450 | In JavaScript, arrays come with a built-in method called `map()`. When we call `map()`, we also pass into it another function which is executed against every item in the array. Each time the function runs, the current item is passed in as an argument. When `map()` is finished running, it will return a new array with each function's return value. 451 | 452 | For example: 453 | 454 | ```js 455 | var array = [1, 2, 3] 456 | var newArray = array.map(function (num) { 457 | return num * 2 458 | }) 459 | 460 | console.log(newArray) 461 | // [2, 4, 6] 462 | ``` 463 | 464 | In the context of our application, we are using `map()` to iterate across each animal in our `state.animals` array. By passing our `animal.js` template into `map()`, we can then create a new array of templates that each represent an animal. These are then rendered in place of `${state.animals.map(animal)}`. 465 | 466 | If you look again inside of `animal.js`, you'll see that an `animal` is passed in as an argument each time the exported function is run. We are then grabbing the `type`,`x`, and `y` values of the animal currently being iterated upon, and returning that new template. 467 | 468 | If we now look at our application, we should see that a new friend has arrived! 469 | 470 | ![second animal](images/starter-second-animal.png "Screenshot of second animal") 471 | 472 | Now is a good time to quickly summarise what we just did: 473 | 474 | - First, we changed our state to reflect not just one animal (a single object), but many animals (an array of objects). 475 | - We then created a new template (`animal.js`) which outputs an `` element, representing one animal. 476 | - Finally, using JavaScript's `map()` function, we "mapped" across `state.animals` inside our `main.js` file. This lets us render each animal inside of `state.animals` to the screen. 477 | 478 | It would be nice if we could add new animals to our application simply by clicking somewhere on the grass, rather than having to update our code by hand to reflect this. Let's figure out a way to update our `state` directly from our templates. 479 | 480 | ## Updating state 481 | As mentioned several times already, *"templates are functions. Their output will change depending on their input"*. 482 | 483 | In the last section of this guide, we began passing our application's state down into our templates. Specifically, their output now depends on what is contained in `state.animals`. 484 | 485 | What would happen if we changed the contents of our state, while the application is still running? Let's find out! 486 | 487 | Looking at our `index.js` file, we initialize our state by passing a function into `app.use()` that modifies the `state` object. If we could run another function inside of this same function, but trigger it directly from our template, we can then update our state at any point in time, which should also update what we see on the screen. 488 | 489 | Let's update `index.js` (specifically, our `app.use()` function): 490 | 491 | ```js 492 | // ... 493 | 494 | app.use(function (state, emitter) { 495 | // initialize state 496 | state.animals = [ 497 | {type: 'lion', x: 200, y: 100}, 498 | {type: 'crocodile', x: 50, y: 300} 499 | ] 500 | 501 | // add animal 502 | emitter.on('addAnimal', function () { 503 | var obj = {type: 'lion', x: 100, y: 200} 504 | state.animals.push(obj) 505 | 506 | emitter.emit('render') 507 | }) 508 | }) 509 | 510 | // ... 511 | ``` 512 | 513 | In this code block, we've introduced a new object called `emitter`, which is available to us inside of the function we pass into `app.use()`. 514 | 515 | `emitter` is what we could refer to as either an "event bus" or a "message bus". It listens for events that occur in other parts of our application, and reacts to them by executing a function. 516 | 517 | Underneath our initialization of `state.animals`, we're calling `emitter.on()`. This function takes two arguments: 518 | 519 | - The name of the "event" to listen for (specified as a string. We chose `addAnimal`, but this can be anything we like.) 520 | - A function that will run when the event is triggered 521 | 522 | Inside the function, we're creating a new lion object, and pushing it into our `state.animals` array. At this point, our state has now been modified, but we then need to tell `choo` to re-render our templates to reflect these changes. 523 | 524 | To do this, we're calling `emitter.emit('render')`. This triggers a pre-configured event handler inside of `choo`, that tells the app to re-render the page. 525 | 526 | With this all set up, we now need a way to trigger the `add` event handler from one of our templates. 527 | 528 | Let's update our template inside of `main.js` to show the following: 529 | 530 | ```js 531 | // import choo's template helper 532 | var html = require('choo/html') 533 | 534 | // import template 535 | var animal = require('./animal.js') 536 | 537 | // export module 538 | module.exports = function (state, emit) { 539 | // create html template 540 | return html` 541 |
542 |
543 | 544 | ${state.animals.map(animal)} 545 |
546 |
547 | ` 548 | 549 | // add new animal to state 550 | function add () { 551 | emit('addAnimal') 552 | } 553 | } 554 | ``` 555 | 556 | There are three significant updates to this file: 557 | 558 | - As well as passing `state` into our function, we're now also passing in `emit`. 559 | 560 | - Our `` element now has an `onclick` property. This means that when the user clicks the image of the grass, a function called `add` will be triggered. 561 | 562 | - Below our template, we're declaring the `add` function referred to in our `` element's `onclick` property. Inside, we're triggering `emit('addAnimal')`, which will cause our `addAnimal` event handler, back in `index.js`, to execute. 563 | 564 | If we switch to our application again, and click on the grass, a new lion should appear: 565 | 566 | ![third animal](images/starter-third-animal.gif "Screenshot of third animal") 567 | 568 | ✨ Awesome! We now have our very first interactive `choo` app. ✨ 569 | 570 | We're not quite done yet, but we just implemented a lot of really cool new functionality, so let's dive deeper to understand exactly what's going on. 571 | 572 | `index.js` holds the state of our application, which is handled via the function we pass into `app.use()`. This function receives two arguments, `state` & `emitter`. 573 | 574 | ```js 575 | // example 576 | app.use(function (state, emitter) { 577 | state.number = 1 578 | 579 | emitter.on('changeNumber', function () { 580 | state.number = 2 581 | }) 582 | }) 583 | ``` 584 | 585 | The `state` object contains the data structures that drive our application. The rendered output of our templates change depending on what this data holds. 586 | 587 | We then use the `emitter` object to register functions that react to the execution of events elsewhere in our application. The names of the events we register can be anything we like. As they are registered inside of the function we pass into `app.use()`, they have the ability to modify the `state` object directly. 588 | 589 | In `main.js`, we have access to both the `state` object, and an `emit` function. These are both passed into any template functions that are registered with a route (eg. `app.route('/', main)`). 590 | 591 | ```js 592 | // example 593 | module.exports = function (state, emit) { 594 | return html` 595 |
596 | ${state.number} 597 |
598 | ` 599 | 600 | function update () { 601 | emit('changeNumber') 602 | } 603 | } 604 | ``` 605 | 606 | We can use the `state` object to render useful bits of information inside of our templates. We can also continue passing that information further down into child templates. 607 | 608 | We can use the `emit()` function to trigger updates to our `state` object, from our templates. If we register `emitter.on('cats')` inside of `app.use()`, this can be triggered by `emit('cats')` somewhere else. 609 | 610 | Finally, if we want to trigger an event by clicking on an element in one of our templates, we can register a function on that element using the `onclick` property (eg. `
`). 611 | 612 | ## Passing data to state 613 | Although our app is now interactive, it's not as fun as it could be. 614 | 615 | When we add an animal to our `state` object, we're adding the same animal (a Lion), and giving it the same coordinates each time. To make this more interesting, when a new animal is created, our app should randomly choose from a set of different animals, and position it with the same coordinates as our cursor. 616 | 617 | To do this, we need a way to pass information to our state from our templates. This can be done via the event handlers we are triggering. 618 | 619 | Looking at our `main.js` file, we are executing our declared `add()` function when we click on the image of the grass. Inside of this function, we're executing `emit('addAnimal')`, which tells our handler back in `index.js` to update our `state` object. 620 | 621 | `emit()` actually accepts two arguments: 622 | 623 | - The first, which we already know, is the name of the event we'd like to trigger (eg. `'addAnimal'`). 624 | 625 | - The second is any data we'd like to pass through to the function on the other end. 626 | 627 | Let's update our `add()` function in `main.js`: 628 | 629 | ```js 630 | // ... 631 | 632 | function add (e) { 633 | var x = e.offsetX - 20 634 | var y = e.offsetY - 10 635 | 636 | var obj = {x: x, y: y} 637 | emit('addAnimal', obj) 638 | } 639 | 640 | // ... 641 | ``` 642 | 643 | Now, when `add()` executes after you click on the grass, two new things are happening: 644 | 645 | - First, we are grabbing the `offsetX` and `offsetY` information from the click event *(notice that `e`, the click event object, is now passed into this function)*. This tells us where we clicked our mouse, relative to the image of the grass. We're slightly adjusting these values to compensate for the cursor itself. 646 | 647 | - Then, we are calling `emit()`, specifying the name of the event listener we'd like to trigger, but also passing in an object that contains the `x` and `y` coordinates of where we clicked. 648 | 649 | Let's go back to `index.js` and make some changes to our `emitter.on('addAnimal')` handler: 650 | 651 | ```js 652 | // ... 653 | 654 | // add animal 655 | emitter.on('addAnimal', function (data) { 656 | var x = data.x 657 | var y = data.y 658 | 659 | var obj = {type: 'lion', x: x, y: y} 660 | state.animals.push(obj) 661 | 662 | emitter.emit('render') 663 | }) 664 | 665 | // ... 666 | ``` 667 | 668 | The function that we pass into this event listener, now takes in a new argument: `data`. This value represents the information that we passed into `emit()` back in `main.js` (the `x` and `y` coordinates). 669 | 670 | We can then construct an object with our coordinates, push this to `state.animals`, then re-render the screen. 671 | 672 | We did mention earlier that we'd like to randomly create a new type of animal for each click, so let's update our code again to reflect that: 673 | 674 | ```js 675 | // ... 676 | 677 | // add animal 678 | emitter.on('addAnimal', function (data) { 679 | var animals = ['crocodile', 'koala', 'lion', 'tiger', 'walrus'] 680 | 681 | var type = Math.floor(Math.random() * 5) 682 | var x = data.x 683 | var y = data.y 684 | 685 | var obj = {type: animals[type], x: x, y: y} 686 | state.animals.push(obj) 687 | 688 | emitter.emit('render') 689 | }) 690 | 691 | // ... 692 | ``` 693 | 694 | Now before pushing a new object to `state.animals`, we randomly select an index from an array of 5 names, and use this as our animal's `type` property. 695 | 696 | If we switch over to our application again, and begin clicking different areas of the grass, we should see something like this: 697 | 698 | ![random animals](images/starter-random-animals.gif "Screenshot of random animals") 699 | 700 | *LOOK AT HOW CUTE THEY ALL ARE!* 701 | 702 | Take a moment to appreciate everything you've just built! Our `choo` app is really starting to shape up. 703 | 704 | Let's quickly revisit what we accomplished in this last section: 705 | 706 | - In our template (`main.js`), we obtained the `x` and `y` coordinates of our mouse click event. 707 | 708 | - We then passed this information through to the `addAnimal` event handler using `emit('addAnimal', obj)`. 709 | 710 | - Over in `index.js`, we received this data inside our `emitter.on('addAnimal')` handler as `data`, and proceeded to push this into our `state.animals` array. 711 | 712 | At this point, we've made a super fun interactive `choo` app that we can share with all our friends! 713 | 714 | __*ProTip™*__: you can send the link of your Glitch app to anyone who'd like to play with your app, or hack on your code with you via Glich's `Share` or `🕶 Show` buttons: 715 | 716 | ![share](images/starter-share.png "Screenshot of share") 717 | 718 | Now that we're on a roll, let's think of a way to build our next feature: removing animals from our app. 719 | 720 | ## Removing data from state 721 | If we can add an animal to our grass field by clicking in a particular spot, we should also be able to click on the animal itself and remove it from the screen. 722 | 723 | This means that instead of updating `state.animals` by adding a new item to its array, we would instead need to remove the item that correlates to the animal that was clicked. 724 | 725 | Let's start by opening `main.js` and update its code: 726 | 727 | ```js 728 | // import choo's template helper 729 | var html = require('choo/html') 730 | 731 | // import template 732 | var animal = require('./animal.js') 733 | 734 | // export module 735 | module.exports = function (state, emit) { 736 | // create html template 737 | return html` 738 |
739 |
740 | 741 | ${state.animals.map(animalMap)} 742 |
743 |
744 | ` 745 | 746 | // map function 747 | function animalMap (obj, i) { 748 | return animal(remove, obj, i) 749 | } 750 | 751 | // add new animal to state 752 | function add (e) { 753 | var x = e.offsetX - 20 754 | var y = e.offsetY - 10 755 | 756 | emit('addAnimal', {x: x, y: y}) 757 | } 758 | 759 | // remove animal from state 760 | function remove (e) { 761 | var index = e.target.id 762 | emit('removeAnimal', index) 763 | } 764 | } 765 | ``` 766 | 767 | There's a ton of new stuff here, so let's go through it carefully: 768 | 769 | First, we created a new function called `animalMap()`, which returns our `animal.js` template: 770 | 771 | ```js 772 | function animalMap (obj, i) { 773 | return animal(remove, obj, i) 774 | } 775 | ``` 776 | 777 | Then, near the top of this file, we're now passing our new `animalMap()` function into `state.animals.map`, rather than the `animal.js` template itself: 778 | 779 | ```js 780 | ${state.animals.map(animalMap)} 781 | ``` 782 | 783 | Finally, we created a new `remove()` function that we'll use to remove animals from our state. Let's come back to that shortly. 784 | 785 | You may have noticed that we're using this new `animalMap()` function we created to pass some new things into our `animal.js` template, before we run it through `map()`. Why are we doing this? 786 | 787 | To build this feature, our `animal.js` template needs access to some extra functions and information. The first thing we pass in is our new `remove()` function. Then, we pass in `obj`, which represents the animal that `map()` is iterating across. The final argument, `i`, represents the index number of the current `map()` iteration. 788 | 789 | These changes may seem confusing and abstract at first. Don't worry! Our next move will hopefully tie these changes together. 790 | 791 | Let's open `animal.js`, and update its code: 792 | 793 | ```js 794 | var html = require('choo/html') 795 | 796 | module.exports = function (onclick, animal, i) { 797 | var type = animal.type 798 | var x = animal.x 799 | var y = animal.y 800 | 801 | // create html template 802 | return html` 803 | 804 | ` 805 | } 806 | ``` 807 | 808 | We've made some subtle but important changes. Let's break these down: 809 | 810 | - Our `animal.js` template now accepts the three arguments we described above: the `remove()` function passed in from `main.js` (represented as `onclick()`), the current `animal` we're iterating over, and also that animal's index number (represented as `i`). 811 | 812 | - We've added an `id` property to our `` element, which accepts the value of `i`. We use this to identify which animal we are clicking, and where it sits within our `state.animals` array. 813 | 814 | - We've also added an `onclick` property to our `` element, which will trigger the `remove()` function we passed in from `main.js`. 815 | 816 | Let's open `index.js`, and update `app.use()`: 817 | 818 | ```js 819 | // ... 820 | 821 | app.use(function (state, emitter) { 822 | // initialize state 823 | state.animals = [ 824 | { type: 'lion', x: 200, y: 100 }, 825 | { type: 'crocodile', x: 50, y: 300 } 826 | ] 827 | 828 | // add animal 829 | emitter.on('addAnimal', function (data) { 830 | var animals = ['crocodile', 'koala', 'lion', 'tiger', 'walrus'] 831 | 832 | var type = Math.floor(Math.random() * 5) 833 | var x = data.x 834 | var y = data.y 835 | 836 | var obj = { type: animals[type], x: x, y: y } 837 | state.animals.push(obj) 838 | 839 | emitter.emit('render') 840 | }) 841 | 842 | // remove animal 843 | emitter.on('removeAnimal', function (i) { 844 | state.animals.splice(i, 1) 845 | emitter.emit('render') 846 | }) 847 | }) 848 | 849 | // ... 850 | ``` 851 | 852 | Towards the bottom of this function, we've added `emitter.on('removeAnimal')`. When we run `emit('removeAnimal')` from `main.js`, this is the block of code it triggers. It takes the index number of the animal that we clicked (this was stored on the ``'s `id` property), and removes it from `state.animals` using JavaScript's built-in `splice()` function. 853 | 854 | Afterwards, we run `emitter.emit('render')`, which tells `choo` to re-render the screen. 855 | 856 | Let's check our application, and see if this works: 857 | 858 | ![deletion](images/starter-delete-animals.gif "Screenshot of deletion") 859 | 860 | Cool! We can add animals to our screen, and also remove them. Talk about cuteness overload! 861 | 862 | If we added a large number of animals to our plot of grass, it would be useful if we could filter the screen so we only see a specific type of animal. 863 | 864 | Let's build this final feature to round out the guide, and finish our `choo-animals` application! 865 | 866 | ## Creating dynamic routes 867 | Before we create this new filter, let's first specify how we'd like the functionality to work. 868 | 869 | At present, our application works entirely from one route (`'/'`). It would be cool to build a second route that allows us to dynamically specify which animal we'd like to filter for. What does this mean? 870 | 871 | Our app can currently choose from five different animals to add to the screen. Rather than creating five different routes, one to filter each animal (eg. `app.route('/filter/lion'), app.route('/filter/tiger')`), we can create one route that handles any type of animal we ask for. 872 | 873 | Below our first and only route in `index.js`, let's add a second one: 874 | 875 | ```js 876 | // ... 877 | 878 | // declare routes 879 | app.route('/', main) 880 | app.route('/filter/:type', main) 881 | 882 | // start app 883 | app.mount('div') 884 | ``` 885 | 886 | You may have noticed that the route we just declared (`/filter/:type`) looks slightly funny. What does `:type` mean? 887 | 888 | If we clicked on a link in our application that directed us to `/filter/lion`, or `/filter/tiger`, we'd be directed to the route we just declared. The `:` syntax tells the router that it will accept any value for this section of the route's path. The router will then make that value available to our application so it can respond to the user in a different way. 889 | 890 | In this particular context, the last value of this URL's path becomes available to our application via `state.params.type`. If our route was declared as `/filter/:meow`, the value would become available to us via `state.params.meow`. 891 | 892 | This means that when our user is directed to a specific `/filter` route, our application can see what type of animal they're asking for, and filter its output accordingly. 893 | 894 | Our new route also points to `main.js`, so let's add new code to that template's `animalMap()` function, so it can filter for specific animals: 895 | 896 | ```js 897 | // ... 898 | 899 | // add new animal to state 900 | function add (e) { 901 | var x = e.offsetX - 20 902 | var y = e.offsetY - 10 903 | 904 | emit('addAnimal', {x: x, y: y}) 905 | } 906 | 907 | // map function 908 | function animalMap (obj, i) { 909 | var type = state.params.type 910 | 911 | if (type && type !== obj.type) { 912 | return // nothing 913 | } else { 914 | return animal(remove, obj, i) 915 | } 916 | } 917 | 918 | // ... 919 | ``` 920 | 921 | This update to `animalMap()` checks whether we've navigated to our `/filter/:type` route. If we have, `state.params.type` would contain a string of the animal we're looking for. If not, this value would be empty. 922 | 923 | If this value contains a string, but does *not* match the `type` of animal our `map()` function is currently iterating over, we exit this function early by returning nothing. This means we don't render anything to the screen during this iteration. In all other circumstances (if there *is* a match, or we aren't filtering at all), then we render the animal to the screen during that iteration. 924 | 925 | To activate our filter, let's add some new template markup to our `main.js` template, which renders a list of anchor elements, each linking out to a specific animal's filter: 926 | 927 | ```js 928 | // ... 929 | 930 | // create html template 931 | return html` 932 |
933 |
934 | 935 | ${state.animals.map(animalMap)} 936 |
937 |
938 | 946 |
947 |
948 | ` 949 | 950 | // ... 951 | ``` 952 | 953 | Let's switch to our application, and see what happens: 954 | 955 | ![filter](images/starter-filter.gif "Screenshot of filter") 956 | 957 | That's so cool! When we click on a filter, our app redirects to a new dynamic route, and our `animalMap()` function handles which animals need to appear on the screen. 958 | 959 | ## Summary 960 | 961 | Woah, we just finished building `choo-animals`. Congratulations! 962 | 963 | I hope you had a lot of fun making this app, just as much as I had fun writing this guide. 964 | 965 | Before we wrap up, let's write down a list of all the cool things we learned to do with `choo`. We learned how to: 966 | 967 | - Create templates 968 | - Create static & dynamic routes 969 | - Create & update application state 970 | - Modularise our code 971 | - Render templates with loops 972 | - Pass data around our application 973 | 974 | These concepts form a large part of any modern web app, not just those built with `choo`, but with other libraries and frameworks as well. 975 | 976 | In the following chapters of this handbook, you'll dive into other topics such as form handling, third-party library integration, and sending & receiving data from external APIs. 977 | -------------------------------------------------------------------------------- /content/your-first-choo-app/images/animals.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/animals.gif -------------------------------------------------------------------------------- /content/your-first-choo-app/images/remix-this.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/remix-this.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/show.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-blank.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-delete-animals.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-delete-animals.gif -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-filter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-filter.gif -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-first-animal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-first-animal.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-first-route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-first-route.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-grass.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-new-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-new-file.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-random-animals.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-random-animals.gif -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-readme.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-second-animal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-second-animal.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-share.png -------------------------------------------------------------------------------- /content/your-first-choo-app/images/starter-third-animal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/content/your-first-choo-app/images/starter-third-animal.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "choo-handbook", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "rm -rf dist && mkdir dist && cp -r src/* dist", 7 | "test": "standard" 8 | }, 9 | "devDependencies": { 10 | "standard": "^10.0.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/images/animals.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/images/animals.gif -------------------------------------------------------------------------------- /src/images/choo-animals-mobile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/images/choo-animals-mobile.gif -------------------------------------------------------------------------------- /src/images/choo-animals.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/images/choo-animals.gif -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | choo handbook 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 14 | 18 |
19 |
20 |
21 |
22 |
23 | 29 |
30 |

Your first choo app

31 |

This guide will take you through your first steps as you build an interactive web app with choo.

32 |

If you're comfortable with the basics of JavaScript, HTML & CSS, but you've never built an interactive web app before, this guide is for you.

33 |
34 |
35 |
36 |
37 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | 27 | body { 28 | line-height: 1; 29 | } 30 | 31 | ol, ul { 32 | list-style: none; 33 | } 34 | 35 | blockquote, q { 36 | quotes: none; 37 | } 38 | 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content: ''; 42 | content: none; 43 | } 44 | 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | 51 | 52 | body { 53 | background: #F5F5F5; 54 | color: #333; 55 | font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 56 | font-weight: 400; 57 | font-size: 18px; 58 | letter-spacing: 0.01rem; 59 | } 60 | 61 | @media (max-width: 414px) { 62 | body {font-size: 16px; } 63 | } 64 | 65 | 66 | 67 | header { 68 | background: #FFB7C0; 69 | padding: 2rem; 70 | display: flex; 71 | flex-direction: row; 72 | align-items: center; 73 | justify-content: center; 74 | } 75 | 76 | @media (max-width: 414px) { 77 | header { 78 | padding: 1rem; 79 | } 80 | } 81 | 82 | 83 | 84 | .container { 85 | width: 800px; 86 | display: flex; 87 | justify-content: space-between; 88 | } 89 | 90 | @media (max-width: 850px) { 91 | .container { width: 100%; } 92 | } 93 | 94 | header > .container { flex-direction: row; } 95 | section > .container { flex-direction: column; } 96 | 97 | 98 | 99 | .logo { 100 | width: 14rem; 101 | font-weight: 600; 102 | font-size: 36px; 103 | text-transform: uppercase; 104 | } 105 | 106 | @media (max-width: 414px) { 107 | .logo { 108 | font-size: 30px; 109 | text-align: center; 110 | width: 100%; 111 | } 112 | } 113 | 114 | .logo > a { text-decoration: none; } 115 | 116 | .o { margin: 0 1.25rem 0 0.6rem; } 117 | 118 | 119 | 120 | nav { 121 | display: flex; 122 | flex-direction: row; 123 | align-self: flex-end; 124 | } 125 | 126 | @media (max-width: 414px) { 127 | nav { 128 | display: none; 129 | } 130 | } 131 | 132 | nav > a { 133 | margin: 0 1rem 0 0; 134 | text-decoration: none; 135 | font-size: 20px; 136 | font-weight: 500; 137 | } 138 | 139 | nav > a:last-child { margin: 0; } 140 | 141 | 142 | 143 | section { 144 | padding: 2rem; 145 | line-height: 1.5rem; 146 | display: flex; 147 | flex-direction: row; 148 | align-items: center; 149 | justify-content: center; 150 | } 151 | 152 | 153 | 154 | article { margin: 0 0 3rem 0; } 155 | 156 | @media (max-width: 414px) { 157 | article { 158 | flex-direction: column; 159 | margin: 0 0 2.5rem 0; 160 | } 161 | } 162 | 163 | article:last-child { 164 | margin: 0 0 1rem 0; 165 | } 166 | 167 | article.home { display: flex; } 168 | 169 | 170 | 171 | .preview-image { 172 | display: flex; 173 | flex-direction: column; 174 | width: 13rem; 175 | font-size: 16px; 176 | line-height: 1.25rem; 177 | margin: 0 2.5rem 0 0; 178 | } 179 | 180 | @media (max-width: 414px) { 181 | .preview-image { 182 | flex-direction: row; 183 | width: initial; 184 | margin:0 0 0.25rem 0; 185 | } 186 | 187 | .preview-image > span { margin-right: 1rem; } 188 | } 189 | 190 | .preview-image > a { text-decoration: none; } 191 | 192 | .preview-text { max-width: 40rem; } 193 | 194 | 195 | 196 | footer { 197 | color: #ffb7c0; 198 | font-size: 16px; 199 | display: flex; 200 | justify-content: center; 201 | margin: 0 0 2rem 0; 202 | } 203 | 204 | footer > span:first-child { 205 | margin-right: 1rem; 206 | } 207 | 208 | 209 | 210 | .title { 211 | margin: 1rem 0 2rem 0; 212 | text-align: center; 213 | } 214 | 215 | @media (max-width: 414px) { 216 | .title { margin: 1rem 0 2.25rem 0; } 217 | } 218 | 219 | 220 | 221 | h1, h2, h3 { 222 | font-weight: 500; 223 | text-decoration: underline; 224 | margin: 3rem 0 1.5rem 0; 225 | line-height: 1.75rem; 226 | letter-spacing: 0.05rem; 227 | } 228 | 229 | h1 { 230 | font-size: 32px; 231 | margin: 3rem 0 1rem 0; 232 | } 233 | 234 | h1:first-child { margin-top: 0; } 235 | 236 | h2 { font-size: 24px; } 237 | h2:first-child { margin-top: 0; } 238 | 239 | @media (max-width: 414px) { 240 | h1 { margin: 2.5rem 0 0.5rem 0; } 241 | h2, h3 { margin: 2.5rem 0 1rem 0; } 242 | } 243 | 244 | 245 | 246 | a { color: #333; } 247 | a:hover { color: #2dbcff; } 248 | 249 | 250 | 251 | img { max-width: 100%; } 252 | img.mobile { margin: 0 0 1rem 0; } 253 | 254 | @media (max-width: 414px) { 255 | img.mobile { display: inherit; } 256 | img.desktop { display: none; } 257 | } 258 | @media (min-width: 415px) { 259 | img.mobile { display: none; } 260 | img.desktop { display: inherit; } 261 | } 262 | 263 | 264 | 265 | p { margin-bottom: 1.5rem; } 266 | p:last-child { margin-bottom: 0; } 267 | 268 | 269 | 270 | pre { 271 | background-color: #eee; 272 | padding: 1rem; 273 | margin: 0 0 2rem 0; 274 | overflow: auto; 275 | } 276 | 277 | code { 278 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 279 | padding: 0.2rem 0.4rem; 280 | font-size: 80%; 281 | background-color: rgba(0,0,0,0.04); 282 | border-radius: 3px; 283 | } 284 | 285 | pre code { 286 | background-color: #eee; 287 | } 288 | 289 | 290 | 291 | ul { margin: 0 0 2rem 0; } 292 | 293 | ul li { 294 | list-style-type: disc; 295 | margin: 0 0 0.25rem 3rem; 296 | padding: 0 0 0 1rem; 297 | } 298 | -------------------------------------------------------------------------------- /src/your-first-choo-app/images/animals.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/animals.gif -------------------------------------------------------------------------------- /src/your-first-choo-app/images/remix-this.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/remix-this.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/show.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-blank.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-delete-animals.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-delete-animals.gif -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-filter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-filter.gif -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-first-animal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-first-animal.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-first-route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-first-route.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-grass.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-new-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-new-file.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-random-animals.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-random-animals.gif -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-readme.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-second-animal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-second-animal.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-share.png -------------------------------------------------------------------------------- /src/your-first-choo-app/images/starter-third-animal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choojs/choo-handbook/76712d20322504ac255eee55eae521b0cd292345/src/your-first-choo-app/images/starter-third-animal.gif -------------------------------------------------------------------------------- /src/your-first-choo-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | choo handbook - your first choo app 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 14 | 18 |
19 |
20 |
21 |
22 |
23 |

Your first choo app

24 | April 18, 2017 ✨ by louis 25 |
26 |
27 |

Who is this guide for?

28 |

If you're comfortable with the basics of JavaScript, HTML & CSS, but you've never built an interactive web app before, this guide is for you.

29 |

If you are a seasoned JavaScript developer with experience in other frameworks, and are looking to try choo for the first time, this guide is also for you.

30 |

What is choo?

31 |

choo is a small framework that helps you build web apps with JavaScript.

32 |

JavaScript makes it easy to add fun interactive elements to our HTML pages. As the language has gained in popularity, developers are now choosing to build their websites entirely with JavaScript. This offers many benefits to both users and developers, most notably that websites can now behave similarly to native desktop or mobile applications.

33 |

choo provides a small but powerful collection of tools that commonly feature in JavaScript web apps, such as templating, routing, and state management. By the end of this guide, you will understand what these terms mean. Don't be concerned if you don't already know what they are. This is why you are here!

34 |

What will we build?

35 |

For this guide, we're going to build an interactive animal simulator called choo-animals. It will be informative, cute, but most of all, fun!

36 |

This is what it looks like:

37 |

screenshot

38 |

The user can click anywhere on the grass to add an animal to the screen. Clicking on an animal will remove it from the screen. The user can also filter which animals they see, using the filter links at the bottom of the page.

39 |

You can try it for yourself here: https://choo-animals.glitch.me

40 |

Let's get started!

41 |

For this guide, we will be using an online code editor called Glitch.

42 |

Glitch lets us write, edit and deploy JavaScript code in the browser. This is useful because we can make changes to our code and see the updated results instantly.

43 |

A starter project is available for you to follow along with this guide: https://glitch.com/edit/#!/project/choo-animals-starter

44 |

When the editor has finished loading, you should make your own copy of the project so you can start coding. To do this, click the "Remix this" button near the top left hand corner of the window:

45 |

remix

46 |

Your screen should now looking something like the following:

47 |

starter

48 |

Starting choo

49 |

In our project's left sidebar, click on the index.js file to open its code in the code editor.

50 |

You will see the following:

51 |
// import choo
 52 | var choo = require('choo')
 53 | 
 54 | // initialize choo
 55 | var app = choo()
 56 | 
 57 | // start app
 58 | app.mount('div')
 59 | 
60 |

At the top of this file, we are importing the choo framework into our project using the require() function.

61 |

require() lets us import other JavaScript files into our code. require() does a lot more than this, but if you're not already familiar with its concepts, for this tutorial all you need to remember is that we will use it to import choo, and a handful of other files we write ourselves into our application.

62 |

We then need to create an instance of choo that we will build our app with. We initialize this instance using choo(), and then store it in the app variable. From this point onwards, when we interact with choo, it will be via app.

63 |

Finally, we start our application by appending it to the <div> element of the HTML page our app will run on. We do this using the app.mount() method that choo conveniently makes available to us. If you open the index.html file, you'll see there is a single <div> element inside <body>. This is where app.mount() attaches our application.

64 |

At this point, we can take a look at what our app currently looks like. To do this, press the "Show" button near the top left hand corner of the window:

65 |

show

66 |

Our application should appear in a new browser tab. However, as you'll see, the screen will be blank:

67 |

blank

68 |

Our app does not yet contain any templates, nor any routes. This means that if we try to run our application, we will only ever see a blank page.

69 |

Let's fix this!

70 |

Building a template

71 |

In web app development, a template often refers to a piece of code that will help us render some HTML to the screen. Being templates, they will render most of the same information each time, but change slightly depending on the different bits of input they receive.

72 |

Templates are essentially functions: given some input, they will render a new output depending on what happens inside that function.

73 |

choo provides another module we can import that will help us build HTML templates with "template strings", one of JavaScript's newer syntax features.

74 |

Let's update the code in our index.js file to read the following:

75 |
// import choo
 76 | var choo = require('choo')
 77 | 
 78 | // import choo's template helper
 79 | var html = require('choo/html')
 80 | 
 81 | // initialize choo
 82 | var app = choo()
 83 | 
 84 | // create a template
 85 | var main = function () {
 86 |   return html`<div>choo animals</div>`
 87 | }
 88 | 
 89 | // start app
 90 | app.mount('div')
 91 | 
92 |

Underneath our choo import near the top of the file, we're now also importing choo's html helper. This becomes accessible via the html variable.

93 |

Towards the end of the file, we're creating a function, and assigning it to the main variable. Inside this function, we're returning a template that we'll use to render <div>choo animals</div> to our page.

94 |

This bit of code might be confusing, so let's dissect further.

95 |

The html variable references a function that parses template strings containing HTML code (eg. html`<h1>choo</h1>` ). A given template string will be interpreted into a special data structure, that choo then renders as HTML on the page.

96 |

Creating a route

97 |

When a friend links you to a specific page on a website, that URL acts as a route so the server knows where to direct you.

98 |

Many JavaScript web apps use this same concept to figure out which template to show you at a given point within the application.

99 |

This means that to see something on our page, we need to create a route that directs us to the template we just made. Let's add some code underneath our template:

100 |
// ...
101 | 
102 | // create a template
103 | var main = function () {
104 |   return html`<div>choo animals</div>`
105 | }
106 | 
107 | // create a route
108 | app.route('/', main)
109 | 
110 | // start app
111 | app.mount('div')
112 | 
113 |

The app.route() function takes in two arguments, the first being the URL path you want to create (this should be specified as a string), and the second being the template you'd like to show users when they arrive at that path (usually a variable that references the template).

114 |

As this is our first route, we will make it the "index" (or "root") of the entire application. We do this by specifying the route as '/'. This route would be the equivalent of visiting something like https://choo.io (the home page). Creating a route with the path /cats would be the equivalent of visiting https://choo.io/cats.

115 |

As we change the code in Glitch's editor, our app's window should automatically update itself. If you open it, you should see the following:

116 |

first route

117 |

If your app hasn't updated yet, you can simply refresh the window yourself.

118 |

Now that we can see text on the screen, it means that you successfully got choo working. Congratulations!

119 |

We're not finished yet, but let's quickly summarise what we've done so far:

120 |
    121 |
  • First, we initialised choo, and mounted it onto the page.
  • 122 |
  • We then created a template using the choo/html template function.
  • 123 |
  • Finally, we created an index route (/), and pointed it to our newly made template.
  • 124 |
125 |

Modularising our templates

126 |

Just like we can require() third-party modules like choo into our application, we can break our code down into smaller pieces so that the different concerns of our app exist in their own neat little files.

127 |

If we were to modularise what we currently have, it would likely be considered "pre-optimisation", since there's not much code there anyway. Attempts to break it out further would likely just increase complexity, but as we're about to add a lot of new markup to our main template in just a moment, let's go ahead and break it out into its own file.

128 |

In Glitch's left sidebar, click the "+ New File" button. Specify your new file's path as templates/main.js, then click Add File:

129 |

new file

130 |

First this creates a new folder named templates, and then adds a file called main.js inside of it. This file should now appear in the sidebar.

131 |

Let's add some code to our new main.js file:

132 |
// import choo's template helper
133 | var html = require('choo/html')
134 | 
135 | // export module
136 | module.exports = function () {
137 |   // create html template
138 |   return html`
139 |     <div class="container">
140 |       <div class="grass">
141 |         <img src="/assets/bg.gif" />
142 |       </div>
143 |     </div>
144 |   `
145 | }
146 | 
147 |

What's happening here?

148 |

First, we are importing choo's html helper so that we can build a new template within this file.

149 |

Next, we are assigning a function that returns a template to the variable called module.exports. What does this variable do exactly? In the context of this application, when we create JavaScript files that we'd like to import into other JavaScript files, whatever is assigned to module.exports is what will be exported out.

150 |

Inside of our template code, we've added some markup that will display an image called bg.gif on the page.

151 |

(NOTE: There are some asset files this project requires that you won't be able to see in Glitch's left sidebar. This image is an example of one those files. Other assets will crop up later in this tutorial, but I'll be sure to point them out to you.)

152 |

Now let's require() this new template into our index.js file, and see the results.

153 |

Start by adding this line towards the top of index.js:

154 |
// ...
155 | 
156 | // import choo's template helper
157 | var html = require('choo/html')
158 | 
159 | // import template
160 | var main = require('./templates/main.js')
161 | 
162 | // initialize choo
163 | var app = choo()
164 | 
165 | // ...
166 | 
167 |

Underneath the line of code where we import choo's html helper, we now import the main.js file we just created. Remembering that we exported a function that returns a template, we can now use that as the designated template for our / route.

168 |

Let's clean up index.js:

169 |
// import choo
170 | var choo = require('choo')
171 | 
172 | // import choo's template helper
173 | var html = require('choo/html')
174 | 
175 | // import template
176 | var main = require('./templates/main.js')
177 | 
178 | // initialize choo
179 | var app = choo()
180 | 
181 | // create a route
182 | app.route('/', main)
183 | 
184 | // start app
185 | app.mount('div')
186 | 
187 |

If we look at our application, we should now see the following:

188 |

grass

189 |

Awesome! Let's recap what we just did:

190 |
    191 |
  • First, we created a file called main.js inside of a new folder called templates.
  • 192 |
  • Inside main.js, we created a function that returns a template, and then exported it using module.exports.
  • 193 |
  • Finally, we imported main.js into index.js, then plugged it into our / route.
  • 194 |
195 |

Now that we have a nice grass field to play in, let's start adding some animals.

196 |

Adding state to our application

197 |

A core behaviour of many modern web applications is that they can be entirely driven and interacted with, without having to refresh or navigate to another static HTML page.

198 |

When you browse a website like Wikipedia or Mozilla Developer Network, each new page you navigate to, or each piece of functionality you interact with, will require a new page from the server to load in your browser.

199 |

In choo, the idea is that when information is changed or new information becomes available, the page will automatically update itself to reflect that, much in the same way that a native desktop or mobile application would.

200 |

To do this, choo uses the concept of "application state" to drive the output of its templates. This could mean absolutely nothing to you right now, so it's a good opportunity to dive further into what "state" is and how it works.

201 |

In programming, it's commonplace for a script to have several variables which contain information that you pass into a function. A function will typically do the same thing each time you run it, so when you pass the same variables in as arguments again and again, it will keep returning the same result:

202 |
// example
203 | var fun = true
204 | var name = 'Alice'
205 | 
206 | function sentence() {
207 |   if (fun) {
208 |     console.log(`Yay! ${name} is having fun!`)
209 |   } else {
210 |     console.log(`Oh no, ${name} is not having fun.`)
211 |   }
212 | }
213 | 
214 | sentence()
215 | sentence()
216 | sentence()
217 | 
218 | // the following is logged to screen:
219 | // "Yay! Alice is having fun!"
220 | // "Yay! Alice is having fun!"
221 | // "Yay! Alice is having fun!"
222 | 
223 |

As mentioned earlier in this guide:

224 |

"Templates are essentially functions: given some input, they will render a new output depending on what happens inside that function."

225 |

In choo, state is passed down into our templates so that we can change what our templates do depending on what is currently contained in that state.

226 |

Let's demonstrate this concept by adding new code to our index.js file:

227 |
// ...
228 | 
229 | // initialize choo
230 | var app = choo()
231 | 
232 | app.use(function (state) {
233 |   // initialize state
234 |   state.animals = {type: 'lion', x: 200, y: 100}
235 | })
236 | 
237 | // declare routes
238 | app.route('/', main)
239 | 
240 | // ...
241 | 
242 |

Underneath our initialization of choo, we are calling app.use() and passing a function into it. The function we pass in receives a state object which represents the state of our entire application. Although it has a wider scope of usage, for now we can just call app.use() when we want to setup the initial parameters of our application's state.

243 |

Inside of app.use(), we're creating a new property on the state object called state.animals. We're then assigning it an object that contains parameters we'll use to place a new animal on our screen: type sets which animal we want, and the x and y properties will dictate the animal's position.

244 |

Passing state into our templates

245 |

Now that we've set a new property on our state object, we can access this state from any templates that our router calls.

246 |

Let's flip over to main.js, and update our template function:

247 |
// import choo's template helper
248 | var html = require('choo/html')
249 | 
250 | // export module
251 | module.exports = function (state) {
252 |   var type = state.animals.type
253 |   var x = state.animals.x
254 |   var y = state.animals.y
255 | 
256 |   // create html template
257 |   return html`
258 |     <div class="container">
259 |       <div class="grass">
260 |         <img src="/assets/bg.gif" />
261 |         <img src="/assets/${type}.gif" style="left: ${x}px; top: ${y}px;" />
262 |       </div>
263 |     </div>
264 |   `
265 | }
266 | 
267 |

A few things have changed, so let's briefly dive in to see what's happening:

268 |

First, the function that we're exporting now takes in an argument called state. This is the same state object from index.js, after it has been run through app.use(). This means that the state.animal property is now available to us inside this template.

269 |

Inside the function, we're initializing a set of variables that point to the different parts of our state.animals object. Then, inside of our template string, we've created a new <img> element. This <img> will represent an animal we place on screen.

270 |

As mentioned earlier, we have several different images located inside an invisible assets folder in Glitch. They are:

271 |
    272 |
  • assets/bg.gif
  • 273 |
  • assets/crocodile.gif
  • 274 |
  • assets/koala.gif
  • 275 |
  • assets/lion.gif
  • 276 |
  • assets/tiger.gif
  • 277 |
  • assets/walrus.gif
  • 278 |
279 |

We can use the state.animals.type property that has been passed into our template to point to the correct image we need to render the animal on screen (src="/assets/${type}.gif").

280 |

Then, we can set a style property on the same <img> element to position the animal using left and top. These correspond to our state.animals.x and state.animals.y values.

281 |

You've probably noticed the interesting ${} syntax being used inside of our template strings. By placing short JavaScript expressions inside these curly braces, the code will be evaluated and replaced with its returned value. In this case, ${type} will evaluate to 'lion', ${x} will evaluate to 200, and so on.

282 |

If we look at our application now, you should see something cute like this:

283 |

first animal

284 |

Nice! A super sweet lion has decided to come and hang out with us while we code.

285 |

That was a lot to digest, so let's quickly go over what we just did:

286 |
    287 |
  • First, we setup app.use() in our index.js file.
  • 288 |
  • Inside of app.use(), we then initialized our app's state object by describing the type and coordinates of an animal.
  • 289 |
  • Then, we updated our main.js file to reflect the information stored in state.
  • 290 |
291 |

As a result, an animal is now rendered on the screen.

292 |

It would be cool to have more animals roaming around without copying and pasting additional <img> elements into our template. Instead, we should add more animals into our state, then have our template reflect those changes.

293 |

Rendering templates with loops

294 |

The state.animals property currently refers to a single object, representing one animal. If we want to render multiple animals on the screen, we'd need a way of representing multiple animals inside this property.

295 |

Using an array, we can store multiple objects inside of it, each representing an animal that the user should see.

296 |

Let's head to index.js and update our initialized state to reflect this change:

297 |
// ...
298 | 
299 | // initialize choo
300 | var app = choo()
301 | 
302 | app.use(function (state) {
303 |   // initialize state
304 |   state.animals = [
305 |     {type: 'lion', x: 200, y: 100},
306 |     {type: 'crocodile', x: 50, y: 300}
307 |   ]
308 | })
309 | 
310 | // declare routes
311 | app.route('/', main)
312 | 
313 | // ...
314 | 
315 |

state.animals now refers to both a Lion and a Crocodile, but main.js also needs to be updated to handle this restructure.

316 |

Our template should render each animal regardless of how many there are in state. To do this, we can program our template to iterate across state.animals, rendering a block of HTML for each animal that exists in the array.

317 |

First, let's create a new file in Glitch with the filepath templates/animal.js. This template will represent a single animal, and will be used to iterate over state.animals inside of main.js.

318 |

Let's wire up animal.js with the following code:

319 |
// import choo's template helper
320 | var html = require('choo/html')
321 | 
322 | // export module
323 | module.exports = function (animal) {
324 |   var type = animal.type
325 |   var x = animal.x
326 |   var y = animal.y
327 | 
328 |   // create html template
329 |   return html`
330 |     <img src="/assets/${type}.gif" style="left: ${x}px; top: ${y}px;">
331 |   `
332 | }
333 | 
334 |

This code you've seen before, as it already exists inside of main.js. We're breaking this template out into its own file so that we can refer to it each time we iterate over state.animals. Let's do that now by updating main.js:

335 |
// import choo's template helper
336 | var html = require('choo/html')
337 | 
338 | // import template
339 | var animal = require('./animal.js')
340 | 
341 | // export module
342 | module.exports = function (state) {
343 |   // create html template
344 |   return html`
345 |     <div class="container">
346 |       <div class="grass">
347 |         <img src="/assets/bg.gif" />
348 |         ${state.animals.map(animal)}
349 |       </div>
350 |     </div>
351 |   `
352 | }
353 | 
354 |

In main.js, we've replaced the HTML markup we shifted into animal.js with ${state.animals.map(animal)}. What is happening here?

355 |

In JavaScript, arrays come with a built-in method called map(). When we call map(), we also pass into it another function which is executed against every item in the array. Each time the function runs, the current item is passed in as an argument. When map() is finished running, it will return a new array with each function's return value.

356 |

For example:

357 |
var array = [1, 2, 3]
358 | var newArray = array.map(function (num) {
359 |   return num * 2
360 | })
361 | 
362 | console.log(newArray)
363 | // [2, 4, 6]
364 | 
365 |

In the context of our application, we are using map() to iterate across each animal in our state.animals array. By passing our animal.js template into map(), we can then create a new array of templates that each represent an animal. These are then rendered in place of ${state.animals.map(animal)}.

366 |

If you look again inside of animal.js, you'll see that an animal is passed in as an argument each time the exported function is run. We are then grabbing the type,x, and y values of the animal currently being iterated upon, and returning that new template.

367 |

If we now look at our application, we should see that a new friend has arrived!

368 |

second animal

369 |

Now is a good time to quickly summarise what we just did:

370 |
    371 |
  • First, we changed our state to reflect not just one animal (a single object), but many animals (an array of objects).
  • 372 |
  • We then created a new template (animal.js) which outputs an <img> element, representing one animal.
  • 373 |
  • Finally, using JavaScript's map() function, we "mapped" across state.animals inside our main.js file. This lets us render each animal inside of state.animals to the screen.
  • 374 |
375 |

It would be nice if we could add new animals to our application simply by clicking somewhere on the grass, rather than having to update our code by hand to reflect this. Let's figure out a way to update our state directly from our templates.

376 |

Updating state

377 |

As mentioned several times already, "templates are functions. Their output will change depending on their input".

378 |

In the last section of this guide, we began passing our application's state down into our templates. Specifically, their output now depends on what is contained in state.animals.

379 |

What would happen if we changed the contents of our state, while the application is still running? Let's find out!

380 |

Looking at our index.js file, we initialize our state by passing a function into app.use() that modifies the state object. If we could run another function inside of this same function, but trigger it directly from our template, we can then update our state at any point in time, which should also update what we see on the screen.

381 |

Let's update index.js (specifically, our app.use() function):

382 |
// ...
383 | 
384 | app.use(function (state, emitter) {
385 |   // initialize state
386 |   state.animals = [
387 |     {type: 'lion', x: 200, y: 100},
388 |     {type: 'crocodile', x: 50, y: 300}
389 |   ]
390 | 
391 |   // add animal
392 |   emitter.on('addAnimal', function () {
393 |     var obj = {type: 'lion', x: 100, y: 200}
394 |     state.animals.push(obj)
395 | 
396 |     emitter.emit('render')
397 |   })
398 | })
399 | 
400 | // ...
401 | 
402 |

In this code block, we've introduced a new object called emitter, which is available to us inside of the function we pass into app.use().

403 |

emitter is what we could refer to as either an "event bus" or a "message bus". It listens for events that occur in other parts of our application, and reacts to them by executing a function.

404 |

Underneath our initialization of state.animals, we're calling emitter.on(). This function takes two arguments:

405 |
    406 |
  • The name of the "event" to listen for (specified as a string. We chose addAnimal, but this can be anything we like.)
  • 407 |
  • A function that will run when the event is triggered
  • 408 |
409 |

Inside the function, we're creating a new lion object, and pushing it into our state.animals array. At this point, our state has now been modified, but we then need to tell choo to re-render our templates to reflect these changes.

410 |

To do this, we're calling emitter.emit('render'). This triggers a pre-configured event handler inside of choo, that tells the app to re-render the page.

411 |

With this all set up, we now need a way to trigger the add event handler from one of our templates.

412 |

Let's update our template inside of main.js to show the following:

413 |
// import choo's template helper
414 | var html = require('choo/html')
415 | 
416 | // import template
417 | var animal = require('./animal.js')
418 | 
419 | // export module
420 | module.exports = function (state, emit) {
421 |   // create html template
422 |   return html`
423 |     <div class="container">
424 |       <div class="grass">
425 |         <img src="/assets/bg.gif" onclick=${add} />
426 |         ${state.animals.map(animal)}
427 |       </div>
428 |     </div>
429 |   `
430 | 
431 |   // add new animal to state
432 |   function add () {
433 |     emit('addAnimal')
434 |   }
435 | }
436 | 
437 |

There are three significant updates to this file:

438 |
    439 |
  • As well as passing state into our function, we're now also passing in emit.

    440 |
  • 441 |
  • Our <img> element now has an onclick property. This means that when the user clicks the image of the grass, a function called add will be triggered.

    442 |
  • 443 |
  • Below our template, we're declaring the add function referred to in our <img> element's onclick property. Inside, we're triggering emit('addAnimal'), which will cause our addAnimal event handler, back in index.js, to execute.

    444 |
  • 445 |
446 |

If we switch to our application again, and click on the grass, a new lion should appear:

447 |

third animal

448 |

✨ Awesome! We now have our very first interactive choo app. ✨

449 |

We're not quite done yet, but we just implemented a lot of really cool new functionality, so let's dive deeper to understand exactly what's going on.

450 |

index.js holds the state of our application, which is handled via the function we pass into app.use(). This function receives two arguments, state & emitter.

451 |
// example
452 | app.use(function (state, emitter) (
453 |   state.number = 1
454 | 
455 |   emitter.on('changeNumber', function () {
456 |     state.number = 2
457 |   })
458 | })
459 | 
460 |

The state object contains the data structures that drive our application. The rendered output of our templates change depending on what this data holds.

461 |

We then use the emitter object to register functions that react to the execution of events elsewhere in our application. The names of the events we register can be anything we like. As they are registered inside of the function we pass into app.use(), they have the ability to modify the state object directly.

462 |

In main.js, we have access to both the state object, and an emit function. These are both passed into any template functions that are registered with a route (eg. app.route('/', main)).

463 |
// example
464 | module.exports = function (state, emit) {
465 |   return html`
466 |     <div onclick=${update}>
467 |       ${state.number}
468 |     </div>
469 |   `
470 | 
471 |   function update () {
472 |     emit('changeNumber')
473 |   }
474 | }
475 | 
476 |

We can use the state object to render useful bits of information inside of our templates. We can also continue passing that information further down into child templates.

477 |

We can use the emit() function to trigger updates to our state object, from our templates. If we register emitter.on('cats') inside of app.use(), this can be triggered by emit('cats') somewhere else.

478 |

Finally, if we want to trigger an event by clicking on an element in one of our templates, we can register a function on that element using the onclick property (eg. <div onclick=${update}></div>).

479 |

Passing data to state

480 |

Although our app is now interactive, it's not as fun as it could be.

481 |

When we add an animal to our state object, we're adding the same animal (a Lion), and giving it the same coordinates each time. To make this more interesting, when a new animal is created, our app should randomly choose from a set of different animals, and position it with the same coordinates as our cursor.

482 |

To do this, we need a way to pass information to our state from our templates. This can be done via the event handlers we are triggering.

483 |

Looking at our main.js file, we are executing our declared add() function when we click on the image of the grass. Inside of this function, we're executing emit('addAnimal'), which tells our handler back in index.js to update our state object.

484 |

emit() actually accepts two arguments:

485 |
    486 |
  • The first, which we already know, is the name of the event we'd like to trigger (eg. 'addAnimal').

    487 |
  • 488 |
  • The second is any data we'd like to pass through to the function on the other end.

    489 |
  • 490 |
491 |

Let's update our add() function in main.js:

492 |
// ...
493 | 
494 | function add (e) {
495 |   var x = e.offsetX - 20
496 |   var y = e.offsetY - 10
497 | 
498 |   var obj = {x: x, y: y}
499 |   emit('addAnimal', obj)
500 | }
501 | 
502 | // ...
503 | 
504 |

Now, when add() executes after you click on the grass, two new things are happening:

505 |
    506 |
  • First, we are grabbing the offsetX and offsetY information from the click event (notice that e, the click event object, is now passed into this function). This tells us where we clicked our mouse, relative to the image of the grass. We're slightly adjusting these values to compensate for the cursor itself.

    507 |
  • 508 |
  • Then, we are calling emit(), specifying the name of the event listener we'd like to trigger, but also passing in an object that contains the x and y coordinates of where we clicked.

    509 |
  • 510 |
511 |

Let's go back to index.js and make some changes to our emitter.on('addAnimal') handler:

512 |
// ...
513 | 
514 | // add animal
515 | emitter.on('addAnimal', function (data) {
516 |   var x = data.x
517 |   var y = data.y
518 | 
519 |   var obj = {type: 'lion', x: x, y: y}
520 |   state.animals.push(obj)
521 | 
522 |   emitter.emit('render')
523 | })
524 | 
525 | // ...
526 | 
527 |

The function that we pass into this event listener, now takes in a new argument: data. This value represents the information that we passed into emit() back in main.js (the x and y coordinates).

528 |

We can then construct an object with our coordinates, push this to state.animals, then re-render the screen.

529 |

We did mention earlier that we'd like to randomly create a new type of animal for each click, so let's update our code again to reflect that:

530 |
// ...
531 | 
532 | // add animal
533 | emitter.on('addAnimal', function (data) {
534 |   var animals = ['crocodile', 'koala', 'lion', 'tiger', 'walrus']
535 | 
536 |   var type = Math.floor(Math.random() * 5)
537 |   var x = data.x
538 |   var y = data.y
539 | 
540 |   var obj = {type: animals[type], x: x, y: y}
541 |   state.animals.push(obj)
542 | 
543 |   emitter.emit('render')
544 | })
545 | 
546 | // ...
547 | 
548 |

Now before pushing a new object to state.animals, we randomly select an index from an array of 5 names, and use this as our animal's type property.

549 |

If we switch over to our application again, and begin clicking different areas of the grass, we should see something like this:

550 |

random animals

551 |

LOOK AT HOW CUTE THEY ALL ARE!

552 |

Take a moment to appreciate everything you've just built! Our choo app is really starting to shape up.

553 |

Let's quickly revisit what we accomplished in this last section:

554 |
    555 |
  • In our template (main.js), we obtained the x and y coordinates of our mouse click event.

    556 |
  • 557 |
  • We then passed this information through to the addAnimal event handler using emit('addAnimal', obj).

    558 |
  • 559 |
  • Over in index.js, we received this data inside our emitter.on('addAnimal') handler as data, and proceeded to push this into our state.animals array.

    560 |
  • 561 |
562 |

At this point, we've made a super fun interactive choo app that we can share with all our friends! 👭

563 |

ProTip™ ✨ you can send the link of your Glitch app to anyone who'd like to play with your app, or hack on your code with you via Glich's Share or 🕶 Show buttons:

564 |

share

565 |

Now that we're on a roll, let's think of a way to build our next feature: removing animals from our app.

566 |

Removing data from state

567 |

If we can add an animal to our grass field by clicking in a particular spot, we should also be able to click on the animal itself and remove it from the screen.

568 |

This means that instead of updating state.animals by adding a new item to its array, we would instead need to remove the item that correlates to the animal that was clicked.

569 |

Let's start by opening main.js and update its code:

570 |
// import choo's template helper
571 | var html = require('choo/html')
572 | 
573 | // import template
574 | var animal = require('./animal.js')
575 | 
576 | // export module
577 | module.exports = function (state, emit) {
578 |   // create html template
579 |   return html`
580 |     <div class="container">
581 |       <div class="grass">
582 |         <img src="/assets/bg.gif" onclick=${add} />
583 |         ${state.animals.map(animalMap)}
584 |       </div>
585 |     </div>
586 |   `
587 | 
588 |   // map function
589 |   function animalMap (obj, i) {
590 |     return animal(remove, obj, i)
591 |   }
592 | 
593 |   // add new animal to state
594 |   function add (e) {
595 |     var x = e.offsetX - 20
596 |     var y = e.offsetY - 10
597 | 
598 |     emit('addAnimal', {x: x, y: y})
599 |   }
600 | 
601 |   // remove animal from state
602 |   function remove (e) {
603 |     var index = e.target.id
604 |     emit('removeAnimal', index)
605 |   }
606 | }
607 | 
608 |

There's a ton of new stuff here, so let's go through it carefully:

609 |

First, we created a new function called animalMap(), which returns our animal.js template:

610 |
function animalMap (obj, i) {
611 |   return animal(remove, obj, i)
612 | }
613 | 
614 |

Then, near the top of this file, we're now passing our new animalMap() function into state.animals.map, rather than the animal.js template itself:

615 |
${state.animals.map(animalMap)}
616 | 
617 |

Finally, we created a new remove() function that we'll use to remove animals from our state. Let's come back to that shortly.

618 |

You may have noticed that we're using this new animalMap() function we created to pass some new things into our animal.js template, before we run it through map(). Why are we doing this?

619 |

To build this feature, our animal.js template needs access to some extra functions and information. The first thing we pass in is our new remove() function. Then, we pass in obj, which represents the animal that map() is iterating across. The final argument, i, represents the index number of the current map() iteration.

620 |

These changes may seem confusing and abstract at first. Don't worry! Our next move will hopefully tie these changes together.

621 |

Let's open animal.js, and update its code:

622 |
var html = require('choo/html')
623 | 
624 | module.exports = function (onclick, animal, i) {
625 |   var type = animal.type
626 |   var x = animal.x
627 |   var y = animal.y
628 | 
629 |   // create html template
630 |   return html`
631 |     <img src="/assets/${type}.gif" style="left: ${x}px; top: ${y}px;" id=${i} onclick=${onclick}>
632 |   `
633 | }
634 | 
635 |

We've made some subtle but important changes. Let's break these down:

636 |
    637 |
  • Our animal.js template now accepts the three arguments we described above: the remove() function passed in from main.js (represented as onclick()), the current animal we're iterating over, and also that animal's index number (represented as i).

    638 |
  • 639 |
  • We've added an id property to our <img> element, which accepts the value of i. We use this to identify which animal we are clicking, and where it sits within our state.animals array.

    640 |
  • 641 |
  • We've also added an onclick property to our <img> element, which will trigger the remove() function we passed in from main.js.

    642 |
  • 643 |
644 |

Let's open index.js, and update app.use():

645 |
// ...
646 | 
647 | app.use(function (state, emitter) {
648 |   // initialize state
649 |   state.animals = [
650 |     { type: 'lion', x: 200, y: 100 },
651 |     { type: 'crocodile', x: 50, y: 300 }
652 |   ]
653 | 
654 |   // add animal
655 |   emitter.on('addAnimal', function (data) {
656 |     var animals = ['crocodile', 'koala', 'lion', 'tiger', 'walrus']
657 | 
658 |     var type = Math.floor(Math.random() * 5)
659 |     var x = data.x
660 |     var y = data.y
661 | 
662 |     var obj = { type: animals[type], x: x, y: y }
663 |     state.animals.push(obj)
664 | 
665 |     emitter.emit('render')
666 |   })
667 | 
668 |   // remove animal
669 |   emitter.on('removeAnimal', function (i) {
670 |     state.animals.splice(i, 1)
671 |     emitter.emit('render')
672 |   })
673 | })
674 | 
675 | // ...
676 | 
677 |

Towards the bottom of this function, we've added emitter.on('removeAnimal'). When we run emit('removeAnimal') from main.js, this is the block of code it triggers. It takes the index number of the animal that we clicked (this was stored on the <img>'s id property), and removes it from state.animals using JavaScript's built-in splice() function.

678 |

Afterwards, we run emitter.emit('render'), which tells choo to re-render the screen.

679 |

Let's check our application, and see if this works:

680 |

deletion

681 |

Cool! We can add animals to our screen, and also remove them. Talk about cuteness overload!

682 |

If we added a large number of animals to our plot of grass, it would be useful if we could filter the screen so we only see a specific type of animal.

683 |

Let's build this final feature to round out the guide, and finish our choo-animals application!

684 |

Creating dynamic routes

685 |

Before we create this new filter, let's first specify how we'd like the functionality to work.

686 |

At present, our application works entirely from one route ('/'). It would be cool to build a second route that allows us to dynamically specify which animal we'd like to filter for. What does this mean?

687 |

Our app can currently choose from five different animals to add to the screen. Rather than creating five different routes, one to filter each animal (eg. app.route('/filter/lion'), app.route('/filter/tiger')), we can create one route that handles any type of animal we ask for.

688 |

Below our first and only route in index.js, let's add a second one:

689 |
// ...
690 | 
691 | // declare routes
692 | app.route('/', main)
693 | app.route('/filter/:type', main)
694 | 
695 | // start app
696 | app.mount('div')
697 | 
698 |

You may have noticed that the route we just declared (/filter/:type) looks slightly funny. What does :type mean?

699 |

If we clicked on a link in our application that directed us to /filter/lion, or /filter/tiger, we'd be directed to the route we just declared. The : syntax tells the router that it will accept any value for this section of the route's path. The router will then make that value available to our application so it can respond to the user in a different way.

700 |

In this particular context, the last value of this URL's path becomes available to our application via state.params.type. If our route was declared as /filter/:meow, the value would become available to us via state.params.meow.

701 |

This means that when our user is directed to a specific /filter route, our application can see what type of animal they're asking for, and filter its output accordingly.

702 |

Our new route also points to main.js, so let's add new code to that template's animalMap() function, so it can filter for specific animals:

703 |
// ...
704 | 
705 | // add new animal to state
706 | function add (e) {
707 |   var x = e.offsetX - 20
708 |   var y = e.offsetY - 10
709 | 
710 |   emit('addAnimal', {x: x, y: y})
711 | }
712 | 
713 | // map function
714 | function animalMap (obj, i) {
715 |   var type = state.params.type
716 | 
717 |   if (type && type !== obj.type) {
718 |     return // nothing
719 |   } else {
720 |     return animal(remove, obj, i)
721 |   }
722 | }
723 | 
724 | // ...
725 | 
726 |

This update to animalMap() checks whether we've navigated to our /filter/:type route. If we have, state.params.type would contain a string of the animal we're looking for. If not, this value would be empty.

727 |

If this value contains a string, but does not match the type of animal our map() function is currently iterating over, we exit this function early by returning nothing. This means we don't render anything to the screen during this iteration. In all other circumstances (if there is a match, or we aren't filtering at all), then we render the animal to the screen during that iteration.

728 |

To activate our filter, let's add some new template markup to our main.js template, which renders a list of anchor elements, each linking out to a specific animal's filter:

729 |
// ...
730 | 
731 | // create html template
732 | return html`
733 |   <div class="container">
734 |     <div class="grass">
735 |       <img src="/assets/bg.gif" onclick=${add} />
736 |       ${state.animals.map(animalMap)}
737 |     </div>
738 |     <div class="controls">
739 |       <ul class="filters">
740 |         <li><a href="/">all</a></li>
741 |         <li><a href="/filter/crocodile">crocodiles</a></li>
742 |         <li><a href="/filter/koala">koalas</a></li>
743 |         <li><a href="/filter/lion">lions</a></li>
744 |         <li><a href="/filter/tiger">tigers</a></li>
745 |         <li><a href="/filter/walrus">walruses</a></li>
746 |       </ul>
747 |     </div>
748 |   </div>
749 | `
750 | 
751 | // ...
752 | 
753 |

Let's switch to our application, and see what happens:

754 |

filter

755 |

That's so cool! When we click on a filter, our app redirects to a new dynamic route, and our animalMap() function handles which animals need to appear on the screen.

756 |

Summary

757 |

Woah, we just finished building choo-animals. Congratulations!

758 |

I hope you had a lot of fun making this app, just as much as I had fun writing this guide.

759 |

Before we wrap up, let's write down a list of all the cool things we learned to do with choo. We learned how to:

760 |
    761 |
  • Create templates
  • 762 |
  • Create static & dynamic routes
  • 763 |
  • Create & update application state
  • 764 |
  • Modularise our code
  • 765 |
  • Render templates with loops
  • 766 |
  • Pass data around our application
  • 767 |
768 |

These concepts form a large part of any modern web app, not just those built with choo, but with other libraries and frameworks as well.

769 |

In the following chapters of this handbook, you'll dive into other topics such as form handling, third-party library integration, and sending & receiving data from external APIs.

770 |
771 |
772 |
773 | 777 | 778 | 779 | --------------------------------------------------------------------------------