Elixir and Elm Tutorial Bijan Boustani This book is for sale at http://leanpub.com/elixir-elm-tutorial This version was published on 2018-01-02 This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do. © 2017 - 2018 Bijan Boustani Tweet This Book! Please help Bijan Boustani by spreading the word about this book on Twitter! The suggested tweet for this book is: I’m learning functional web programming with Elixir and Elm! https://leanpub.com/elixir-elm-tutorial The suggested hashtag for this book is #ElixirElmTutorial. Find out what other people are saying about the book by clicking on this link to search for this hashtag on Twitter: #ElixirElmTutorial Contents Introduction . . . . . . . . . What We’re Building . . Acknowledgements . . . Who Is This Book For? . Prerequisites . . . . . . . Why Elixir and Elm? . . Technology Stack . . . . Functional Programming Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 1 1 2 2 4 4 4 Diving In . . . . . . . . . . . Installation . . . . . . . . Creating the Platform . . Configuring the Database Running the Server . . . Our First Resource . . . . Routing . . . . . . . . . . Running a Migration . . . Creating Players . . . . . Updating our Home Page Writing Elixir Code . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 6 7 9 10 12 12 16 18 20 Elixir Introduction . . . . . . . . . . . Creating an Elixir Project . . . . . . Elixir Testing . . . . . . . . . . . . . Elixir Compilation . . . . . . . . . . Elixir Modules and Functions . . . . Functions, Tests, and Documentation Writing Tests . . . . . . . . . . . . . IEx . . . . . . . . . . . . . . . . . . The Pipe Operator . . . . . . . . . . More Piping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 21 22 23 23 25 27 29 30 31 CONTENTS Function Arity . . . . . . . Shorthand Function Syntax Pattern Matching . . . . . Guards . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 32 33 34 35 Phoenix Testing and Deployment Running Phoenix Tests . . . . Git and GitHub . . . . . . . . Heroku . . . . . . . . . . . . . Heroku Setup . . . . . . . . . . Heroku Configuration . . . . . Production Deploy . . . . . . . Deployment . . . . . . . . . . Up and Running . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 36 38 38 39 40 41 42 43 44 Phoenix Sign Up . . . . . . . . . . . . Extending Player Account Features Player Fields . . . . . . . . . . . . Adding Fields . . . . . . . . . . . Updating the Player Schema . . . Player Changeset . . . . . . . . . Generating a Migration . . . . . . Running the Migration . . . . . . Updating Our Application . . . . . Working with Forms . . . . . . . . Show Player Page . . . . . . . . . Edit Player Page . . . . . . . . . . Shared Form . . . . . . . . . . . . Saving Our Progress . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 45 47 48 48 49 49 50 51 51 54 55 58 58 59 Phoenix Authentication . . . . . . . . . Fetching Dependencies . . . . . . . . Player Changesets . . . . . . . . . . . Using Our New Changeset . . . . . . Accounts Tests and Module Attributes Fixtures, Maps, and Structs . . . . . . Player Controller Tests . . . . . . . . Speeding Up Tests . . . . . . . . . . . Authentication Plug . . . . . . . . . . Router . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 60 61 63 64 65 66 67 68 69 CONTENTS Authenticate Function . . . . . . . Manual Testing . . . . . . . . . . . Fixing Our Tests . . . . . . . . . . Signing In . . . . . . . . . . . . . Sessions . . . . . . . . . . . . . . . Player Sign In View and Template Session Routing . . . . . . . . . . Signing In and Out . . . . . . . . . Trying Things Out . . . . . . . . . Displaying the Player Status . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 71 72 73 76 77 78 78 79 80 82 Phoenix API and Ecto Relationships Generating the JSON API . . . . . API Routing . . . . . . . . . . . . Establishing Relationships . . . . . Creating Gameplays . . . . . . . . Our New Gameplay Schema . . . Running Our Migration . . . . . . Trying Out our JSON API . . . . . Creating a Game . . . . . . . . . . Player API . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 83 84 85 86 87 89 90 90 91 95 Elm Introduction . . . . . . . . . . Introduction . . . . . . . . . . . Hello.elm . . . . . . . . . . . . . Elm Syntax . . . . . . . . . . . . Modules, Functions, and Types . Main Function . . . . . . . . . . elm-format . . . . . . . . . . . . Comments and Type Signatures Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 . 96 . 96 . 97 . 97 . 98 . 101 . 101 . 103 Elm Setup . . . . . . . . . . . . . . . Configuring Elm within Phoenix Updating .gitignore . . . . . . . Elm Folder . . . . . . . . . . . . Main.elm . . . . . . . . . . . . . Brunch Configuration . . . . . . Compiling with Phoenix . . . . Displaying Our Elm Application Working Elm application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 104 105 106 106 107 108 108 109 CONTENTS Live Reload . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 Elm Application . . . . . . . Getting Acquainted . . . elm-format . . . . . . . . Main.elm . . . . . . . . . Elm View . . . . . . . . . Creating a List of Games Breaking Up the View . . Extracting Our Data . . . Passing Data to the View Elm Maybe . . . . . . . . Maybe.withDefault . . . Why Maybe? . . . . . . . Iterating Through the List Tying It All Together . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 112 112 112 113 114 116 119 120 122 123 123 124 125 128 Elm Architecture . . . . . . . . . . . . . . Adapting Our Existing Elm Application Removing the Original Main Function . Starting with the Model . . . . . . . . . Update . . . . . . . . . . . . . . . . . . Update Messages . . . . . . . . . . . . . Changing the Model . . . . . . . . . . . Performing the Update . . . . . . . . . Subscriptions . . . . . . . . . . . . . . . Updating the View . . . . . . . . . . . . The Main Function . . . . . . . . . . . . Displaying Our List of Games . . . . . . Handling Events . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 129 129 130 133 133 134 135 136 136 137 138 140 142 Elm and API Data . . . . . . . Where Were We? . . . . . Updating Our Initial Model Changing the Update . . . Changing the View . . . . Importing Packages . . . . Fetching Games . . . . . . JSON Decoding . . . . . . FetchGamesList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 144 146 147 148 148 150 150 152 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CONTENTS Performing the Fetch . . . . . . . . Decoding the Remaining Game Data Rendering Our List of Players . . . . Refactoring Our View . . . . . . . . Performing Another Fetch . . . . . . Sorting Results . . . . . . . . . . . . Handling Errors . . . . . . . . . . . Handling Potentially Missing Data . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 153 154 157 158 159 162 164 166 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 167 168 169 170 172 173 174 177 179 182 185 186 187 189 Game Setup . . . . . . . . . . . . . . . . . . Creating a Game File . . . . . . . . . . . Configuring Elm Brunch . . . . . . . . . Updating app.js . . . . . . . . . . . . . . Extending Our GameController . . . . . . Adding a Route . . . . . . . . . . . . . . Creating a Template . . . . . . . . . . . . Working with Slugs . . . . . . . . . . . . Updating the Schema . . . . . . . . . . . Running the Migration and Adding a Slug Fixing the Tests . . . . . . . . . . . . . . Using Our New Slug Field . . . . . . . . . Pretty URLs . . . . . . . . . . . . . . . . Working Links . . . . . . . . . . . . . . . Decoding Slug Data in Elm . . . . . . . . Featured Game Link . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191 191 191 192 193 193 194 195 195 196 197 198 198 199 201 203 204 Layout and Design . . . . Pages . . . . . . . . . . Phoenix Layout . . . . Bootstrap . . . . . . . . Logo . . . . . . . . . . app.css . . . . . . . . . Featured Section . . . . Featured Game Data . . Authentication Section User Deletion . . . . . Authorization . . . . . Fixing Our Tests . . . . List of Games . . . . . List of Players . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CONTENTS Our First Game . . . . . . . . . . . Base Application for Our Game . Creating a Game Canvas . . . . Setting Up a Game Window . . . Adding the Sky and the Ground Creating Our Character . . . . . Importing Our Character . . . . Changing the Character Position Updating the Model . . . . . . . Adding an Item . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 205 207 208 210 212 213 215 216 219 221 Adding Interaction . . . . . . . . . Subscriptions . . . . . . . . . . . Importing the Keyboard Package Tracking Key Presses . . . . . . Setting the Correct Keys . . . . . Changing Direction . . . . . . . Character Direction . . . . . . . Collecting Items . . . . . . . . . Spawning Items . . . . . . . . . Working with Randomness . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 222 222 223 225 226 226 230 234 237 239 Displaying Game Data . . . . . . . . Scoring with Item Collection . . . Rendering Text Data . . . . . . . . Displaying Items Collected . . . . Displaying Time . . . . . . . . . . Updating the Player Score . . . . . Implementing a Countdown Timer Incorporating Time . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 240 241 243 245 247 248 249 251 Handling Game States Game State . . . . . Union Types . . . . Adjusting the View Starting Screen . . . Space Bar to Start . Clean Starting State Success State . . . . Restarting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252 252 252 253 255 256 257 259 260 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CONTENTS Game Over State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264 Phoenix Channel Setup . . . . . . . Channels . . . . . . . . . . . . . Score Channel . . . . . . . . . . Joining the Channel . . . . . . . elm-phoenix-socket . . . . . . . Configuring elm-phoenix-socket The Update Function . . . . . . Socket Subscription . . . . . . . Using the DevTools . . . . . . . Sending Data Over the Socket . . Phoenix.Push . . . . . . . . . . . Executing the Push . . . . . . . Joining the Score Channel . . . . Triggering SaveScore . . . . . . Adding a Button to the View . . Testing Out the Socket . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 265 265 266 267 268 270 270 272 272 273 274 275 276 276 277 278 Syncing Score Data . . . . . . . . . . . . . Planning Out Our Approach . . . . . . Updating our ScoreChannel . . . . . . . Tracking Data Over the Socket . . . . . Creating the Payload . . . . . . . . . . Creating Gameplays . . . . . . . . . . . Viewing Gameplay Records . . . . . . . Receiving and Rendering Score Changes Displaying the Results . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 279 279 280 280 281 283 285 286 289 Socket Authentication . . . . . . Phoenix.Token . . . . . . . . . User Tokens . . . . . . . . . . Passing the Token to JavaScript Verifying the Token . . . . . . Sending the Token to Elm . . . ProgramWithFlags . . . . . . . Configuring Flags . . . . . . . Finishing the Score Channel . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 290 290 290 291 292 292 293 293 295 296 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CONTENTS What’s Next? . . . . . . . . . . . . Additional Platformer Features More Minigames . . . . . . . . Multiplayer . . . . . . . . . . . Flexible Game Creation . . . . Chat . . . . . . . . . . . . . . OTP Content . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297 297 297 298 298 298 299 299 Outline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300 Appendix . . . . . . . . . . Quick Install . . . . . . Working with Versions Recommended Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303 303 304 305 Contact . . . Congrats Feedback Slack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307 307 307 307 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introduction Welcome to the world of functional web programming! In this book, we’ll learn how to create fun, scalable, and maintainable web applications. We’ll be using a wide range of web technologies along with the latest ideas from emerging languages like Elixir and Elm to craft a fun experience. Rather than focusing on theory, we’ll take a practical approach and build a real-world application. What We’re Building The application we’ll be building together is a small game platform for the web. We’ll use Elixir and the Phoenix web framework to power the back-end, where players can sign in and keep track of their scores. Then, we’ll use Elm on the front-end to create fun minigames. We’ll connect everything together so we can pass data back and forth between the back-end and front-end. Users on our platform will view available minigames, and the scores from those games will be updated on the platform in real-time. We’ll focus on building things with a solid foundation so we can use these same concepts to create different web applications as well. Acknowledgements I would like to thank Envy Labs and Code School for fostering environments where I was able to work hard and learn and grow. I’d also like to thank José Valim and Evan Czaplicki for crafting such beautiful and fun languages. Thanks to Michael Hartl for setting the standard for technical writing with The Ruby on Rails Tutorial, and Bret Victor for inspiring all of us with his visions of the future. Who Is This Book For? This book is written for developers who already have some existing experience with web programming. The goal is for the book to be a practical introduction to building a project with functional web programming languages like Elixir and Elm. We won’t assume any prior experience with Elixir and Elm, and consider it more likely that you’ve worked with languages like Ruby and JavaScript. But keep in mind that we’ll occasionally forego in-depth explanations and theory in an effort to gain insight into shipping a real project. We’ll walk through initial explanations to give you just enough information about the fundamentals and concepts so you can be productive. But there are other books that will provide more depth when it comes to learning more about the languages themselves: Introduction 2 • Programming Elixir1 by Dave Thomas • An Introduction to Elm2 by Evan Czaplicki The material in this book is intended to be crafted in such a way that you can follow along simply by typing in the relevant code examples. Beginners can still learn a lot simply by following along and building the application, because sometimes in programming you need to be exposed to certain concepts and ideas before they become easy to understand. The experience of building something will be fun and engaging; and a deeper understanding will follow with increased familiarity and experience. Prerequisites In addition to the notes above about the intended audience for this book, here are some additional prerequisites to keep in mind: • • • • Some experience with HTML and CSS. Familiarity with the command line and a text editor. Preferably previous experience with Git and GitHub. Preferably some experience working with a web framework. Why Elixir and Elm? Elixir Elixir3 is a dynamic, functional language designed for building scalable and maintainable applications. • Elixir is built on top of the Erlang virtual machine, and therefore inherits decades worth of stability and scalability. • Concurrency is at the heart of Elixir. Instead of getting faster processors, computers these days are getting processors with more cores. That means we need to write our programs in such a way that allows them to be distributed across multiple cores so our programs can outperform our competitors. As an example, compare the latest 13-inch Macbook Pro models4 with dualcore processors with 15-inch Macbook Pro models5 with quad-core processors. Then, see how many cores you’ll have access to when you deploy your application to a multi-core web server6 . 1 https://pragprog.com/book/elixir13/programming-elixir-1-3 https://guide.elm-lang.org 3 http://elixir-lang.org 4 http://www.apple.com/shop/buy-mac/macbook-pro/13-inch 5 http://www.apple.com/shop/buy-mac/macbook-pro/15-inch 6 https://www.digitalocean.com/pricing/#droplet 2 Introduction 3 • The Phoenix web framework provides us with the ability to create new projects quickly. For web developers that have worked with Ruby on Rails, the concepts will be familiar and easy to pick up. • Elixir also inherits amazing features from other languages: – Ruby’s readable syntax and philosophy of developer happiness. – Erlang’s stability and scalability. – F#’s magical pipe operator for data transformation. – LISP’s macros and metaprogramming. Elm Elm7 is an exciting new functional language that is still evolving. It’s the fastest, most reliable frontend web programming language currently available. • Elm is a compiled, functional language. • Elm is blazingly fast. • Elm programs are free from runtime errors. That means the language was designed in such a way that makes a certain class of errors impossible, which provides us with an ability to make guarantees about how our programs work. • The Elm compiler can be a helpful guide towards writing high quality code, and the error messages provided are extremely helpful. • The elm-format tool helps with writing consistent code that is easier to read, write, and maintain. While optional, this tool is highly recommended for Elm beginners because you can configure it to automatically format code when you save a file in your editor. • With all the features Elm has to offer, the net result is confidence. As developers, we can be more confident that our code is performing the way we intended, and that our programs will function properly for our users. • Elm code is maintainable. Refactoring is a dream, and you’ll find yourself surprised at how easy a significant refactor can feel after coming from other languages. Elixir and Elm? Elixir and Elm are young, functional programming languages that are optimized for your happiness as a developer. They offer a programming experience that will make it fun to develop applications, and over time those applications will be easy to extend and maintain. The primary reason to pick up new languages like Elixir and Elm is that it will afford you with an opportunity to acquire new ways of thinking. Many great lessons have been learned in the field of programming over the past several decades, and unfortunately many developers are still working in the dark on a daily basis. We ignore history at a great cost, and all too often make things difficult on ourselves. Elixir and Elm are a chance at a fresh perspective. 7 http://elm-lang.org Introduction 4 Technology Stack There are many technologies involved in building and deploying modern web applications. We’ll be using a straightforward stack of technologies that will allow us the flexibility to scale our applications gracefully. Here’s the short version of the technology stack: • Back-end: Elixir • Front-end: Elm These technologies stand on the shoulders of giants, so here’s a little more information about other technologies we’ll also use while building our applications: • • • • • Back-end: Elixir and Phoenix Front-end: Elm, the Elm Architecture, and JavaScript Version Control: Git and GitHub Data: Ecto, PostgreSQL, and JSON Deployment: Heroku Functional Programming If you’re coming from a background in working with Ruby on Rails or JavaScript web frameworks, then you’ll have a head start in being able to grasp the content and move smoothly through the book. Something to keep in mind is that Elixir and Elm are functional languages. If you’re coming from an object-oriented background, you may find some of the programmatic approaches to be unfamiliar at first, but the initial discomfort will pay off in the long run as you learn to solve problems in an elegant functional manner. Summary In this introduction, we touched briefly on the application we’ll be building and some of the reasoning for choosing Elixir and Elm as languages. But the fun part is creating with these technologies and experiencing the benefits first-hand, so let’s dive in and start building our application in the next chapter. Diving In Instead of simply reading about Elixir and Elm, let’s dive in and experience what these languages have to offer. In this chapter, we’re going to avoid detailed explanations and theory in favor of quickly building the foundation for our project. We’ll focus on running the necessary commands in the Terminal and writing the code we’ll need to start creating our application. Later in the book, we’ll cover more about the fundamentals of Elixir, but for now let’s focus on following along and getting a glimpse of how to get an application up and running. Installation If you haven’t already set up your development environment with Elixir, Phoenix, and PostgreSQL, check out the Appendix in the back of the book for quick installation instructions. Also note that we’re working with the latest version of Phoenix in this book. Make sure you have Phoenix 1.3 installed, or the commands and files will all look different as you work through the material. Creating the Platform The first step we need to take is to create the foundation for our application. To do that, let’s open up our Terminal and run the following command: 1 $ mix phx.new platform It will ask the following question: Fetch and install dependencies? Enter the letter Y and the following output will be displayed (note that some of the file creation lines were trimmed for the sake of readability): 6 Diving In 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 $ * * * * * * * * mix phx.new platform creating platform/mix.exs creating platform/README.md creating platform/assets/... creating platform/config/... creating platform/lib/platform/... creating platform/lib/platform_web/... creating platform/priv/... creating platform/test/... Fetch and * running * running * running install dependencies? [Yn] Y mix deps.get mix deps.compile cd assets && npm install && node node_modules/brunch/bin/brunch build We are all set! Go into your application by running: $ cd platform Then configure your database in config/dev.exs and run: $ mix ecto.create Start your Phoenix app with: $ mix phx.server You can also run your app inside IEx (Interactive Elixir) as: $ iex -S mix phx.server Phoenix displays a lot of helpful information. First, the output shows all the files that were generated (don’t worry if it seems overwhelming at first; we’re only going to start with a handful of these files). Then, we see some information about how to configure our database and start the server. Configuring the Database Now that we’ve created the files for our Phoenix application, let’s change to that directory: 1 $ cd platform We can set up the database for our project by running the following command: Diving In 1 7 $ mix ecto.create If you run into issues here, it likely means you’ll have to configure your PostgreSQL installation or adjust the database username and password fields at the bottom of the config/dev.exs file. You can also check out the Appendix at the back of this book for more information on PostgreSQL. Since this is the first time we’re running a command with our new application, we’ll see that it takes time for the source code to compile. Elixir runs on the Erlang virtual machine, and needs to compile the source to bytecode before we can run our programs. It takes time to run initially, but subsequent commands will run noticeably faster after this. If the database creation was successful, we’ll see the following message at the bottom: 1 2 3 4 $ mix ecto.create Compiling files (.ex) ... Generated platform app The database for Platform.Repo has been created We have successfully created our Phoenix application, compiled it, and set up our database. Running the Server Let’s see what our new application looks like in the browser. To start the web server, run the following command: 1 $ mix phx.server This will start a server and allow us to visit http://0.0.0.0:40008 in a browser to see our new application running. Here is what the output will look like: 1 2 $ mix phx.server [info] Running PlatformWeb.Endpoint with Cowboy using http://0.0.0.0:4000 8 http://0.0.0.0:4000 8 Diving In Phoenix Default Start Page At this point, you might be impressed that we managed to get a full back-end up and running so quickly. Or, you may have seen similar features in other frameworks, and perhaps you’re nonplussed with our progress so far. We’re going to start adding features to our application, but it’s worth taking a moment to appreciate how much we already have going for us with just a few commands. Feel free to take a look at some of the great documentation listed on the default Phoenix start page. Before we move on, let’s stop the Phoenix web server. Go back to the Terminal where the server is running, and press Control + C on your keyboard twice to stop the server. This tends to be the simplest way to exit a running Elixir program, and here is what the output will look like as you stop the running web server: Diving In 1 2 3 4 5 6 7 8 9 10 11 12 13 9 $ mix phx.server [info] Running Platform.Endpoint with Cowboy using http://0.0.0.0:4000 [info] Compiled 6 files into 2 files, copied 3 in 2.1 sec [info] GET / [debug] Processing by PlatformWeb.PageController.index/2 Parameters: %{} Pipelines: [:browser] [info] Sent 200 in 67ms ^C BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution ^C $ Our First Resource Since we are building a small game platform, there are two primary resources that we’ll want to get started with: • Players • Games We’re going to start by using something called a generator to keep moving quickly. It’s going to create a lot of the files we need to work with. Then, we’ll cover more about how it all works later. Let’s generate the resource for our players with the following command: 1 $ mix phx.gen.html Accounts Player players username:string score:integer With this command, we’re creating players for our game platform. We want to be able to use our browser to interact with the data, so we’re starting with phx.gen.html9 to generate an HTML resource. Because we’re creating player accounts for our application, we use Accounts to provide a context for our resource. Then, we use Player for the module name and players to indicate the pluralized form, which will also be used to create the database table. For the player fields, each player account will have a username (stored as a string), and a score (stored as an integer). We’ll eventually extend the capabilities of our players with additional fields, but for now this will give us a good starting point to start creating a list of players. You’ll see that the generator creates quite a few files for us, and once again Phoenix gives us some helpful tips about what to do next: 9 https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.html Diving In 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ * * * * * * * * * * * * * * 10 mix phx.gen.html Accounts Player players username:string score:integer creating lib/platform_web/controllers/player_controller.ex creating lib/platform_web/templates/player/edit.html.eex creating lib/platform_web/templates/player/form.html.eex creating lib/platform_web/templates/player/index.html.eex creating lib/platform_web/templates/player/new.html.eex creating lib/platform_web/templates/player/show.html.eex creating lib/platform_web/views/player_view.ex creating test/platform_web/controllers/player_controller_test.exs creating lib/platform/accounts/player.ex creating priv/repo/migrations/20170807120444_create_players.exs creating lib/platform/accounts/accounts.ex injecting lib/platform/accounts/accounts.ex creating test/platform/accounts/accounts_test.exs injecting test/platform/accounts/accounts_test.exs Add the resource to your browser scope in lib/platform_web/router.ex: resources "/players", PlayerController Remember to update your repository by running migrations: $ mix ecto.migrate Routing Don’t worry too much about all those files yet, but the information at the bottom is important. In order to configure our application to work with our new player accounts, we’ll need to add them to the router first, and then run a migration to update the database with a new players table. Phoenix makes things easy on us with the helpful notes in the Terminal. Let’s go ahead and follow along. Open the lib/platform_web/router.ex file and see what it looks like: 1 2 3 4 5 6 7 8 9 defmodule PlatformWeb.Router do use PlatformWeb, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers Diving In 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 11 end pipeline :api do plug :accepts, ["json"] end scope "/", PlatformWeb do pipe_through :browser # Use the default browser stack get "/", PageController, :index end # Other scopes may use custom stacks. # scope "/api", PlatformWeb do # pipe_through :api # end end The Phoenix router comes with two separate “pipelines” by default. One of them is for HTML (which we’re going to use now), and the other one is for JSON (which we’ll also use later). And we can even see that the scope is already set up for us to access the HTML with our browser. That’s how we were able to load the http://0.0.0.0:4000 URL and see the initial starter page. Don’t worry if it seems confusing at first. All you need to know is that this block of code is where we’ll focus for now: 1 2 3 4 5 scope "/", PlatformWeb do pipe_through :browser # Use the default browser stack get "/", PageController, :index end And we’re going to update it with our new players resource: 1 2 3 4 5 6 scope "/", Platform.Web do pipe_through :browser # Use the default browser stack get "/", PageController, :index resources "/players", PlayerController end That means when we access http://0.0.0.0:4000/players10 , we’ll soon be able to start creating the players for our game platform. 10 http://0.0.0.0:4000/players Diving In 12 Running a Migration Our application has all the information it needs to render the players resource that we created, but we still need to tell the database about the changes we made. For the database to store our player data (with the username and score fields), we’ll need to run a migration. Go back to the Terminal, and run the following command: 1 $ mix ecto.migrate This will create a new database table called players. If everything goes according to plan, then we should see the following output: 1 2 3 4 5 6 7 $ mix ecto.migrate Compiling 10 files (.ex) Generated platform app 08:18:44.181 [info] == Running Platform.Repo.Migrations.CreatePlayers.change/0 \ forward 08:18:44.182 [info] create table players 08:18:44.371 [info] == Migrated in 0.1s Creating Players Let’s start our server again and see our new player resource in action: 1 $ mix phx.server Now we can access http://0.0.0.0:4000/players11 and we should see the following: 11 http://0.0.0.0:4000/players 13 Diving In Empty List of Players on Player Index Page This is excellent. We can now add players to our platform using a web browser. Click the New Player link at the bottom and try creating a player on the http://0.0.0.0:4000/players/new12 page. 12 http://0.0.0.0:4000/players/new 14 Diving In New Player Page After we successfully create a new player account, we’ll see the “show” page with the individual player’s data (notice the player id number is displayed in the URL too): 15 Diving In Player Show Page Feel free to create additional player accounts so we have data to work with on our players page: 16 Diving In Players Index with Sample Data Updating our Home Page We have a working players resource with an index of all the players, a show page to view a single player, an edit page to update a single player, and the ability to delete players. But when we go back to our home page at http://0.0.0.0:400013 , these pages aren’t accessible. Our users wouldn’t know that they need to visit the /players/new page to create their account. At some point, we will only want our users to be able to create their accounts without being able to edit or delete others. To get started, let’s figure out where the HTML code is coming from for our home page. Inside the lib/platform_web folder, there is a templates folder. This is where we put the HTML code that we want to render in the browser. And instead of standard .html files, we’ll see that the files have a .html.eex extension. That means we can write standard HTML code, and we can also embed Elixir code too. Let’s open the lib/platform_web/templates/page/index.html.eex file and take a look (note that some of the HTML was trimmed for the sake of readability): 13 http://0.0.0.0:4000 Diving In 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 17 <div class="jumbotron"> <h2><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h2> <p class="lead">A productive web framework that<br />does not compromise speed and maintainability.</p> </div> <div class="row marketing"> <div class="col-lg-6"> <h4>Resources</h4> <ul> <!-- ... --> </ul> </div> <div class="col-lg-6"> <h4>Help</h4> <ul> <!-- ... --> </ul> </div> </div> This should look familiar in that it’s mostly comprised of standard HTML code. It’s the HTML that we’re seeing when we load http://0.0.0.0:400014 . Let’s delete this code and create a couple of simple links to our player pages. First, remove all the existing code in the lib/platform_web/templates/page/index.html.eex file. Then, replace it with the following: 1 2 3 4 <div class="container"> <a class="btn btn-success" href="/players/new">Create Player Account</a> <a class="btn btn-info" href="/players">List All Players</a> </div> Save the file and go back to the browser to see the changes (make sure the Phoenix web server is still running or restart the server with mix phx.server) at http://0.0.0.0:400015 : 14 15 http://0.0.0.0:4000 http://0.0.0.0:4000 18 Diving In Home Page with List Players Link Phoenix comes with a Live Reload feature that automatically refreshes our application in the browser. If the Phoenix server was still running, then the home page was automatically regenerated and should now display the buttons that we created. Try them out, and they should enable users to successfully navigate to the player pages in our application. Writing Elixir Code Lastly, let’s get some experience with writing Elixir code in our templates by converting our buttons to use embedded Elixir code instead of simple HTML. The page will work the same way, but this will give us a chance to use a Phoenix feature instead of writing HTML. Phoenix gives us a link16 function we can use, and we can see a handful of examples provided in the documentation. Since we’re working with a .eex file, that means we can embed Elixir code by surrounding it with tags like this: <%= ... %>. The Elixir code that we put inside those tags will be evaluated, and then rendered onto the page. A helpful debugging technique while working with Elixir is to use the IO.inspect17 function to display results. In this example, we’re using the IO module with the inspect function, and we’re passing it the string "Hello World!": 16 17 https://hexdocs.pm/phoenix_html/Phoenix.HTML.Link.html#link/2 https://hexdocs.pm/elixir/IO.html#inspect/2 19 Diving In 1 2 3 4 5 6 <div class="container"> <a class="btn btn-success" href="/players/new">Create Player Account</a> <a class="btn btn-info" href="/players">List All Players</a> <%= IO.inspect("Hello World!") %> </div> Let’s take a look at the results in our browser: Embedded Elixir We can do something similar to embed a link on our page. We won’t need to explicitly mention the module (Phoenix.HTML.Link), because we already have access to some helpful Phoenix functions in this context. We can recreate our existing HTML links with the following code by passing the link text, the location, and some extra classes for Bootstrap (which comes preloaded with Phoenix by default) to make it look nice: 1 2 3 4 5 <div class="container"> <%= link("Create Player Account", to: "/players/new", class: "btn btn-success"\ ) %> <%= link("List All Players", to: "/players", class: "btn btn-info") %> </div> We can now verify that our links still work the same way they did previously: 20 Diving In Link to Players Page Using Embedded Elixir Summary In this chapter, we managed to cover a lot of ground. We were able to create the entire foundation for our application with a Phoenix back-end. We leveraged the Phoenix generators to create our players resource, started getting an idea of what the Phoenix folder structure looks like, and began editing files. We also learned a little about routing and working with the database. We’ve gotten an introductory look at how to create a full Elixir and Phoenix web platform, and we even created a couple of player records that we can use as we continue building. But we moved quickly through these steps, and we don’t have a solid understanding of how all these pieces fit together yet. In the next chapter, we’ll delve into some Elixir basics. And instead of using generators like we did in this chapter, we’ll manually create features in our application so we can continue increasing our experience. Elixir Introduction In the last chapter, we created a full Phoenix back-end platform. By default, Phoenix applications contain a lot of Elixir code. But if we’re just getting started with the Elixir language, we should probably take a look at some simple Elixir examples and get a better idea of how to work with the tools. Creating an Elixir Project Let’s create a small temporary project. We’ll use the same mix tool that we used in the last chapter when we created our Phoenix application, but this time we’re just creating a small Elixir project called temporary. Run the following command in the Terminal: 1 $ mix new temporary The mix tool is a simple build tool that ships with Elixir. Here’s what the output should look like when we create our project: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ * * * * * * * * * * mix new temporary creating README.md creating .gitignore creating mix.exs creating config creating config/config.exs creating lib creating lib/temporary.ex creating test creating test/test_helper.exs creating test/temporary_test.exs Your Mix project was created successfully. You can use "mix" to compile it, test it, and more: cd temporary mix test Run "mix help" for more commands. Let’s change into the new project’s directory and take a look at the files that were generated for us: 22 Elixir Introduction 1 $ cd temporary The first thing you might notice is that our Elixir project and our Phoenix project share a lot in common. The folder structure is similar: Elixir Folder Structure The platform project we created in the last chapter contains more files and folders, but it’s important to note that it’s still an Elixir project just like the temporary one that we just created. The config folder contains configuration settings, the lib folder is where we’ll write most of our Elixir code, and the test folder contains the tests that let us know our application is working as intended. Elixir Testing Depending on which programming languages you’ve worked with in the past, you might potentially have a lot of experience writing tests, or perhaps not. If you haven’t written tests before, the basic idea is that tests give us a way to feel confidence that our code is actually working as expected. We write our expectations (or “assertions”), and they give Elixir Introduction 23 us a quick way to check that the code we’re writing works (and doesn’t break other code). We’ll delve deeper into testing with our Phoenix app, but for now let’s just try it out. Run the mix test command inside the temporary folder: 1 $ mix test The output should look something like this: 1 2 3 4 5 6 7 8 9 $ mix test Compiling 1 file (.ex) Generated temporary app .. Finished in 0.03 seconds 2 tests, 0 failures Randomized with seed 670956 Elixir Compilation Here’s where we’ll start to see some key features of Elixir. First, we can see that Elixir is a compiled language. The first time we run our code after making changes, Elixir needs to compile it to Erlang bytecode. This can be really helpful because it means we’ll catch errors early instead of having to debug our app while we’re using it. In fact, if we run the mix test command again, we’ll see that Elixir doesn’t need to recompile the code because we haven’t made any changes: 1 2 3 4 5 6 7 $ mix test .. Finished in 0.03 seconds 2 tests, 0 failures Randomized with seed 114557 Elixir Modules and Functions The Elixir code that we’ll write in this project is contained in the lib/temporary.ex file. Files that end in .ex are Elixir files that will be compiled and run (you’ll also notice that we have files that end in .exs, which are Elixir scripts). Inside the lib/temporary.ex file, we see the basic structure for all the Elixir programs that we’ll be writing. Elixir Introduction 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 24 defmodule Temporary do @moduledoc """ Documentation for Temporary. """ @doc """ Hello world. ## Examples iex> Temporary.hello :world """ def hello do :world end end We start out with a module that encapsulates our related code: 1 2 3 defmodule Temporary do # ... end If we ignore the documentation for now, we’ll see that we have one function: 1 2 3 def hello do :world end Inside that function, we have our return value: 1 :world Let’s start making some changes. Rename the hello function to add. We’ll pass two parameters (x and y), and we’ll return the addition of these two values using x + y inside the function: Elixir Introduction 1 2 3 25 def add(x, y) do x + y end Functions, Tests, and Documentation The example above is admittedly simple, but it’s good in the sense that we know how to create a function now. But how do we use it? We generally use the Module.function(arguments) syntax to invoke the functions we’ve declared. If we look at the original documentation for our hello function, we can see the example usage: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 defmodule Temporary do @moduledoc """ Documentation for Temporary. """ @doc """ Hello world. ## Examples iex> Temporary.hello :world """ def add(x, y) do x + y end end This tells us that we should be able to run Temporary.hello and it should return :world. This is where things get interesting, so let’s run mix test again: 1 $ mix test Since we no longer have our hello function, it’s not surprising that our tests failed. But we actually have two test failures. One of them is from the test file located in the test folder, but the first failure is actually coming from the example in our documentation (which is called a doctest). Elixir Introduction 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 $ mix test Compiling 1 file (.ex) 1) test doc at Temporary.add/2 (1) (TemporaryTest) test/temporary_test.exs:3 Doctest failed: got UndefinedFunctionError with message "function Temporary\ .hello/0 is undefined or private" code: Temporary.hello stacktrace: (temporary) Temporary.hello() (for doctest at) lib/temporary.ex:11: (test) 2) test greets the world (TemporaryTest) test/temporary_test.exs:5 ** (UndefinedFunctionError) function Temporary.hello/0 is undefined or priv\ ate code: assert Temporary.hello() == :world stacktrace: (temporary) Temporary.hello() test/temporary_test.exs:6: (test) Finished in 0.04 seconds 2 tests, 2 failures Randomized with seed 520513 Let’s update the documentation so that it shows an example of how to use our new add function: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 defmodule Temporary do @moduledoc """ Documentation for Temporary. """ @doc """ Add two numbers together. ## Examples iex> Temporary.add(1, 1) 2 """ Elixir Introduction 15 16 17 18 27 def add(x, y) do x + y end end We can run our tests again, and the doctest example in our lib/temporary.ex file should now be passing: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $ mix test � temporary mix test Compiling 1 file (.ex) . 1) test greets the world (TemporaryTest) test/temporary_test.exs:5 ** (UndefinedFunctionError) function Temporary.hello/0 is undefined or priv\ ate code: assert Temporary.hello() == :world stacktrace: (temporary) Temporary.hello() test/temporary_test.exs:6: (test) Finished in 0.03 seconds 2 tests, 1 failure Randomized with seed 682227 Doctests are an awesome feature of Elixir. They allow us to write our functions and document them and test that they work all at once! It encourages us to write and maintain our documentation, and gives us confidence that our code is actually doing what we think it is. Writing Tests Let’s write a few tests to ensure that our add function is working as intended. Open the test/temporary_test.ex file and add the following: Elixir Introduction 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 28 defmodule TemporaryTest do use ExUnit.Case doctest Temporary test "the add function takes two integers and adds them together" do result = Temporary.add(1, 1) assert result == 2 end test "the add function takes two floats and adds them together" do result = Temporary.add(1.5, 1.5) assert result == 3.0 end test "the add function returns a number" do result = Temporary.add(1.5, 3.5) result_is_a_number = is_number(result) assert result_is_a_number end end Looking at the test/temporary_test.exs file, note that the test code still takes the same general format we used in lib/temporary.ex, where we have a module defined at the top that encapsulates the rest of our code. We also see that we’re using the default ExUnit18 library to write our tests. Then, there’s the doctest Temporary line, which is how the tests knew to run the examples we were writing in our documentation. The three test cases show basic examples of how we can call our function with some example numbers and verify that the result is correct. For example, passing 1 and 1 as arguments to our add function should return a result of 2. There are other assertions19 that ExUnit provides, but we’ll stick with assert for now to ensure that we’re getting a true value from our tests. We should be able to run our tests again and see that all three of these test cases are passing (and the doctest is still passing as well): 18 19 https://hexdocs.pm/ex_unit/ExUnit.html https://hexdocs.pm/ex_unit/ExUnit.Assertions.html Elixir Introduction 1 2 3 4 5 6 7 29 $ mix test .... Finished in 0.03 seconds 4 tests, 0 failures Randomized with seed 867380 IEx How would we run our code in an interactive environment? The tests are doing a good job of testing values, but what if something wasn’t working the way we had expected? Let’s run the following command from inside our temporary project folder: 1 $ iex -S mix This allows us to run Elixir code in an interactive environment and see the results (you can also just use iex from the command line to get started, but using iex -S mix is preferable because it means we don’t have to manually import the modules we want to work with). Here’s an example where we are basically recreating our first test case to call the add function from the Temporary module and see the result: 1 2 3 4 $ iex -S mix Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help) iex(1)> Temporary.add(1, 1) 2 We can try our second example case too: 1 2 iex(2)> Temporary.add(1.5, 1.5) 3.0 Everything works as expected in the tests, and now we have an interactive way of checking our code too. Elixir Introduction 30 The Pipe Operator Our third test case uses a common pattern seen in programming. We break up our code into small chunks, and we assign the values to variables. This has a great benefit of being able to name things in obvious ways, but Elixir has an alternative approach that helps us reconsider the way we write code. The idea behind the “pipe operator” (|>) is that it encourages us to think about our functions in terms of data transformation. Instead of using variables, we take an initial value, pipe it through a handful of functions, and return the result at the end. Let’s take another look at our third test case: 1 2 3 4 5 test "the add function returns a number" do result = Temporary.add(1.5, 3.5) result_is_a_number = is_number(result) assert result_is_a_number end This example was intentionally written with extraneous variables that aren’t particularly necessary. Let’s use the pipe operator syntax, keeping in mind that the code is still going to accomplish the same thing: 1 2 3 4 5 test "the add function returns a number" do Temporary.add(1.5, 3.5) |> is_number |> assert end Whoa. What’s happening here? Instead of using variables, we’re evaluating a result and then using the |> to pass it along as the argument to the next function. It also enables us to take the data and “pipe it through” other functions. For example, let’s inspect what value is getting passed to assert at the end: 1 2 3 4 5 6 test "the add function returns a number" do Temporary.add(1.5, 3.5) |> is_number |> IO.inspect |> assert end We can run mix test from the command line to see the results: Elixir Introduction 1 2 3 4 5 6 7 8 31 $ mix test ...true . Finished in 0.03 seconds 4 tests, 0 failures Randomized with seed 312071 That explains why our test is passing, because true is getting passed to the assert function. More Piping These are simple examples, because these functions each expect a single argument. But what if we want to pipe to a function that takes multiple arguments? When we use the pipe operator in Elixir, it pipes the value as the first argument in the next function. In other words, we could actually refactor our test case to pass the first value like this: 1 2 3 4 5 6 7 test "the add function returns a number" do 1.5 |> Temporary.add(3.5) |> is_number |> IO.inspect |> assert end This demonstrates that we can pipe the value 1.5 to the Temporary.add function, and it will use the value as the first argument. And 3.5 will be sent as the second argument to the Temporary.add function. Before we move on, let’s go back to the original pipe operator example since it was cleaner code. But the examples above are an important demonstration of how we can use the pipe operator. 1 2 3 4 5 test "the add function returns a number" do Temporary.add(1.5, 3.5) |> is_number |> assert end Elixir Introduction 32 Function Arity In this book, we’ve referred to functions like the add function as simply add. But functions in Elixir are often referred to in terms of their “arity”. The arity of a function is the number of arguments it takes. In our examples above, the add function takes two arguments, so it would be referred to as add/2. The is_number function only takes a single argument, so we would refer to it as is_number/1. For the rest of the book, we’ll try to be consistent about referring to functions with their arity included, because the arity is of particular importance in the Elixir language. In fact, we can define multiple functions that share the same name, but behave differently depending on the number of arguments we pass to them. This can be a tricky concept at first, but we’ll see some examples soon that should help to clarify. Shorthand Function Syntax At this point, we’re probably dying to get back to Phoenix and build our application. But there are just a couple more Elixir topics that will help us when we start looking at our Phoenix code. Without knowing the concepts behind Elixir, it can be tough to really understand what’s going on in Phoenix. The functions we’ve seen so far take the following format: 1 2 3 def function_name(arguments) do # ... end When we’re dealing with small, simple functions like add/2, we can make our code more concise and readable by using the shorthand function syntax: 1 def function_name(arguments), do: ... Note that there are extra , and : characters, but this enables us to remove the end keyword and move our entire function definition to a single line. Here’s our add/2 function as a single line. You can update our example in the lib/temporary.ex file and then run mix test again to verify that it still works. 1 def add(x, y), do: x + y There aren’t strict rules about when you should use shorthand function syntax. On the one hand, it’s good practice to break up our programs into small functions that are easy to reason about, which could mean many of our functions fit well on a single line. However, we shouldn’t feel compelled to force multiline functions to fit on a single line arbitrarily. The reason we’re introducing this concept now is that the add/2 function will be easier to visualize and reason about in the next section when using the shorthand function syntax. 33 Elixir Introduction Pattern Matching The reason for introducing shorthand function syntax here is that it gives us a more obvious way to see one of Elixir’s most powerful features in action: pattern matching. Pattern matching can be difficult to comprehend at first, so be gentle with yourself if it takes some time to understand. It’s one of those things that needs to be seen and experienced a couple of times before it starts to sink in. Elixir allows us to create multiple “clauses” of our functions. That means we can create a new clause of our add/2 function, and Elixir will use pattern matching to determine which clause actually gets used when the function is called. It sounds confusing, so let’s see an example. When we add 0 to a number, the result is actually just the number itself (this is known as the identity property of addition). So 0 + 1 should return a result of 1. When we use the add/2 function, it would look like this: 1 2 iex> Temporary.add(0, 1) 1 That means whenever we pass 0 as an argument to our function, we don’t really need to perform any addition. We can add a new clause for our add/2 function that handles cases where the first argument is 0: 1 2 def add(0, y), do: y def add(x, y), do: x + y And we can do the same thing when the second argument is 0: 1 2 3 def add(0, y), do: y def add(x, 0), do: x def add(x, y), do: x + y We can even add another clause that “matches” for when both arguments are 0s: 1 2 3 4 def def def def add(0, add(0, add(x, add(x, 0), y), 0), y), do: do: do: do: 0 y x x + y And here’s our full example with a few more doctests to verify that everything is working as expected: 34 Elixir Introduction 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 defmodule Temporary do @moduledoc """ Documentation for Temporary. """ @doc """ Add two numbers together. ## Examples iex> Temporary.add(0, 0) 0 iex> Temporary.add(0, 1) 1 iex> Temporary.add(1, 0) 1 iex> Temporary.add(1, 1) 2 """ def def def def end add(0, add(0, add(x, add(x, 0), y), 0), y), do: do: do: do: 0 y x x + y Guards Lastly, Elixir allows us to add “guard clauses” to our functions to make sure they’re working with the right values. Our add/2 function is only designed to work with numbers. They can be integers or floating point numbers, but we don’t want to add strings together (string concetenation would actually require the use of the <> operator). We can use the when keyword along with the is_number/1 function we saw previously to make sure that our function is working with numerical values. 35 Elixir Introduction 1 2 3 4 def def def def add(0, add(0, add(x, add(x, 0), do: y) when 0) when y) when 0 is_number(y), do: y is_number(x), do: x is_number(x) and is_number(y), do: x + y Functions like is_number/1 can be really helpful to check the types of values we’re working with. One helpful tip is to find similar functions available to us using iex. Inside iex, type is_ and then hit the TAB key on your keyboard, and it will display the different checks that are available: 1 2 3 4 5 6 7 $ iex iex(1)> is_ # Press TAB key after is_ is_atom/1 is_binary/1 is_float/1 is_function/1 is_list/1 is_map/1 is_pid/1 is_port/1 is_bitstring/1 is_function/2 is_nil/1 is_reference/1 is_boolean/1 is_integer/1 is_number/1 is_tuple/1 Summary This chapter was intended to give a brief introduction to Elixir concepts. These will be invaluable as we start building our Phoenix applications, because we’ll see that Phoenix code is really just Elixir code. Without some background in Elixir, it can occasionally be difficult to understand what’s going on. This was not meant as exhaustive introduction to Elixir. There are many more concepts, and there are several books available that give a full introduction to the language. In this book, we’re going to keep moving because our goal is to build a practical real-world application, and we’ll learn what we need along the way. But check out these free resources if you want to get a little more Elixir experience before continuing: • Elixir Getting Started Guide20 • Elixir School21 • Try Elixir Course22 Feel free to delete the temporary project, and in the next chapter we’ll continue working towards building our platform application. 20 http://elixir-lang.org/getting-started/introduction.html https://elixirschool.com/en/lessons/basics/basics 22 https://www.codeschool.com/courses/try-elixir 21 Phoenix Testing and Deployment Now that we have some familiarity with Elixir, let’s get back to our Phoenix application. In this chapter, we’re going to be working with Phoenix tests, GitHub version control, and Heroku deployment. If you’re already familiar with these concepts and only want to work locally for this project, feel free to skim through the content. Even if you decide to skip the Heroku deployment, the concepts here are important. Keeping our tests passing and consistently checking in our code contributes to a sane development workflow. Running Phoenix Tests In the last chapter, we learned that we could use the mix test command to run the tests for our simple Elixir project. We can do the same thing for our Phoenix project. Let’s go to our platform folder and run mix test (the results below have been cleaned up for readability): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ mix test ................... 1) test GET / (PlatformWeb.PageControllerTest) test/platform_web/controllers/page_controller_test.exs:4 Assertion with =~ failed code: assert html_response(conn, 200) =~ "Welcome to Phoenix!" left: "<!DOCTYPE html><html lang=\"en\"><body><div class=\"container\"><ma\ in role=\"main\">\n<a class=\"btn btn-success\" href=\"/players/new\">Create Pla\ yer Account</a>\n<a class=\"btn btn-info\" href=\"/players\">List All Players</a\ ></main></div></body></html>" right: "Welcome to Phoenix!" stacktrace: test/platform_web/controllers/page_controller_test.exs:6: (test) Finished in 0.2 seconds 20 tests, 1 failure Randomized with seed 905 The good news is it looks like we already have 20 tests. Some of them came with Phoenix by default when we ran mix phx.new platform. Other tests were created when we ran the mix phx.gen.html generator for our players resource. Phoenix Testing and Deployment 37 The bad news is one of our tests is no longer passing. Let’s take a look at the test/platform_web/controllers/page_controller_test.exs file: 1 2 3 4 5 6 7 8 defmodule PlatformWeb.PageControllerTest do use PlatformWeb.ConnCase test "GET /", %{conn: conn} do conn = get conn, "/" assert html_response(conn, 200) =~ "Welcome to Phoenix!" end end It looks like this test is making an HTTP get request to the default route ("/"). If we look at the http://0.0.0.0:4000/ URL, you can think of that trailing slash as the default / route. Our test is expecting the text "Welcome to Phoenix!" to appear somewhere on the page, but remember that we replaced the default Phoenix page. Keep in mind that we don’t need a full understanding of everything going on in the tests yet. For now, we just want to get back to where all the tests are passing. In this case, a quick fix is to assert that the home page contains the word "Players" instead of "Welcome to Phoenix!". This is admittedly a brittle test that’s subject to break when we make changes to our home page, but since we’re making a game platform, it’s likely that our home page is going to say "Players" somewhere, and this test still allows us to ensure that our home page is loading properly. Let’s go ahead and update our test with the following: 1 2 3 4 5 6 7 8 defmodule PlatformWeb.PageControllerTest do use PlatformWeb.ConnCase test "GET /", %{conn: conn} do conn = get conn, "/" assert html_response(conn, 200) =~ "Players" end end We can run our tests again, and we should see all green: Phoenix Testing and Deployment 1 2 3 4 5 6 7 38 $ mix test .................... Finished in 0.4 seconds 20 tests, 0 failures Randomized with seed 187055 Git and GitHub Now that all our tests are passing and our application is in working condition, let’s go ahead and commit what we have so far and push it to GitHub. We won’t cover version control in detail in this book, and you’re welcome to skip the parts with git commands if you’d like. But it’s a good idea to keep a history of your project so you can always recover from mistakes. This will also help when it comes time to deploy our application to Heroku. If you have Git23 installed, run the following commands inside the platform folder to commit what we have so far: 1 2 3 $ git init $ git add . $ git commit -m "Initial Phoenix platform application" If you have a GitHub account, you can create a new public repository at https://github.com/new. Give it the name platform, and then the following commands will allow us to push our local application to GitHub (you’ll need to add your username on the first line): 1 2 $ git remote add origin https://github.com/YOURUSERNAME/platform.git $ git push -u origin master Keep in mind that the audience for this book is expected to have some experience with these topics already. Feel free to take your time, skim quickly, or skip ahead as needed. Heroku For those that haven’t used Heroku24 before, it essentially gives us an easy and free way to deploy our application and see it running live. 23 24 https://git-scm.com/ https://www.heroku.com Phoenix Testing and Deployment 39 It’s not always an ideal deployment environment for Elixir applications, but it’s perfect for our purposes, because we’re just looking for the simplest way to get our application up and running. Sign up for a Heroku account if you haven’t already, and then our deployments will be as simple as running git push heroku master when we’re ready. Heroku Setup After you’ve signed up for a Heroku account, create a free app using their web interface (the name platform will already be taken, so you’ll have to come up with a name you like or allow Heroku to choose a random name for you). Then, download the Heroku toolbelt25 command line tool. Once you have that installed, you can run the heroku login command to sign in to your account. Since we have an existing Git repository, we can use the following command to add our application to Heroku: 1 $ heroku git:remote -a YOURAPPNAME Now inside our platform folder, we can run the git remote command and see that we now have two remotes for our project: 1 2 3 $ git remote heroku origin When we push to origin, we’ll be pushing our project to GitHub. When we push to heroku, we’ll be pushing our project to Heroku. Before we can do that, we’ll have to set things up for Heroku to know what kind of application we’re building. We’ll add a couple of “buildpacks” to set things up: 1 2 3 $ heroku buildpacks:add https://github.com/HashNuke/heroku-buildpack-elixir.git $ heroku buildpacks:add https://github.com/gjaldon/heroku-buildpack-phoenix-stat\ ic.git Keep in mind that these commands won’t make changes to our local files. If we run the git status command, we’ll see that nothing has changed. But we should see the following output to let us know that our Heroku app is configured to work with the Elixir code that we’re going to send: 25 https://toolbelt.heroku.com Phoenix Testing and Deployment 1 2 3 4 5 6 7 8 9 10 40 $ heroku buildpacks:add https://github.com/HashNuke/heroku-buildpack-elixir.git Buildpack added. Next release on platform will use https://github.com/HashNuke/h\ eroku-buildpack-elixir.git. Run git push heroku master to create a new release using this buildpack. $ heroku buildpacks:add https://github.com/gjaldon/heroku-buildpack-phoenix-stat\ ic.git Buildpack added. Next release on platform will use: 1. https://github.com/HashNuke/heroku-buildpack-elixir.git 2. https://github.com/gjaldon/heroku-buildpack-phoenix-static.git Run git push heroku master to create a new release using these buildpacks. To set up the correct versions we need for our application, create a file called elixir_buildpack.config in the platform folder. 1 2 erlang_version=20.0 elixir_version=1.5.2 Since we’re using the latest versions of Erlang, Elixir, and Phoenix, these settings are necessary for the deployment to work. Heroku Configuration There are still a couple more steps we need to take to make sure our application is secured to be pushed live to Heroku. Let’s create a Procfile to tell Heroku which command we want to run to start the Phoenix server. We’ll also add a line to make sure that database migrations are successful before we start the server in the production environment. Create a file called Procfile inside the platform folder, and add the following code: 1 2 release: MIX_ENV=prod mix ecto.migrate web: MIX_ENV=prod mix phx.server Now we’ll need to set some environment variables on Heroku. These are configuration settings we’ll need to get our app running. If we run the heroku config command right now, we’ll see that we don’t currently have any variables set up: 1 2 $ heroku config === platform Config Vars Since we’re using PostgreSQL for our Phoenix application, we’ll also want to create a free Heroku add-on for our project with the following command: Phoenix Testing and Deployment 1 41 $ heroku addons:create heroku-postgresql:hobby-dev This is what the output should look like: 1 2 3 4 5 6 7 $ heroku addons:create heroku-postgresql:hobby-dev Creating heroku-postgresql:hobby-dev on � platform... free Database has been created and is available ! This database is empty. If upgrading, you can transfer ! data from another database with pg:copy Created ... as DATABASE_URL Use heroku addons:docs heroku-postgresql to view documentation If we run heroku config again, we’ll see that we now have a DATABASE_URL variable configured (the actual URL has been removed in this example): 1 2 3 $ heroku config === platform Config Vars DATABASE_URL: postgres://... We’ll also need to set a SECRET_KEY_BASE environment variable for production. Phoenix can generate a secret key for us with the following command (note that your key will be different from the example shown below): 1 2 $ mix phx.gen.secret ChVyb+s5O6qVKZabMCWwDPYHJbYqMpWppTZFZmsnULd+PDyXqQU36H8Rs6HXU0nl Then we can take that key and set it as the Heroku environment variable with the following command (don’t forget to replace the example key shown here with the one you generated above): 1 2 $ heroku config:set SECRET_KEY_BASE="ChVyb+s5O6qVKZabMCWwDPYHJbYqMpWppTZFZmsnULd\ +PDyXqQU36H8Rs6HXU0nl" Now when we run the heroku config command we should be able to see the settings for both the DATABASE_URL and the SECRET_KEY_BASE. 1 2 3 4 $ heroku config === platform Config Vars DATABASE_URL: ... SECRET_KEY_BASE: ... Production Deploy Lastly, we need to make a few changes to the prod.exs file in the config folder. Open that file and you’ll see the following code along with a lot of explanatory comments: Phoenix Testing and Deployment 1 2 3 4 42 config :platform, PlatformWeb.Endpoint, load_from_system_env: true, url: [host: "example.com", port: 80], cache_static_manifest: "priv/static/cache_manifest.json" Let’s make a couple of quick changes so it looks like this (replace the YOURAPPNAME with the app name you created on Heroku): 1 2 3 4 5 config :platform, PlatformWeb.Endpoint, load_from_system_env: true, url: [host: "YOURAPPNAME.herokuapp.com", port: 80], cache_static_manifest: "priv/static/cache_manifest.json", secret_key_base: System.get_env("SECRET_KEY_BASE") This is how Heroku knows to use the SECRET_KEY_BASE environment variable in the production environment. Below that, we also want to add a new block of code to configure our database: 1 2 3 4 5 config :platform, Platform.Repo, adapter: Ecto.Adapters.Postgres, url: System.get_env("DATABASE_URL"), pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), ssl: true If you scroll down to the very bottom of the prod.exs file, you’ll see a line to import a secret configuration file. Since we’re using environment variables for Heroku, we won’t need this. Let’s comment that line out by placing a # character at the beginning of the line: 1 2 3 # Finally import the config/prod.secret.exs # which should be versioned separately. # import_config "prod.secret.exs" You can also delete the prod.secret.exs file if you’d like since we won’t need it anymore. Deployment Let’s run our tests one more time to make sure we didn’t break anything: 1 $ mix test If everything is still passing, let’s commit our latest changes: 43 Phoenix Testing and Deployment 1 2 $ git add . $ git commit -m "Update production configuration" We’ll push the updates to GitHub first: 1 $ git push origin master And now (the moment we’ve all been waiting for), we can push to Heroku: 1 $ git push heroku master We’ll see a lot of output when we deploy. If something goes wrong with the deployment, it will let us know. Don’t worry too much if you run into an issue or two, because this process is admittedly tedious. There are Stack Overflow sections for both Heroku26 and Phoenix27 that can be really useful if you run into any errors. It’s worth the trouble once we get to see our app up and running live in production! Up and Running Our app is finally up and running on Heroku! The Procfile we created automatically handles production database migrations. Inside the platform folder, let’s run the following from the command line to see our application running on Heroku: 1 $ heroku open Working Heroku Deploy Our application is working in production! 26 27 https://stackoverflow.com/questions/tagged/heroku https://stackoverflow.com/questions/tagged/phoenix-framework Phoenix Testing and Deployment 44 Summary In this chapter, we were able to get all our Phoenix tests passing, push our code to GitHub, and deploy our application successfully to Heroku! We’re off to a solid start for our platform. Our back-end is up and running, and we’ve picked up some introductory knowledge about Phoenix and Elixir. In the next chapter, we’ll learn more about Phoenix as we extend our player features and allow new users to sign up. Phoenix Sign Up There are multiple options available for handling player sign up and sign in features. In our case, we want to keep things straightforward so players can sign up easily and play games on our platform, which means we’ll forego asking them for their email or building out a full featured authentication system. We’re also eager to start using Elm to build the front-end, so we’re going to take a simple approach. We’ll use Phoenix to handle authentication initially, and once users are signed in, they’ll be redirected to the Elm front-end application we’ll be building. We’ll need to refactor some of these features later, but this approach will provide a quick way for players to sign up as we extend the player resource features we generated previously. Extending Player Account Features First, let’s take a look at the existing features provided in the Platform.Accounts module we created earlier. Check out the functions available in the lib/platform/accounts/accounts.ex file: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 defmodule Platform.Accounts do @moduledoc """ The Accounts context. """ import Ecto.Query, warn: false alias Platform.Repo alias Platform.Accounts.Player @doc """ Returns the list of players. ## Examples iex> list_players() [%Player{}, ...] """ def list_players do Phoenix Sign Up 21 22 23 24 25 26 46 Repo.all(Player) end # ... end Inside this module, there’s a list_players/0 function that will fetch all the Player records from the database. We can think of Repo as an abstraction for our database, so Repo.all(Player) means we’ll query for all the players we have stored. We can test out this function using iex to interactively query for our existing player records. First, we’ll start the interactive Elixir prompt: 1 $ iex -S mix phx.server Now we can query for our players with the following: 1 2 3 4 5 6 7 8 9 10 iex> Platform.Accounts.list_players [debug] QUERY OK source="players" db=3.0ms decode=3.7ms SELECT a0."id", a0."score", a0."username", a0."inserted_at", a0."updated_at" FRO\ M "players" AS a0 [] [%Platform.Accounts.Player{__meta__: #Ecto.Schema.Metadata<:loaded, "players">, id: 1, inserted_at: ~N[2017-04-08 14:55:28.674971], score: 1000, updated_at: ~N[2017-04-08 14:55:28.681607], username: "josevalim"}, %Platform.Accounts.Player{__meta__: #Ecto.Schema.Metadata<:loaded, "players">, id: 2, inserted_at: ~N[2017-04-08 14:55:34.139085], score: 2000, updated_at: ~N[2017-04-08 14:55:34.139091], username: "evancz"}] This function provides a good demonstration of how Phoenix uses well-named functions to abstract away implementation details and make things easy to use. It means we can use the list_players/0 function to fetch all the players from the database for our List Players page. 47 Phoenix Sign Up List Players Page Similarly, the Platform.Accounts module contains a get_player!/1 function that’s useful for our Show Player page, a create_player/1 function that’s useful for our New Player page, an update_player/2 function that’s useful for our Edit Player page, and a delete_player/1 function that we can use to remove player accounts. Player Fields When we created our players resource, we ran the generator to create two fields: • username • score It looks like Phoenix also included a couple of other fields by default: • id • inserted_at • updated_at Phoenix Sign Up 48 Adding Fields We have some existing fields for our players, but what if we want to add new fields? We’ll not only have to update our application, but also update our database. Let’s add a couple of new fields for our player accounts: • display_name • password • password_digest We’ll use a display_name field so players can display something other than their username on our game platform. We’ll also create a “virtual” field called password that users will enter on the sign up form. But we’ll only use the password_digest field to store a secure hash for user passwords, so we’re never storing the password field in plain text. Updating the Player Schema To start adding our new fields, let’s update the lib/platform/accounts/player.ex file with the following: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 defmodule Platform.Accounts.Player do use Ecto.Schema import Ecto.Changeset alias Platform.Accounts.Player schema "players" do field :display_name, :string field :password, :string, virtual: true field :password_digest, :string field :score, :integer, default: 0 field :username, :string, unique: true timestamps() end @doc false def changeset(%Player{} = player, attrs) do player |> cast(attrs, [:display_name, :password, :score, :username]) Phoenix Sign Up 21 22 23 24 49 |> validate_required([:username]) |> unique_constraint(:username) end end We’re adding our new fields to the "players" schema. Each of our new fields is a :string type. We’ll also take this opportunity to add a few settings to our fields so they contain the correct data. The password field is marked with virtual: true so it doesn’t get saved to the database (only the password_digest will get stored). The score field will be set to a default value with default: 0. And the username field is set to unique: true because we want every player to have a unique individual username. Player Changeset In the code example above, note that we didn’t just change the player schema. We also updated the changeset/2 function with the following: 1 2 3 4 5 6 def changeset(%Player{} = player, attrs) do player |> cast(attrs, [:display_name, :password, :score, :username]) |> validate_required([:username]) |> unique_constraint(:username) end The Ecto.Changeset28 module allows us to filter, cast, and validate our data. In our case, we only want to use the validate_required/1 function to verify that a new player enters a username so they can sign up for an account easily. We’ll also require the password field later, but we’ll need to implement additional functionality before we can get that working properly. Although the display_name field isn’t required when users sign up for an account, we want them to be able to change this field on the Edit Player page, so we add it to the cast/2 function along with the other fields. Lastly, we need to pipe to the unique_constraint/1 function with our :username field so users will get a message if they try creating multiple accounts with the same username. Generating a Migration Now we’ll need to update the database so it knows about the new fields we we’re adding to our players. Let’s run the following command to generate a migration file: 28 https://hexdocs.pm/ecto/Ecto.Changeset.html Phoenix Sign Up 1 50 $ mix ecto.gen.migration add_fields_to_player_accounts And we should see output showing that it successfully generated the migration file, which we’ll update next: 1 2 3 $ mix ecto.gen.migration add_fields_to_player_accounts * creating priv/repo/migrations * creating priv/repo/migrations/20170812150719_add_fields_to_player_accounts.exs Let’s update the migration file that we created in the priv/repo/migrations folder before we actually run the migration. We’ll alter the existing players database table and add the new fields. We’ll also add a unique_index at the bottom to ensure that each player has a unique username field: 1 2 3 4 5 6 7 8 9 10 11 12 defmodule Platform.Repo.Migrations.AddFieldsToPlayerAccounts do use Ecto.Migration def change do alter table(:players) do add :display_name, :string add :password_digest, :string end create unique_index(:players, [:username]) end end Running the Migration Now we can run the migration to update our database. This is the command we use to run our database migrations: 1 $ mix ecto.migrate When the migrations are successful, we should see output like this: 51 Phoenix Sign Up 1 2 3 4 5 6 $ mix ecto.migrate 11:11:29.028 [info] ts.change/0 forward 11:11:29.028 [info] 11:11:29.046 [info] 11:11:29.051 [info] == Running Platform.Repo.Migrations.AddFieldsToPlayerAccoun\ alter table players create index players_username_index == Migrated in 0.0s Updating Our Application Let’s update our application to work with our new player account fields. We’ll start with what we want our users to do when they first sign up. Open up the lib/platform_web/templates/player/new.html.eex file: 1 2 3 4 5 <h2>New Player</h2> <%= render "form.html", Map.put(assigns, :action, player_path(@conn, :create)) %> <span><%= link "Back", to: player_path(@conn, :index) %></span> From the looks of the code here, the page is rendering a "form.html" file, which is shared between our New Player page and Edit Player page. But we want slightly different behavior for our application. We want users to be able to sign up with minimal effort by entering only a username and password. Once they’re signed up, they can enter additional fields like their display_name. Working with Forms Here’s what our original New Player page looks like: 52 Phoenix Sign Up Original New Player Page We don’t actually want our players to be able to manually enter their scores, because the games should track player scores and update their accounts automatically. For now, we just want players to create a username and password to sign up. We’re going to move part of the form.html.eex file over to the new.html.eex file. We’ll also create a form in the edit.html.eex file, and ultimately we’ll be able to delete the shared form.html.eex file as a result. Let’s start by updating our new.html.eex file: 1 2 3 4 5 6 7 8 9 10 <h2>New Player</h2> <%= form_for @changeset, player_path(@conn, :create), fn f -> %> <%= if @changeset.action do %> <div class="alert alert-danger"> <p>Oops, something went wrong! Please check the errors below.</p> </div> <% end %> <div class="form-group"> 53 Phoenix Sign Up 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <%= label f, :username, "Player Username", class: "control-label" %> <%= text_input f, :username, placeholder: "Enter username...", class: "form-\ control" %> <%= error_tag f, :username %> </div> <div class="form-group"> <%= label f, :password, "Player Password", class: "control-label" %> <%= password_input f, :password, placeholder: "Enter password...", class: "f\ orm-control" %> <%= error_tag f, :password %> </div> <div class="form-group"> <%= submit "Submit", class: "btn btn-primary" %> </div> <% end %> We’re basically moving some of the content from the form.html.eex file into our new.html.eex file along with some minor changes. On a successful submission, we’re using the create action from our player controller. This is what it should look like in the browser: Updated New Player Page Phoenix Sign Up 54 Show Player Page After a new user is created, the application currently redirects them to the Show Player page. When we have the full platform application built out later, we’ll probably want to redirect them to a list of games to play. For now, we’ll update this page to display all the relevant fields for the player’s account: • • • • id display_name username score Let’s update the lib/platform_web/templates/player/show.html.eex file with the following: 1 2 3 4 5 6 7 8 9 10 11 12 13 <h2>Show Player</h2> <ul> <li><strong>ID: </strong><%= @player.id %></li> <li><strong>Display Name: </strong><%= @player.display_name %></li> <li><strong>Username: </strong><%= @player.username %></li> <li><strong>Score: </strong><%= @player.score %></li> </ul> <span><%= link "Edit", to: player_path(@conn, :edit, @player), class: "btn btn-d\ efault" %></span> <span><%= link "Back", to: page_path(@conn, :index), class: "btn btn-default" %>\ </span> We display all the relevant data on the page, and added a few classes to the buttons at the bottom to make things look nicer. Users can choose to Edit their accounts from here, and if they click the Back button they can navigate back to the home page. 55 Phoenix Sign Up Show Player Page Edit Player Page For the Edit Player page, we’re going to do much of the same that we did for the New Player page. We want users to be able to adjust their username and display_name fields for example, but they shouldn’t be able to manually alter their score field since that data should be coming from the games they play on the platform. Since we won’t have a full authentication system with a reset password feature, we’ll also allow users to change their password as needed on this page. Update the lib/platform_web/templates/player/edit.html.eex file to contain the following: 1 2 3 4 5 6 7 8 9 10 11 <h2>Edit Player</h2> <%= form_for @changeset, player_path(@conn, :update, @player), fn f -> %> <%= if @changeset.action do %> <div class="alert alert-danger"> <p>Oops, something went wrong! Please check the errors below.</p> </div> <% end %> <div class="form-group"> <%= label f, :display_name, "Change Display Name", class: "control-label" %> Phoenix Sign Up 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 56 <%= text_input f, :display_name, placeholder: "Enter display name...", class\ : "form-control" %> <%= error_tag f, :display_name %> </div> <div class="form-group"> <%= label f, :username, "Change Username", class: "control-label" %> <%= text_input f, :username, placeholder: "Enter username...", class: "form-\ control" %> <%= error_tag f, :username %> </div> <div class="form-group"> <%= label f, :password, "Change Password", class: "control-label" %> <%= text_input f, :password, placeholder: "Enter password...", class: "form-\ control" %> <%= error_tag f, :password %> </div> <div class="form-group"> <%= submit "Submit", class: "btn btn-primary" %> <span><%= link "Back", to: page_path(@conn, :index), class: "btn btn-default\ " %></span> </div> <% end %> After saving that file, we can go back to the Edit Player page and change the display_name field for one of our players: 57 Phoenix Sign Up Editing the Display Name Field On submission, we’ll be redirected to the Show Player page and see that the field was successfully changed: 58 Phoenix Sign Up Successful Player Update Shared Form Now that we’ve adjusted our New Player page and our Edit Player page, we’re able to delete the form.html.eex file that was shared between them since it’s no longer used anywhere. Saving Our Progress Since we’ve made quite a few changes, now would be a good time to run our tests locally with mix test: 1 2 3 4 5 6 $ mix test Compiling 2 files (.ex) .................... Finished in 0.2 seconds 20 tests, 0 failures If everything is passing, let’s go ahead and commit our changes: Phoenix Sign Up 1 2 59 $ git add . $ git commit -m "Update player fields and adjust templates" We can hold off on pushing this to production because we want to tackle authentication first, and that will be the topic of our next chapter. Summary We managed to accomplish a lot in this chapter. First, we learned how to add fields to our players and update the database accordingly. We also got some experience working with templates and designing how we want our users to interact with our application. We’re going to be working with Elm for the front-end of our application in this book, but this chapter gave a good introduction into what it feels like to add features to a Phoenix application. The first step towards adding authentication features is taken care of, and we’ll work towards working sign up and sign in features in the next chapter. Phoenix Authentication In the last chapter, we managed to update our players with the fields we’ll need to continue. Now, we can start implementing our authentication features. We’re going to begin with the bare minimum so users can sign up and sign in to our platform, but we’re not going to worry about more advanced authentication features like email verification or forgotten password mailers. Our goal is to allow users to sign up and sign in quickly, easily, and securely. Fetching Dependencies To get started with Phoenix authentication, we’ll need to add a couple of new dependencies. At the root of our project, take a look at the mix.exs file and find the deps/0 function. This function is where we specify which dependencies our application requires, and we can see there are already a handful that Phoenix includes by default. 1 2 3 4 5 6 7 8 9 10 11 12 defp deps do [ {:phoenix, "~> 1.3.0"}, {:phoenix_pubsub, "~> 1.0"}, {:phoenix_ecto, "~> 3.2"}, {:postgrex, ">= 0.0.0"}, {:phoenix_html, "~> 2.10"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:gettext, "~> 0.11"}, {:cowboy, "~> 1.0"} ] end The dependency we’ll use for securing our passwords is called (somewhat ironically) comeonin29 . This is what the syntax looks like for adding a new dependency: 1 {:comeonin, "~> 4.0"} In Elixir, this syntax is called a tuple. It’s commonly used as a way to reference keys and values. In this example, the first element of the tuple is an atom (:comeonin), and the second element is a string that indicates the version number ("∼> 4.0"). Comeonin allows us to choose from different password hashing algorithms, so we’ll also need to import another dependency called bcrypt_elixir30 to get everything working. Let’s update our deps/0 29 30 https://hex.pm/packages/comeonin https://hex.pm/packages/bcrypt_elixir Phoenix Authentication 61 function with the following: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 defp deps do [ {:phoenix, "~> 1.3.0"}, {:phoenix_pubsub, "~> 1.0"}, {:phoenix_ecto, "~> 3.2"}, {:postgrex, ">= 0.0.0"}, {:phoenix_html, "~> 2.10"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:gettext, "~> 0.11"}, {:cowboy, "~> 1.0"}, {:comeonin, "~> 4.0"}, {:bcrypt_elixir, "~> 1.0"}, ] end Save that file, and then from the command line we’ll run the mix command that fetches dependencies: 1 $ mix deps.get We should see the following results: 1 2 3 4 5 6 7 8 9 10 11 12 13 $ mix deps.get Resolving Hex dependencies... Dependency resolution completed: bcrypt_elixir 1.0.5 comeonin 4.0.3 ... * Getting comeonin (Hex package) Checking package (https://repo.hex.pm/tarballs/comeonin-4.0.3.tar) Fetched package * Getting bcrypt_elixir (Hex package) Checking package (https://repo.hex.pm/tarballs/bcrypt_elixir-1.0.5.tar) Fetched package ... Player Changesets Now that we’ve included our new dependencies, let’s take a look at the existing changeset/2 function inside the lib/platform/accounts/player.ex file. Phoenix Authentication 1 2 3 4 5 6 62 def changeset(%Player{} = player, attrs) do player |> cast(attrs, [:display_name, :password, :score, :username]) |> validate_required([:username]) |> unique_constraint(:username) end This is where we can add additional validations for our data and ensure that it conforms to our expectations. This function will remain our default player changeset, but we’ll also add a separate one called registration_changeset/2 for when players create a new account. Let’s add some validations and a new function that will allow us to encrypt passwords so they’re not stored in plain text. Update the changeset/2 function and add the following code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @doc false def changeset(%Player{} = player, attrs) do player |> cast(attrs, [:display_name, :password, :score, :username]) |> validate_required([:username]) |> unique_constraint(:username) |> validate_length(:username, min: 2, max: 100) |> validate_length(:password, min: 6, max: 100) |> put_pass_digest() end @doc false def registration_changeset(%Player{} = player, attrs) do player |> cast(attrs, [:password, :username]) |> validate_required([:password, :username]) |> unique_constraint(:username) |> validate_length(:username, min: 2, max: 100) |> validate_length(:password, min: 6, max: 100) |> put_pass_digest() end defp put_pass_digest(changeset) do case changeset do %Ecto.Changeset{valid?: true, changes: %{password: pass}} -> put_change(changeset, :password_digest, Comeonin.Bcrypt.hashpwsalt(pass)) _ -> changeset Phoenix Authentication 30 31 63 end end With this code, we’re able to add a couple of quick validations to ensure data is structured properly. We’re making sure that users enter both a username and password when they create a new account, and that those fields are of a certain length. More importantly, we’re piping into our new put_pass_digest/1 function, which will encrypt passwords using the comeonin dependency. Our new put_pass_digest/1 function takes in the player changeset, and then we add a case statement to determine whether or not it is valid. If the changeset is valid, we’re using our new dependency to hash the password field with Comeonin.Bcrypt.hashpwsalt(pass), and then store the hash in the password_digest field using the put_change/331 function. This is the reason we set the password field to virtual: true in the player schema, because we’re only going to store the hash in the player_digest field. In the event that the changeset was not valid, we just return it at the bottom of our put_pass_digest/1 function without any changes. This is a common pattern we can use for case statements where _ is a useful default case if none of the branches above applied. There is admittedly some code duplication between the two changeset functions. This is primarily due to the fact that we’re allowing users to sign up with a username and password, and we’re also allowing them to change their password field when they edit an account. But the main idea is that we can have different changesets that apply to different situations. In our case, we wanted to use the registration_changeset/2 for when players sign up, and use a separate changeset function for any other player changes. Using Our New Changeset To get these features working, we’ll need to adjust the create_player/1 function in the lib/platform/accounts/accounts.ex file. In the other functions, we’ll continue using the changeset/2 function, but in this one we want to use the registration_changeset/2 function so that both the username and password fields are required to create an account. Update the create_player/1 function with the following: 1 2 3 4 5 def create_player(attrs \\ %{}) do %Player{} |> Player.registration_changeset(attrs) |> Repo.insert() end 31 https://hexdocs.pm/ecto/Ecto.Changeset.html#put_change/3 Phoenix Authentication 64 Accounts Tests and Module Attributes When we run our tests, we use different sets of attributes to simulate valid and invalid data. When we ran the generator command to create our players resource, Phoenix created some initial values for us. We’ve since updated the fields that we’re working with, so we’ll need to make some changes to our tests as well. Let’s open the test/platform/accounts/accounts_test.exs file and take a look at the attributes: 1 2 3 4 5 6 7 8 9 describe "players" do alias Platform.Accounts.Player @valid_attrs %{score: 42, username: "some username"} @update_attrs %{score: 43, username: "some updated username"} @invalid_attrs %{score: nil, username: nil} # ... end In Elixir, these are called module attributes32 . They’re useful for creating constants that we can use throughout our tests. For example, we assign a map of valid player fields to the @valid_attrs module attribute, and then we can use those fields in the tests below. Let’s make some changes to our attribute data to account for the changes we’ve made to our player schema: 1 2 3 4 @valid_attrs %{password: "some password", username: "some username"} @update_attrs %{display_name: "some updated display name", password: "some updat\ ed password", score: 43, username: "some updated username"} @invalid_attrs %{password: nil, username: nil} We want to ensure that our players can sign up with just a username and password, so we include those in our @valid_attrs. Then, we can ensure that our other fields work by including them in @update_attrs. Lastly, we ensure that nil values won’t work to create new accounts by including them in @invalid_attrs. Let’s also go ahead and update our create_player/1 test case with the following to since we want users to create accounts with valid username and password fields. 32 https://elixir-lang.org/getting-started/module-attributes.html Phoenix Authentication 1 2 3 4 5 65 test "create_player/1 with valid data creates a player" do assert {:ok, %Player{} = player} = Accounts.create_player(@valid_attrs) assert player.password == "some password" assert player.username == "some username" end Fixtures, Maps, and Structs In the same test/platform/accounts/accounts_test.exs file, we have a player_fixture/1 function that returns a player “struct” we use as a sample player throughout the test cases. We still want to use this function to create a sample player, but we also want to ignore the password field in our test environment. We can use this as an opportunity to learn a little bit about Elixir structs and maps. We saw some examples of Elixir maps33 in the previous section about attributes. They are useful as key-value stores, and they’re considered an essential data structure. Here’s a simple example with two keys and two values: 1 %{password: "some password", username: "some username"} Elixir structs34 are similar to maps, but have additional structure for defining keys and values. Here’s an example of what a simple player struct might look like: 1 %Platform.Accounts.Player{password: "some password", username: "some username"} Our actual player struct is more complicated, because it needs to account for all the other fields, including things like id, updated_at, and other metadata. Let’s take a look at our existing player_fixture/1 function: 1 2 3 4 5 6 7 8 def player_fixture(attrs \\ %{}) do {:ok, player} = attrs |> Enum.into(@valid_attrs) |> Accounts.create_player() player end 33 34 https://elixir-lang.org/getting-started/keywords-and-maps.html#maps https://elixir-lang.org/getting-started/structs.html Phoenix Authentication 66 This function takes our map of valid player attributes (@valid_attrs) and uses the Accounts.create_player() function to create a new player account. Then, we pattern match the result to get the player struct and return the player at the bottom. We’re going to make a slight change to remove the password field. We’ll need to convert the struct into a map to delete the field, and then we’ll merge the fields back together to return the player struct. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 def player_fixture(attrs \\ %{}) do {:ok, player} = attrs |> Enum.into(@valid_attrs) |> Accounts.create_player() player_attrs_map = player |> Map.from_struct() |> Map.delete(:password) %Player{} |> Map.merge(player_attrs_map) end Rather than just returning the player struct at the bottom of the function, we’re converting the struct to a map using Map.from_struct() and then deleting the password field with Map.delete(:password). This gives us a map of all the player attributes except the password field. At the bottom of the function, we create a player struct with %Player{} and then merge all of the fields in our map together using Map.merge(player_attrs_map). Keep in mind that this is a lot to take in as we’re dealing with new data structures and a lot of new functions, so don’t worry if this seems slightly overwhelming at first. Working with maps and structs is so common in Elixir and Phoenix applications that we’ll pick it up easily as we gain experience. In the meantime, this provided a good starting point while we work towards our goal of fixing our test suite. Player Controller Tests We’ve updated our player changeset/2 function and fixed the tests for our player accounts, but we still have a few failing tests. Let’s switch to the test/platform_web/controllers/player_controller_test.exs file and make some changes. Similar to the way we updated our module attributes in the previous sections, let’s change @create_attrs, @update_attrs, and @invalid_attrs with the following: Phoenix Authentication 1 2 3 4 67 @create_attrs %{password: "some password", username: "some username"} @update_attrs %{display_name: "some updated display name", password: "some updat\ ed password", score: 43, username: "some updated username"} @invalid_attrs %{password: nil, username: nil} Now that we’ve made changes to our player schema and adjusted our attributes, we should be able to run our tests again and see them all passing: 1 2 3 4 5 6 7 $ mix test .................... Finished in 4.4 seconds 20 tests, 0 failures Randomized with seed 77808 Speeding Up Tests This part is optional, but it provides a good example of how we can configure our test environment and speed things up. You may have noticed that our tests are running more slowly than they were before. The password hashing algorithm takes a while, and we don’t necessarily need this in our test environment. Let’s add a configuration setting at the bottom of our config/test.exs file: 1 2 # Reduce bcrypt rounds to speed up tests config :bcrypt_elixir, :log_rounds, 4 Save the file, and then let’s try running our tests again to see if there’s a difference: 1 2 3 4 5 6 7 8 9 $ mix test Compiling 20 files (.ex) Generated platform app .................... Finished in 0.2 seconds 20 tests, 0 failures Randomized with seed 749042 We reduced the number of encryption rounds our hashing algorithm is running (only in our test environment), and this resulted in a noticeable difference in the amount of time our tests took to run (from 4.4 seconds to 0.2 seconds). 68 Phoenix Authentication Authentication Plug Players are currently able to create new accounts at http://0.0.0.0:4000/players/new. But we’ll want to add features so that users can sign in and sign out. Player Sign Up Page Let’s make a new controller called PlayerAuthController. Create a lib/platform_web/controllers/player_auth_controller.ex file, and add the following content: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 defmodule PlatformWeb.PlayerAuthController do import Plug.Conn alias Platform.Accounts.Player def init(opts) do Keyword.fetch!(opts, :repo) end def call(conn, repo) do player_id = get_session(conn, :player_id) player = player_id && repo.get(Player, player_id) assign(conn, :current_user, player) end end Phoenix Authentication 69 This will allow us to collect information about the current player’s session and assign it to :current_user so we can refer to that when handling our authentication features. Router Remembering back to when we set up our PlayerController in the Phoenix router, we used the default browser pipeline. If we open the lib/platform_web/router.ex file, we’ll see that there are quite a few plugs at the top: 1 2 3 4 5 6 7 8 9 10 11 12 13 defmodule PlatformWeb.Router do use PlatformWeb, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end # ... end At the bottom of the pipeline :browser block, let’s add our new authentication plug: 1 2 3 4 5 6 7 8 pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers plug PlatformWeb.PlayerAuthController, repo: Platform.Repo end This plug is going to allow us to restrict access to certain pages. Let’s update our application so users will be redirected to the New Player page at http://0.0.0.0:4000/players/new if they haven’t already signed in. Currently, when we access http://0.0.0.0:4000 in the browser, we see the index page from our PageController. In the upcoming chapters, we’ll be turning this into our Elm front-end application. 70 Phoenix Authentication Home Page We’re going to use our new PlayerAuthController to redirect users to the New Player page before they can view our home page. Authenticate Function Currently, users are able to navigate to all the pages within our application. We want to ensure that players are signed in before they access our Elm application and start playing games. At the bottom of our PageController, let’s add an authenticate/2 function. Open up the lib/platform_web/controllers/page_controller.ex file and add the following beneath the index/2 function: 1 2 3 4 5 6 7 8 9 10 defp authenticate(conn, _opts) do if conn.assigns.current_user() do conn else conn |> put_flash(:error, "You must be signed in to access that page.") |> redirect(to: player_path(conn, :new)) |> halt() end end Since we assigned the current player’s session to be the current_user inside our PlayerAuthController, we can use that to determine whether a visitor to our site is signed in. If they are, we’ll just return the connection and allow them to continue. Otherwise, if they’re attempting to access a restricted resource, we’ll display a message and redirect them back to the New Player page. Phoenix Authentication 71 In the same file, we’ll use the authenticate/2 function that we just created to determine whether or not players should be able to render the PageController index page. Above the index/2 function, add the following line of code: 1 plug :authenticate when action in [:index] This is what the full lib/platform_web/controllers/page_controller.ex file should look like: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 defmodule PlatformWeb.PageController do use PlatformWeb, :controller plug :authenticate when action in [:index] def index(conn, _params) do render conn, "index.html" end defp authenticate(conn, _opts) do if conn.assigns.current_user() do conn else conn |> put_flash(:error, "You must be signed in to access that page.") |> redirect(to: player_path(conn, :new)) |> halt() end end end Manual Testing If you’re wondering if the updates above broke our tests, you’re right. We usually run our test suite with mix test, but this time let’s manually test things out in the browser. Open up your browser and try visiting the PageController index page at http://0.0.0.0:4000: 72 Phoenix Authentication Restricted Page Alert It looks like our changes worked, because the application redirected us back to the New Player page. We managed to restrict access to the index page, and we see the flash alert (“You must be signed in to access that page.”) that we wrote in the authenticate/2 function at the bottom of the PageController. Fixing Our Tests Let’s push a quick fix for our tests. Open the test/platform_web/controllers/page_controller_test.exs file and replace it with the following code that tests our default route and the redirect that we created: Phoenix Authentication 1 2 3 4 5 6 7 8 73 defmodule PlatformWeb.PageControllerTest do use PlatformWeb.ConnCase test "redirects unauthenticated users away from index page", %{conn: conn} do conn = get conn, "/" assert html_response(conn, 302) =~ "redirect" end end Go ahead and run the mix test command again, and we should be all set with passing tests that give us confidence to keep adding features. Signing In How do we allow users to sign in to their newly created accounts? Let’s define a sign_in/2 function in our PlayerAuthController: 1 2 3 4 5 6 def sign_in(conn, player) do conn |> assign(:current_user, player) |> put_session(:player_id, player.id) |> configure_session(renew: true) end Right after a new player creates an account, we automatically want to use the sign_in/2 function to authenticate them on our platform. To accomplish this, we’ll need to update the create/2 function in our PlayerController. We’ll use the pipe operator to sign the player in before we display the flash message and redirect them: 1 2 3 4 5 6 7 8 9 10 11 def create(conn, %{"player" => player_params}) do case Accounts.create_player(player_params) do {:ok, player} -> conn |> PlatformWeb.PlayerAuthController.sign_in(player) |> put_flash(:info, "Player created successfully.") |> redirect(to: player_path(conn, :show, player)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end 74 Phoenix Authentication Let’s try it out. Go to the New Player page at http://0.0.0.0:4000/players/new and create a new user with both the username and password fields set to chrismccord: Creating a New User The new user we just created should now be signed in, and we’ll be directed to the Show Player page for the new user. 75 Phoenix Authentication Show Player Page Now, let’s try to access the PageController index page to verify that the user is authenticated. Go to the http://0.0.0.0:4000 page in your browser: Signed In Access to Index Page Success! We’re able to access this page because we created a new account and authenticated the new player at the same time. Phoenix Authentication 76 Sessions To complete our authentication features, we’ll need to handle user sessions. We were able to handle sign ins in the previous section because we took care of it while creating an account, but now we’ll also want to allow users to sign out and sign back in whenever they’d like. Let’s start by creating a PlayerSessionController. Create a new file called lib/platform_web/controllers/player_session_controller.ex, and add the following code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 defmodule PlatformWeb.PlayerSessionController do use PlatformWeb, :controller def new(conn, _) do render conn, "new.html" end def create(conn, %{"session" => %{"username" => user, "password" => pass}}) do case PlatformWeb.PlayerAuthController.sign_in_with_username_and_password(con\ n, user, pass, repo: Platform.Repo) do {:ok, conn} -> conn |> put_flash(:info, "Welcome back!") |> redirect(to: page_path(conn, :index)) {:error, _reason, conn} -> conn |> put_flash(:error, "Invalid username/password combination.") |> render("new.html") end end def delete(conn, _) do conn |> PlatformWeb.PlayerAuthController.sign_out() |> redirect(to: player_session_path(conn, :new)) end end This may seem like a lot at first, but we’ll go through it quickly along with the updates we make to the PlayerAuthController. The new/2 function will allow us to display a Player Sign In page (as opposed to the New Player page that new players will use). When we create new sessions, we’ll use a new function (which we’ll create soon) called sign_in_with_username_and_password/4. Lastly, the delete/2 function will allow users to sign out by deleting their session. Phoenix Authentication 77 Player Sign In View and Template Let’s create the view and the template for our player Player Sign In page. Add a new file called player_session_view.ex inside the lib/platform_web/views folder. Then, add the following to the file: 1 2 3 defmodule PlatformWeb.PlayerSessionView do use PlatformWeb, :view end We’ll also need to create the corresponding template. Create a lib/platform_web/templates/player_session folder, and then add a new.html.eex file inside with the following content: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <h2>Player Sign In</h2> <%= form_for @conn, player_session_path(@conn, :create), [as: :session], fn f ->\ %> <div class="form-group"> <%= label f, :username, "Player Username", class: "control-label" %> <%= text_input f, :username, placeholder: "Enter username...", class: "form-\ control" %> <%= error_tag f, :username %> </div> <div class="form-group"> <%= label f, :password, "Player Password", class: "control-label" %> <%= password_input f, :password, placeholder: "Enter password...", class: "f\ orm-control" %> <%= error_tag f, :password %> </div> <div class="form-group"> <%= submit "Sign In", class: "btn btn-primary" %> </div> <% end %> This gives us a Player Sign In page with a form for existing users to enter their username and password. Phoenix Authentication 78 Session Routing If you were still running a Phoenix server this whole time, you’ve probably noticed we’ve been creating errors in our application. But we’re getting close to a working authentication system. We’ll need to update our router to reflect our new session features. Open the lib/platform_web/router.ex file and add our session resource: 1 2 3 4 5 6 7 scope "/", PlatformWeb do pipe_through :browser get "/", PlayerController, :new resources "/players", PlayerController resources "/sessions", PlayerSessionController, only: [:new, :create, :delete] end This will add the necessary routes so our players can create and delete their sessions to sign in and out of the platform. Signing In and Out Lastly, we’ll create the functions in our PlayerAuthController that tie everything together. Add the sign_in_with_username_and_password/4 function and the sign_out/1 functions below the sign_in/2 function at the bottom of the lib/platform_web/controllers/player_auth_controller.ex file. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def sign_in_with_username_and_password(conn, username, given_pass, opts) do repo = Keyword.fetch!(opts, :repo) player = repo.get_by(Player, username: username) cond do player && Comeonin.Bcrypt.checkpw(given_pass, player.password_digest) -> {:ok, sign_in(conn, player)} player -> {:error, :unauthorized, conn} true -> Comeonin.Bcrypt.dummy_checkpw() {:error, :not_found, conn} end end 79 Phoenix Authentication 16 17 18 def sign_out(conn) do configure_session(conn, drop: true) end What the sign_in_with_username_and_password/4 function does is to grab the player from the database by their username field. If the player exists and has the correct password, they will be signed in. Otherwise, we’ll return an error. For the sign_out/1 function, we’re dropping the current user’s session to sign them out of the platform. Trying Things Out It’s a good idea to try out these features using “incognito” browser windows. That will give us a way to open a new browser window in a clean state. If you’re using Google Chrome on macOS, you can create a new incognito window with Command + Shift + N. It’s also a good idea to restart your Phoenix server with mix phx.server at this point to get things up and running. We can test out the sign in process with the same account that we created in the previous sections. Go to the Player Sign In page at http://0.0.0.0:4000/sessions/new and try entering chrismccord for both the username and password fields. Player Sign In Page 80 Phoenix Authentication Success! We get a “Welcome back!” message letting us know that we were able to sign in successfully: Successful Sign In Displaying the Player Status We’ve given users the ability to create new accounts at /players/new, and players can sign in with existing accounts at /sessions/new. As a last step for this chapter, let’s find a place to let players know when they’re signed in, and give them a way to sign out. First, let’s open the lib/platform_web/templates/layout/app.html.eex file and remove the default Phoenix header. Update the contents of the <header> tag with the following: 1 2 3 4 5 6 7 8 9 <header class="header"> <nav role="navigation"> <ul class="nav nav-pills pull-right"> <%= link "Sign Up", to: player_path(@conn, :new), class: "btn btn-sm btn-s\ uccess" %> <%= link "Sign In", to: player_session_path(@conn, :new), class: "btn btn-\ sm btn-primary" %> </ul> </nav> 81 Phoenix Authentication 10 11 <span class="logo"></span> </header> This places buttons in the header for users to either sign up or sign in. Header Sign Up and Sign In Buttons Depending on whether players are signed in or not, we want to show something different in the header. Before a player has authenticated, we want to display the buttons that we see in the screenshot above. If a player has signed in, we want to show them their username in the header along with a sign out button. Let’s update our <header> tag again with the following: 1 2 3 4 5 6 7 8 9 10 11 <header class="header"> <nav role="navigation"> <ul class="nav nav-pills pull-right"> <%= if @current_user do %> <p class="small">Signed in as <strong><%= @current_user.username %></str\ ong></p> <%= link "Sign Out", to: player_session_path(@conn, :delete, @current_us\ er), method: "delete", class: "btn btn-sm btn-danger" %> <% else %> <%= link "Sign Up", to: player_path(@conn, :new), class: "btn btn-sm btn\ -success" %> 82 Phoenix Authentication 12 13 14 15 16 17 18 <%= link "Sign In", to: player_session_path(@conn, :new), class: "btn bt\ n-sm btn-primary" %> <% end %> </ul> </nav> <span class="logo"></span> </header> In the code above, we’re creating a conditional to determine whether or not a @current_user is present. When there is a user signed in, we display their username with small text along with a sign out button. Header Signed In Display Summary We managed to accomplish what we set out to do in this chapter. We’ve given our users a simple way to sign up, sign in, and sign out of our application. We also have the ability to restrict access to certain pages to ensure that users are authorized to access the correct data. Next, we’ll be building out the games section for our platform, and getting our first taste of what it’s like to work with Elm. Phoenix API and Ecto Relationships We now have players for our gaming platform, but we don’t have any games yet. We’re going to create an Elm application for our front-end that will display our lists of players and games, but first let’s create the back-end JSON API. And we’ll find a way to establish relationships between our players and games so that we can model our data properly. Generating the JSON API Let’s begin by creating an endpoint for our games. We want each game to have fields for title, description, thumbnail, and featured. These fields will allow us to display a list of games for players to choose from, and the featured field will be a simple boolean value we can use to feature particular games in a special section. We can also add other fields later, but this will be a good start. We’ll run the Phoenix generator from the command line to get started: 1 2 $ mix phx.gen.json Products Game games description:string featured:boolean thumb\ nail:string title:string This is similar to the way we created our players resource, but this time we’re using phx.gen.json35 instead of phx.gen.html36 . Also note that we’re keeping our contexts intentionally abstract in this book. Although we’re building features that are specific to our gaming domain (players and games), we want to be able to adapt this same material for other uses too. We’re using the Accounts context for our players and the Products context for our games, and we could easily use these same concepts to the domain of a bookstore where our Accounts might be readers and our Products could be books. Here’s what the output should look like when we run the generator for our new games resource: 35 36 https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Json.html https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.html Phoenix API and Ecto Relationships 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 84 $ mix phx.gen.json Products Game games description:string featured:boolean thumb\ nail:string title:string * creating lib/platform_web/controllers/game_controller.ex * creating lib/platform_web/views/game_view.ex * creating test/platform_web/controllers/game_controller_test.exs * creating lib/platform_web/views/changeset_view.ex * creating lib/platform_web/controllers/fallback_controller.ex * creating lib/platform/products/game.ex * creating priv/repo/migrations/20170826154100_create_games.exs * creating lib/platform/products/products.ex * injecting lib/platform/products/products.ex * creating test/platform/products/products_test.exs * injecting test/platform/products/products_test.exs Add the resource to your :api scope in lib/platform_web/router.ex: resources "/games", GameController, except: [:new, :edit] Remember to update your repository by running migrations: $ mix ecto.migrate Let’s hold off on running the Ecto migration, because we’ll want to make a few changes in the next sections first. API Routing Let’s follow the instructions that Phoenix provides for us. Open up the lib/platform_web/router.ex file. Instead of adding to the browser scope like we did previously, we’re going to add this resource to the /api scope. This means our two scopes will look like this: 1 2 3 4 5 6 7 8 9 10 scope "/", PlatformWeb do pipe_through :browser get "/", PageController, :index resources "/players", PlayerController resources "/sessions", PlayerSessionController, only: [:new, :create, :delete] end scope "/api", PlatformWeb do pipe_through :api Phoenix API and Ecto Relationships 11 12 13 85 resources "/games", GameController, except: [:new, :edit] end Establishing Relationships We generated a new migration for our games table above. But, we also want to form an association from our players table to our new games table. We’re going to create a new table called gameplays that will store a game_id as a reference to the games table, a player_id as a reference to the players table, and a player_score that will track a player’s score for the current play through the game. Let’s take a look at the schema files for players and games. Then, we’ll create a new schema for our gameplays. First, open the lib/platform/accounts/player.ex file, and update our schema with the following: 1 2 3 4 5 6 7 8 9 10 11 schema "players" do many_to_many :games, Game, join_through: Gameplay field field field field field :display_name, :string :password, :string, virtual: true :password_digest, :string :score, :integer, default: 0 :username, :string, unique: true timestamps() end We’ll also need to add two alias lines above for the Game and Gameplay modules to work: 1 2 alias Platform.Products.Game alias Platform.Products.Gameplay This means that we’re establishing a many_to_many37 relationship between our players and games, and that we’re joining these through the Gameplay module that we’ll be creating shortly. Next, we’ll do something similar for the lib/platform/products/game.ex file. We’ll add our two alias lines for Gameplay and Player at the top: 37 https://hexdocs.pm/ecto/Ecto.Schema.html#many_to_many/3 Phoenix API and Ecto Relationships 1 2 86 alias Platform.Products.Gameplay alias Platform.Accounts.Player Then, we’ll add our many_to_many relationship between our games and our players through the Gameplay module. 1 2 3 4 5 6 7 8 9 10 schema "games" do many_to_many :players, Player, join_through: Gameplay field field field field :description, :string :featured, :boolean, default: false :thumbnail, :string :title, :string timestamps() end Creating Gameplays Now, we can create the Gameplay part of our application that connects our players and games. We’re using the same mix phx.gen.json generator we used for our games, but this time we’re using references to associate our gameplay records with player_id and game_id fields. 1 2 $ mix phx.gen.json Products Gameplay gameplays game_id:references:games player_i\ d:references:players player_score:integer Keep in mind that we could manually create many of these files and custom build these features into our application. But using these generators gives us a lot of scaffolding that allows us to move quickly, and it will save us a ton of effort later in the book when we’re ready to display our player scores. Here is what the output should look like for our generator command. We’ll be prompted with a warning about overwriting some files, but we can just enter the Y character to continue since we just generated those files for games. Phoenix API and Ecto Relationships 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 87 $ mix phx.gen.json Products Gameplay gameplays game_id:references:games player_i\ d:references:players player_score:integer The following files conflict with new files to be generated: * lib/platform_web/views/changeset_view.ex * lib/platform_web/controllers/fallback_controller.ex See the --web option to namespace similarly named resources Proceed with interactive overwrite? [Yn] Y * creating lib/platform_web/controllers/gameplay_controller.ex * creating lib/platform_web/views/gameplay_view.ex * creating test/platform_web/controllers/gameplay_controller_test.exs * creating lib/platform/products/gameplay.ex * creating priv/repo/migrations/20171217194455_create_gameplays.exs * injecting lib/platform/products/products.ex * injecting test/platform/products/products_test.exs Add the resource to your :api scope in lib/platform_web/router.ex: resources "/gameplays", GameplayController, except: [:new, :edit] Remember to update your repository by running migrations: $ mix ecto.migrate Next, we’ll go ahead and add the resources line that we see in the output above to our lib/platform_web/router.ex file. Here is the "/api" scope for our router: 1 2 3 4 5 6 scope "/api", PlatformWeb do pipe_through :api resources "/games", GameController, except: [:new, :edit] resources "/gameplays", GameplayController, except: [:new, :edit] end Our New Gameplay Schema Let’s open the lib/platform/products/gameplay.ex file to take a look at our new schema. Phoenix API and Ecto Relationships 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 88 defmodule Platform.Products.Gameplay do use Ecto.Schema import Ecto.Changeset alias Platform.Products.Gameplay schema "gameplays" do field :player_score, :integer field :game_id, :id field :player_id, :id timestamps() end @doc false def changeset(%Gameplay{} = gameplay, attrs) do gameplay |> cast(attrs, [:player_score]) |> validate_required([:player_score]) end end We used many_to_many relationships in both our Player schema and our Game schema. For our Gameplay schema, we’ll use belongs_to38 to form the association. We’ll also add a few alias statements and add a default: 0 value for the player_score field so we don’t end up storing nil values in the database. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 defmodule Platform.Products.Gameplay do use Ecto.Schema import Ecto.Changeset alias Platform.Products.Game alias Platform.Products.Gameplay alias Platform.Accounts.Player schema "gameplays" do belongs_to :game, Game belongs_to :player, Player field :player_score, :integer, default: 0 38 https://hexdocs.pm/ecto/Ecto.Schema.html#belongs_to/3 Phoenix API and Ecto Relationships 15 16 17 18 19 20 21 22 23 24 89 timestamps() end @doc false def changeset(%Gameplay{} = gameplay, attrs) do gameplay |> cast(attrs, [:player_score]) |> validate_required([:player_score]) end end Running Our Migration We can now run our migration to update the database with mix ecto.migrate. This creates tables for both games and gameplays. 1 2 3 4 5 6 7 8 9 10 11 $ mix ecto.migrate Compiling 21 files (.ex) Generated platform app [info] == Running Platform.Repo.Migrations.CreateGames.change/0 forward [info] create table games [info] == Migrated in 0.0s [info] == Running Platform.Repo.Migrations.CreateGameplays.change/0 forward [info] create table gameplays [info] create index gameplays_game_id_index [info] create index gameplays_player_id_index [info] == Migrated in 0.0s Lastly, we’ll run our tests to make sure everything is still working: 1 2 3 4 5 $ mix test ................................................ Finished in 0.5 seconds 48 tests, 0 failures It’s great that we have 48 passing tests. Granted, these were created by the Phoenix generators, but it gives us some level of confidence that our application is working when the tests are passing. 90 Phoenix API and Ecto Relationships Trying Out our JSON API Let’s start up our Phoenix server with mix phx.server. For our players resource, we were using URLs like http://0.0.0.0:4000/players to access the templates. But now that we added a JSON resource, we’ll need to use /api in our URLs. Try to access http://0.0.0.0:4000/api/games in the browser. We shouldn’t see an error, but we also don’t have any game data to display yet (note that your browser might display JSON data differently): Games API with No Data Creating a Game We can’t interact with our game resources the same way we did with players, because we’re only working with JSON and don’t have any HTML pages to view. Instead, let’s fire up an iex session and create a new game from the command line. First, enter the following at the command line to get started: 1 $ iex -S mix phx.server Now we can create a new game called “Platformer” manually by using the create_game/1 function from the Platform.Products module. 1 2 3 iex> Platform.Products.create_game(%{title: "Platformer", description: "Platform\ game example.", thumbnail: "http://via.placeholder.com/300x200", featured: true\ }) We’re setting values for the game’s title, description, thumbnail, and featured fields. And the output should look something like this: 91 Phoenix API and Ecto Relationships 1 2 3 4 5 6 7 8 9 10 11 iex> Platform.Products.create_game(%{title: "Platformer", description: "Platform\ game example.", thumbnail: "http://via.placeholder.com/300x200", featured: true\ }) # ... {:ok, %Platform.Products.Game{__meta__: #Ecto.Schema.Metadata<:loaded, "games">, description: "Platform game example.", featured: true, id: 1, inserted_at: ~N[2017-12-04 15:16:16.957673], players: #Ecto.Association.NotLoaded<association :players is not loaded>, thumbnail: "http://via.placeholder.com/300x200", title: "Platformer", updated_at: ~N[2017-12-04 15:16:16.967729]}} Now that we have some data, we should be able to restart the server and reload the http://0.0.0.0:4000/api/games URL in our browser to see the results: Games API with Data Player API We had previously been using the browser to work with our player accounts. But since we’re transitioning to an API for our games, we’ll also want to make our players resource accessible as JSON too. Let’s create a new controller in the lib/platform_web/controllers folder. We’ll call the file player_api_controller.ex and then add the following contents: Phoenix API and Ecto Relationships 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 defmodule PlatformWeb.PlayerApiController do use PlatformWeb, :controller alias Platform.Accounts alias Platform.Accounts.Player action_fallback PlatformWeb.FallbackController def index(conn, _params) do players = Accounts.list_players() render(conn, "index.json", players: players) end def create(conn, %{"player" => player_params}) do with {:ok, %Player{} = player} <- Accounts.create_player(player_params) do conn |> put_status(:created) |> put_resp_header("location", player_path(conn, :show, player)) |> render("show.json", player: player) end end def show(conn, %{"id" => id}) do player = Accounts.get_player!(id) render(conn, "show.json", player: player) end def update(conn, %{"id" => id, "player" => player_params}) do player = Accounts.get_player!(id) with {:ok, %Player{} = player} <- Accounts.update_player(player, player_para\ ms) do render(conn, "show.json", player: player) end end def delete(conn, %{"id" => id}) do player = Accounts.get_player!(id) with {:ok, %Player{}} <- Accounts.delete_player(player) do send_resp(conn, :no_content, "") end end 92 Phoenix API and Ecto Relationships 43 93 end This may look like a lot of code, but we’re essentially copying the same thing that the Phoenix generators gave us for our games API and making small adjustments so that we can work with accounts and players instead of products and games. We’ll also want to update our lib/platform_web/router.ex file with the new resource: 1 2 3 4 5 6 7 scope "/api", PlatformWeb do pipe_through :api resources "/games", GameController, except: [:new, :edit] resources "/gameplays", GameplayController, except: [:new, :edit] resources "/players", PlayerApiController, except: [:new, :edit] end To finish making our players accessible via a JSON API, we need to add a view. Create a file named player_api_view.ex inside the lib/platform_web/views folder and add the following content: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 defmodule PlatformWeb.PlayerApiView do use PlatformWeb, :view alias PlatformWeb.PlayerApiView def render("index.json", %{players: players}) do %{data: render_many(players, PlayerApiView, "player.json")} end def render("show.json", %{player: player}) do %{data: render_one(player, PlayerApiView, "player.json")} end def render("player.json", %{player_api: player_api}) do %{id: player_api.id, username: player_api.username, display_name: player_api.display_name, score: player_api.score} end end Our PlayerApiView module is similar to what we have in the GameView module. When we load the http://0.0.0.0:4000/api/players URL, we’re using render_many/3 to list all the players. When we only want to show a single player, we can use a URL like http://0.0.0.0:4000/api/players/1 94 Phoenix API and Ecto Relationships that will use render_one/3 to only display a single user’s JSON data. At the bottom, we’re creating a function that returns a map with all our player data. We can add or remove fields here whenever we want to adjust the fields that are accessible via the JSON API. This is all great news because it means we can still use the http://0.0.0.0:4000/players URL to access our list of players in the browser, and we can use http://0.0.0.0:4000/api/players to see our player data as JSON. Player Data in Browser Scope 95 Phoenix API and Ecto Relationships Player Data in API Scope Now would be a good time to commit changes to your Git repository if you haven’t already done so since we’ve come a long way in this chapter. Summary We managed to accomplish our goal for this chapter of creating a JSON API for the games on our platform. And we also learned about Ecto relationships as we connected our players, games, and gameplays together. In the next chapter, we’ll get an introduction to the Elm language. And we’ll start working towards using the Phoenix JSON API that we built here to supply data for our Elm application. Elm Introduction We’re going to build the front-end of our application using the Elm language. That’s going to involve using data from our back-end Phoenix application, and decoding it into an Elm front-end application. Then, we’ll also use Elm to start building minigames for our platform. But before we get into all that, let’s take a quick look about what Elm is, how it works, and why we’re so eager to use it. Introduction Elm is likely to look and feel a little foreign at first. But don’t let that scare you away, because it’s a very strong language that can be very nice to work with. The design of Elm allows the language to offer things that many other languages simply can’t. Not only does it enable developers to write code that is free from errors, but also results in code that is easier to refactor and more maintainable. I could go on for months about how great Elm is (and I do that at the local Elm meetup that I help organize), but let’s dive into some basic examples. Hello.elm We can start with a simple “Hello World” example as a demonstration of how easy it can be to get Elm up and running. Here’s what a simple Elm program (with a filename of Hello.elm) looks like: 1 2 3 4 5 6 7 module Hello exposing (..) import Html main = Html.text "Hello World" The syntax is concise, and this serves as a quick way to display some text on a page in the browser (we’ll get to that in a second). But this is also a good example in the sense that it shows a handful of things about Elm that might seem unsettling at first. Where are all the parentheses and curly braces? What’s with the odd spacing? Elm Introduction 97 Elm Syntax The first line is Elm boilerplate for defining our module. Since this is a functional language, everything we do is essentially going to be modules and functions. Functions will be everything to us, and modules will help us gather and organize all those functions. What we’re saying with the first line of code is that we’re creating the Hello module, and “exposing” everything (..) from it. So if someone wanted to use our application, they would just need to import it and they could use any function they’d like. Modules, Functions, and Types Speaking of imports, the next line says import Html. This allows us to use any function from Elm’s built-in Html library39 . The function that we’re using in this program is the Html.text40 function, which takes a single string as an argument. If we take a look at the documentation for the text function, we can see the Elm type syntax: 1 text : String -> Html msg That means when we added Html.text "Hello World" to our program, the "Hello World" string was our only argument. And Html msg was the return type. Don’t worry too much about typing yet, but it’s helpful to think about a couple of quick things for now: • How many arguments does a function take? • What is the type of each argument? • What is the type of the return value? In our case, text is the name of the function. Then we use a : symbol before the list of arguments. This function only takes a single argument, and it’s a String. Then, we use the arrow symbol -> after the argument. Lastly, we indicate the return type, which is an Html msg. The msg part isn’t important, and we could have said Html a. The important part is that we are returning some HTML code that we are going to use to display the text on a page in the browser. 39 40 http://package.elm-lang.org/packages/elm-lang/html/latest/Html http://package.elm-lang.org/packages/elm-lang/html/latest/Html#text Elm Introduction 98 Main Function The main function is basically the starting point for our application. When we run it, the Elm runtime is going to look for main. But Elm is way more fun to play with than it is to read about, so let’s finally run our example. Before we can run Elm code, we’ll need to install Elm. There are different ways of doing it, but the easiest way is to use npm since most web developers have Node.js41 installed already. Alternatively, there are also installers available on the Elm home page42 . 1 $ npm install -g elm That should be all you need. It globally installs the elm command on your machine, so you can run it from the command line. In fact, type elm now and take a look at the output: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ elm Elm Platform 0.18.0 - a way to run all Elm tools Usage: elm <command> [<args>] Available commands include: make package reactor repl Compile an Elm file or project into JS or HTML Manage packages from <http://package.elm-lang.org> Develop with compile-on-refresh and time-travel debugging A REPL for running individual expressions You can learn more about a specific command by running things like: elm make --help elm package --help elm <command> --help In all these cases we are simply running 'elm-<command>' so if you create an executable named 'elm-foobar' you will be able to run it as 'elm foobar' as long as it appears on your PATH. We’ll use all these features eventually, but for now let’s focus on two of them. First, let’s create a temporary directory called elm that we’ll use to hold our Elm files, and then we’ll use the elmpackage command we see above to fetch the libraries we need. Also note that we don’t necessarily need to place this folder inside our existing Phoenix application yet, and that we’ll take care of integrating Phoenix and Elm in the next chapter. 41 42 https://nodejs.org/en/ http://elm-lang.org/ Elm Introduction 1 2 3 99 $ mkdir elm $ cd elm $ elm-package install We’re creating an empty elm folder, and then when we run elm-package install it’s going to create all the files we need: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ elm-package install Some new packages are needed. Here is the upgrade plan. Install: elm-lang/core 5.1.1 elm-lang/html 2.0.0 elm-lang/virtual-dom 2.0.4 Do you approve of this plan? [Y/n] Y Starting downloads... � elm-lang/virtual-dom 2.0.4 � elm-lang/html 2.0.0 � elm-lang/core 5.1.1 Packages configured successfully! This is everything we need. Let’s create the simple Hello.elm file for our “Hello World” program: 1 2 3 4 5 6 7 module Hello exposing (..) import Html main = Html.text "Hello World" All that’s left is to run our program. Elm comes with a utility called elm-reactor that we can use to serve our program locally and then access it in our browser. Go ahead and run it from the command line: 100 Elm Introduction 1 2 3 $ elm-reactor elm-reactor 0.18.0 Listening on http://localhost:8000 Now you can visit http://localhost:8000 in your browser and see the files and dependencies for our project. We only have a single Elm file (Hello.elm), so let’s click it to compile and see the results. elm-reactor Elm Hello World Example Elm Introduction 101 elm-format “Hello World” is admittedly not the most exciting example. But the good news is that we have Elm installed and we have a simple program up and running. Let’s take a look a couple more things that will make our lives easier with this example still in mind. Before we move any further, definitely consider trying elm-format43 . It’s a plugin that you can use with your editor that makes working with Elm much easier. It’s particularly great when you’re getting started, because you can type a rough idea of what you want to do and then immediately know if it’s valid code as long as elm-format reformats the code and doesn’t mention errors or warnings. While elm-format is not strictly necessary, and it’s possible to write solid Elm code without it, it’s a remarkably helpful tool that can help you focus on problem solving instead of worrying about valid syntax. Comments and Type Signatures Let’s go ahead and add a quick comment with the -- comment syntax to indicate the filename at the top: 1 2 3 4 5 6 7 8 -- Hello.elm module Hello exposing (..) import Html main = Html.text "Hello World" Next, let’s refactor our import declaration slightly to be more explicit about which functions we want to import from the Html module: 43 https://github.com/avh4/elm-format Elm Introduction 1 2 3 4 5 6 7 8 102 -- Hello.elm module Hello exposing (..) import Html exposing (Html, text) main = text "Hello World" We can also add a type signature for our main function: 1 2 3 4 5 6 7 8 9 -- Hello.elm module Hello exposing (..) import Html exposing (Html, text) main : Html msg main = text "Hello World" When we create type signatures, we’re restating the function name first. Then, after the : character, we give the types of the arguments and the return value. Since this function doesn’t have any arguments, we’re just giving the return type here, which is Html msg. That just means we’re returning some HTML code when we return a value from the Html.text function that we’re using. The pipe operator we discovered in Elixir also works in Elm. In fact, we can rewrite our “Hello World” text in all uppercase letters with the following: 1 2 3 4 5 6 7 8 9 10 11 -- Hello.elm module Hello exposing (..) import Html exposing (Html, text) main : Html msg main = "Hello World" |> String.toUpper |> text Elm Introduction 103 Inside the main function, we start with the raw "Hello World" string. Then, we pipe that to the toUpper44 function from the String45 module. Then, we pipe the result of that to the text function that will return the Html msg that our function is meant to return. After seeing the results in the browser, feel free to delete the temporary elm folder that we created in this chapter since we’ll be setting things up inside our Phoenix application in the next chapter. Summary There’s a lot more to Elm than what we’ve covered in our simple “Hello World” example. But this is a good start to get a look at the syntax and see that it’s not so scary as it might seem at first. In the next sections, we’re going to set up our Phoenix application to use Elm for the front-end. We’re also going to talk about the Elm Architecture, and how to pull data from our API into the front-end application. 44 45 http://package.elm-lang.org/packages/elm-lang/core/latest/String#toUpper http://package.elm-lang.org/packages/elm-lang/core/latest/String Elm Setup We’re excited to have our back-end up and running, and we’ve gotten a brief look at the Elm language. Now, let’s figure out how we can write Elm code for the front-end of our Phoenix application. We already installed Elm globally in the last chapter, and now we can take a look at configuring Elm with Phoenix using a tool called Brunch. Configuring Elm within Phoenix Phoenix front-end files are located in the assets folder. Let’s begin by checking out the existing package.json file that Phoenix gives us by default: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "repository": {}, "license": "MIT", "scripts": { "deploy": "brunch build --production", "watch": "brunch watch --stdin" }, "dependencies": { "phoenix": "file:../deps/phoenix", "phoenix_html": "file:../deps/phoenix_html" }, "devDependencies": { "babel-brunch": "6.1.1", "brunch": "2.10.9", "clean-css-brunch": "2.10.0", "uglify-js-brunch": "2.10.0" } } We can use any front-end build tool we prefer, but Phoenix comes with a minimalist tool called brunch46 , which we can see listed in the devDependencies. You’re welcome to try other options, but Brunch tends to work well for our purposes. The initial configuration takes some work, but afterward we’ll be able to focus on development with Elixir and Elm. 46 http://brunch.io Elm Setup 105 Let’s navigate to the assets folder from the command line and run the following command to ensure our project works with the elm language and the elm-brunch tool: 1 $ npm install --save-dev elm elm-brunch This will add two new lines to the devDependencies section of our package.json file. 1 2 3 4 5 6 7 8 "devDependencies": { "babel-brunch": "6.1.1", "brunch": "2.10.9", "clean-css-brunch": "2.10.0", "elm": "^0.18.0", "elm-brunch": "^0.9.0", "uglify-js-brunch": "2.10.0" } Updating .gitignore The default .gitignore file is configured to ignore all the files that get added to the node_modules folder. So our repository tracks changes to the package.json file, but ignores all the files created in the node_modules directory when we ran the npm install command. Let’s take this opportunity to update the .gitignore file in the root of our platform project so our repository won’t need to track extraneous files that Elm will generate for us. Similar to the way that npm creates a node_modules folder, Elm will generate a folder called elm-stuff that we can ignore. Open the .gitignore file at the root of our project, and add the following code to the bottom: 1 2 3 # Elm /assets/elm-stuff /assets/js/main.js Now, run the following command from inside the assets folder of our Phoenix project to install Elm packages: 1 $ elm-package install It should show similar output to what we saw in the previous chapter: Elm Setup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 106 $ elm-package install Some new packages are needed. Here is the upgrade plan. Install: elm-lang/core 5.1.1 elm-lang/html 2.0.0 elm-lang/virtual-dom 2.0.4 Do you approve of this plan? [Y/n] Y Starting downloads... � elm-lang/html 2.0.0 � elm-lang/virtual-dom 2.0.4 � elm-lang/core 5.1.1 Packages configured successfully! This command creates a new elm-package.json file that we need inside our assets folder. And it also creates the elm-stuff folder that won’t need to be tracked by our repository. This is a great location for our files, because it means we’ll have collocated our front-end files with package.json for any Node libraries and elm-package.json for any Elm libraries we want to include. Elm Folder We’ll need a place to put our Elm code inside our Phoenix application. So let’s create a new folder called elm inside our assets folder. We’ll use this folder to store all of our Elm source code. This allows us to collocate our Elm front-end code with the rest of our front-end code. It also means that we won’t need to put all of our Elm files inside the lib directory, which is good because we don’t want to make Phoenix scan all these files when it recompiles our Elixir code. The Elm source code we write will be committed to our repository, but we want our Phoenix application to compile it to JavaScript automatically (we won’t have to work with those JavaScript output files directly). Main.elm Inside our new assets/elm folder, let’s create a new file called Main.elm and add the following content: Elm Setup 1 2 3 4 5 6 7 8 107 module Main exposing (..) import Html exposing (Html, text) main : Html msg main = text "Hello from Elm!" This is a simple Elm program that will print “Hello from Elm!” in the browser once we get everything wired together. The way it will work is that the Brunch build tool will watch for changes to our Elm source code, and then compile the results to a JavaScript file. So the changes we make to the assets/elm/Main.elm file will be compiled to assets/js/main.js. To get this up and running, we’ll need to configure Brunch. Brunch Configuration Inside the assets folder, open the brunch-config.js file and replace the contents with the code below (note that the default file contains a lot of comments, but we’re removing them here for brevity): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 exports.config = { files: { javascripts: { joinTo: "js/app.js" }, stylesheets: { joinTo: "css/app.css" }, templates: { joinTo: "js/app.js" } }, conventions: { assets: /^(static)/ }, paths: { watched: ["static", "css", "js", "vendor", "elm"], public: "../priv/static" }, plugins: { babel: { ignore: [/vendor/] }, elmBrunch: { mainModules: ["elm/Main.elm"], makeParameters: ["--debug"], outputFolder: "../assets/js" } }, modules: { Elm Setup 21 22 23 24 108 autoRequire: { "js/app.js": ["js/app"] } }, npm: { enabled: true } }; This file has been condensed quite a bit to keep it short, but the main things to note are the watched paths section, and the elmBrunch plugin. These configuration options are basically telling the Phoenix application where to look for our Elm source code ("elm/Main.elm"), and where we’ll output the results ("../assets/js"). We also add a --debug parameter that will allow us to use the Elm debugger while we develop our application. Compiling with Phoenix We’ll still need to take a few additional steps before we can render our Elm application in the browser, but at this point we should be able to get Phoenix to compile our Elm source code when we run the server. Let’s start up our Phoenix server with mix phx.server, and we should see a message in the Terminal about how the elm/Main.elm file was compiled to the ../assets/js/main.js file. 1 2 3 4 $ mix phx.server [info] Running PlatformWeb.Endpoint with Cowboy using http://0.0.0.0:4000 16:02:47 - info: compiled 7 files into 2 files, copied 3 in 801 ms Elm compile: elm/Main.elm, to ../assets/js/main.js Displaying Our Elm Application Now that Brunch is automatically compiling our Elm code in Main.elm to JavaScript in main.js, we can find a place to display our front-end application within Phoenix. We’ll use the same index.html.eex page we worked with previously. Here’s what our existing application looks like with a user signed in. 109 Elm Setup Existing Phoenix Application Let’s replace everything below the header with our new Elm front-end. To do this, we’ll open the lib/platform_web/templates/page/index.html.eex and and replace everything with a single line: 1 <div id="elm-container"></div> We can embed our Elm application inside this container. Let’s open the assets/js/app.js file and add the following code at the bottom: 1 2 3 4 5 6 // Elm import Elm from "./main" const elmContainer = document.querySelector("#elm-container"); if (elmContainer) Elm.Main.embed(elmContainer); Working Elm application With our configuration finished, we now have the ability to write Elm code in our Phoenix application! The code in our Main.elm file is being automatically compiled to JavaScript using our minimal Brunch configuration, and then the resulting Elm application is inserted into our Phoenix application. 110 Elm Setup Working Elm Application Inside Phoenix Also note that we have our Elm debugger available to explore the history of changes, and this will be useful as we start tracking state changes in our application with the Elm Architecture. Live Reload One of the great features is that we can keep working with Elm, and the live reload feature will allow us to see changes without needing to restart the server or refresh the page in the browser. Try making a small change to the string in our Main.elm file: 1 2 3 4 5 6 7 8 module Main exposing (..) import Html exposing (Html, text) main : Html msg main = text "Hello from Elm inside Phoenix!" The content should be reloaded in the browser without needing a refresh (after waiting a second or two for our code to compile). 111 Elm Setup Working Live Reload for Elm Summary We’ve come a long way already in this book, and we now have the ability to write code in both Elixir and Elm to create our platform. In this chapter, we learned about configuring Elm to work inside Phoenix, and this will enable us to start building the front-end for our application. In the next chapter, we’ll start putting together our Elm front-end application so that we can start working with the JSON data from our API. Elm Application We have Elm up and running in our Phoenix application. We’ve also gotten a brief introduction to Elm syntax, but we could use some practice writing Elm code as we start to get an understanding of the concepts. Although some of these concepts might feel unfamiliar at first, let’s keep moving and we’ll increase our comfort level as we write small snippets of Elm code that we’ll incorporate into our platform. Getting Acquainted One of the best ways to get acquainted with something new is to start with something we’re already familiar with. If we already know how to write HTML code, we can use that knowledge as a bridge to learning Elm. Granted, Elm will afford us opportunities to accomplish so much more than what we could with HTML. But starting with the view will allow us to render something on the page and give us something tangible to work with. elm-format We mentioned this briefly in the Elm Introduction chapter, but it bears repeating that the elmformat47 tool is invaluable in this situation. It will really help cut down on initial mistakes and make Elm code easier to write. Main.elm Let’s take a look at our existing Elm application: 47 https://github.com/avh4/elm-format Elm Application 1 2 3 4 5 6 7 8 113 module Main exposing (..) import Html exposing (Html, text) main : Html msg main = text "Hello from Elm inside Phoenix!" Our goal for this chapter will be to show a list of games on the page, and later we’ll take care of pulling in real data from our JSON API. What would our game list look like in standard HTML? We could create a <div> element, and then add an unordered list that displays our games. Perhaps we’d start with something like this: 1 2 3 4 5 6 <div class="games-index"> <ul class="games-list"> <li>Platform Game</li> <li>Adventure Game</li> </ul> </div> Elm View Instead of displaying our "Hello from Elm inside Phoenix!" text, let’s create our games list in the main function. We’ll start with an empty div element: 1 2 3 4 5 6 7 8 module Main exposing (..) import Html exposing (Html, text, div) main : Html msg main = div [] [] Note that the first step is to import the div48 function from Elm’s Html49 module. Then, we replace our original text with div [] []. Those empty square brackets indicate that we’re passing two empty lists to the div function. The first one will be a list of attributes (like our class name), and the second will be the contents of our div (our unordered list). In order to use the class attribute, we’ll need to import that too: 48 49 http://package.elm-lang.org/packages/elm-lang/html/latest/Html#div http://package.elm-lang.org/packages/elm-lang/html/latest/Html Elm Application 1 2 3 4 5 6 7 8 9 114 module Main exposing (..) import Html exposing (Html, text, div) import Html.Attributes exposing (class) main : Html msg main = div [ class "games-index" ] [] In other words, we can import HTML elements from the Html module, and we can import HTML attributes from the Html.Attributes module. In fact, we can import all the functions available in these modules using .. and we won’t have to worry about manually importing things one at a time. On the one hand, we’re importing a lot of functions with this approach, but it will save us time while we’re in development mode and we can go back later and refactor to import only what we need. Let’s adjust our import declarations with the following: 1 2 3 4 5 6 7 8 9 module Main exposing (..) import Html exposing (..) import Html.Attributes exposing (..) main : Html msg main = div [ class "games-index" ] [] Now we have access to any HTML element or attribute that we’ll need. But we still only have an empty div on the page, so let’s start adding our list. Creating a List of Games Remember that we are passing two lists to our div function: 1 div [ class "games-index" ] [] The first list contains our attributes, and we’ll add the contents of our div in the second list (we’ll also move our second list to a new line for readability): Elm Application 1 2 115 div [ class "games-index" ] [ ul [] [] ] The ul function works the same way as our div. We’ll add our class attribute first: 1 2 div [ class "games-index" ] [ ul [ class "games-list" ] [] ] And now we can add the contents of our unordered list: 1 2 div [ class "games-index" ] [ ul [ class "games-list" ] [ li [] [] ] ] This is starting to look a little confusing because we have a list item nested inside an unordered list, which is nested inside our div. But let’s finish adding our current example by adding our list items: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 module Main exposing (..) import Html exposing (..) import Html.Attributes exposing (..) main : Html msg main = div [ class "games-index" ] [ ul [ class "games-list" ] [ li [] [ text "Platform Game" ] , li [] [ text "Adventure Game" ] ] ] This might seem like a lot of work to display a simple list on the page. But soon we’ll see how everything we’re working with here is a pure function, which makes it really simple to compose small, understandable pieces into a bigger picture. And the good news is that now we have our list displaying on the page: 116 Elm Application Initial Games List Breaking Up the View The nesting of elements inside elements is making things a little confusing in our code so far. Let’s break things up into small functions. Instead of assigning everything directly to our main function, let’s split things up into a games index container, a list, and individual list items. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 module Main exposing (..) import Html exposing (..) import Html.Attributes exposing (..) main : Html msg main = div [ class "games-index" ] [ ul [ class "games-list" ] [ li [] [ text "Platform Game" ] , li [] [ text "Adventure Game" ] ] ] gamesIndex = Elm Application 18 19 20 21 22 23 24 25 26 117 div [] [] gamesList = ul [] [] gamesListItem = li [] [] Now we can start to break up our main function into smaller parts. Each of these functions will return simple HTML, so we can add our type annotations: 1 2 3 4 5 6 7 8 9 10 11 12 13 gamesIndex : Html msg gamesIndex = div [] [] gamesList : Html msg gamesList = ul [] [] gamesListItem : Html msg gamesListItem = li [] [] And now we can fill out our functions with the existing example we created in our main function. Note that we’re only going to add a single list item for now, and then we’re going to extract our data into a separate list: 1 2 3 4 5 6 7 8 9 10 11 gamesIndex : Html msg gamesIndex = div [ class "games-index" ] [ gamesList ] gamesList : Html msg gamesList = ul [ class "games-list" ] [ gamesListItem ] gamesListItem : Html msg Elm Application 12 13 118 gamesListItem = li [] [ text "Platform Game" ] With our main function, we can create a div that will display a header and our gamesIndex to render our new structure to the page with a single game: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 module Main exposing (..) import Html exposing (..) import Html.Attributes exposing (..) main : Html msg main = div [] [ h1 [] [ text "Games" ] , gamesIndex ] gamesIndex : Html msg gamesIndex = div [ class "games-index" ] [ gamesList ] gamesList : Html msg gamesList = ul [ class "games-list" ] [ gamesListItem ] gamesListItem : Html msg gamesListItem = li [] [ text "Platform Game" ] 119 Elm Application Refactored Games List Extracting Our Data Currently, we’ve hard-coded our view data. But most Elm applications will separate the data into a model. I find it helpful to add comments and placeholder functions to my code so I can start to scaffold out where I want things to go. In this example, we’re going to start our model as an empty list: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module Main exposing (..) import Html exposing (..) import Html.Attributes exposing (..) -- MAIN main : Html msg main = div [] [ h1 [] [ text "Games" ] , gamesIndex ] Elm Application 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 120 -- MODEL model = [] -- VIEW gamesIndex : Html msg gamesIndex = div [ class "games-index" ] [ gamesList ] gamesList : Html msg gamesList = ul [ class "games-list" ] [ gamesListItem ] gamesListItem : Html msg gamesListItem = li [] [ text "Platform Game" ] In our initial example, we just want our data to be a list of game titles as strings. That means we can add our data and type annotation: 1 2 3 4 5 model model [ , ] : List String = "Platform Game" "Adventure Game" Having the comma characters at the beginning of the line might seem foreign if you’re coming from other languages, but it’s easy to get used to. And if you’re using elm-format then we’re able to focus less on syntax and more on the overall concepts we’re learning here. Passing Data to the View We’re going to work towards displaying our full list of games, but we’ll start with trying to display a single game from our list because it introduces some interesting concepts in Elm. Elm Application 121 What if we want to find the first game in our list, and then pass that to the gamesListItem function instead of using the hard-coded string we currently have there? Our first step would be to use the model list and find the first item. To do that, we’d head to the Elm documentation for the List module, and try to find a function that would give us the results we’re looking for. Take a look at the List50 module documentation and find the head function. The List.head function is exactly what we need to grab the first item from our model: 1 2 3 4 5 6 7 8 9 model model [ , ] : List String = "Platform Game" "Adventure Game" firstGame = List.head model You might think that firstGame would be set to "Platform Game", but in actuality we’re working with something called a “Maybe” in Elm. The List.head function doesn’t return the result itself, it returns a Maybe. The reason for this is that our list of data could be empty. To illustrate what’s happening, we can take a quick look in the interactive Elm REPL. From the command line, type elm-repl to get started: 1 $ elm-repl This will display an interactive prompt where we can type in some Elm code and view the output. Here’s an example where we’re accessing the head from a list that contains a couple of strings and another example where we’re trying to access the head from an empty list. 1 2 3 4 5 6 7 8 $ elm-repl ---- elm-repl 0.18.0 ----------------------------------------------------------:help for help, :exit to exit, more at <https://github.com/elm-lang/elm-repl> -------------------------------------------------------------------------------> List.head [ "Platform Game", "Adventure Game" ] Just "Platform Game" > List.head [] Nothing 50 http://package.elm-lang.org/packages/elm-lang/core/latest/List Elm Application 122 Elm Maybe This can be a really confusing concept, so don’t worry if this looks strange at first. The idea is that we need to handle both possible cases. The first case is when our list contains the data we’re looking for, and the second case is when our list is empty. The first step that will help clarify this is to rename the firstGame function to firstGameMaybe and add a type annotation: 1 2 3 4 5 6 7 8 9 10 model model [ , ] : List String = "Platform Game" "Adventure Game" firstGameMaybe : Maybe String firstGameMaybe = List.head model Now we can use Elm’s case syntax to get the data we’re looking for. When our list has an actual game title string, we’ll want to take that as a result. Otherwise, we’ll just return an empty string. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 model model [ , ] : List String = "Platform Game" "Adventure Game" firstGameMaybe : Maybe String firstGameMaybe = List.head model firstGameTitle : String firstGameTitle = case firstGameMaybe of Just gameTitle -> gameTitle Nothing -> "" Elm Application 123 If this is overwhelming or confusing, don’t worry too much. Sometimes it just takes repeated exposure to these concepts before they become obvious. The naming in our example should help with our understanding. We’re trying to get the first game title from our model. So we start by using List.head, which returns a Maybe type, and we assign that to firstGameMaybe. That could contain Just a game title or possibly contain Nothing in the event that the list was empty. We handle both of those cases in the firstGameTitle function. If the list contains strings like we’re expecting, we return the first string with gameTitle. If the list was empty, we just return an empty string with "". The result is that we can add this to our view and our application should still work as intended by showing the "Platform Game" as the only list item on the page: 1 2 3 gamesListItem : Html msg gamesListItem = li [] [ text firstGameTitle ] Maybe.withDefault Since this is a common pattern in Elm, there is a function called withDefault51 in the Maybe52 module that can be really helpful to gather the results we’re looking for. Let’s refactor our firstGameTitle function with the following approach: 1 2 3 firstGameTitle : String firstGameTitle = Maybe.withDefault "" firstGameMaybe This basically just grabs the first game title as a string, and if it doesn’t exist then we fall back to an empty string "" as a default value. This allows us to accomplish the same thing with a lot less code. Why Maybe? You might be asking yourself why we have to go through all this trouble to get a single value from our model. It seems like a lot of work at first, but this ultimately proves to be one of the most powerful concepts in Elm, because it insulates us from a massive class of errors. We never have to run into errors like “undefined is not a function” in Elm, because we always account for situations where something might not have a value. There is no usage of null or undefined in 51 52 http://package.elm-lang.org/packages/elm-lang/core/latest/Maybe#withDefault http://package.elm-lang.org/packages/elm-lang/core/latest/Maybe Elm Application 124 Elm. This is why Elm can make a claim of producing no runtime errors, because we’re always able to account for possible values. It means the users of our applications won’t run into errors, and enables us to make guarantees that many languages cannot. If you’re interested in these concepts, be sure to read more about the null reference on Wikipedia53 . This is one of the more difficult concepts to grasp at first when working with Elm, but it becomes one of our most treasured assets. Iterating Through the List We know how to display a single item from our model using List.head. But how do we iterate through all the game titles and display them on the page? We can use List.map54 to iterate through our values and pass them to our gamesList function. We can start by refactoring our view functions to accept arguments. This is going to be a little difficult at first since we have to make changes to gamesIndex, gamesList, and gamesListItem all at the same time. At the end, we’ll have all the data from our model being displayed in the view. First, let’s update our gamesIndex function to take our model as an argument, and we’ll call the argument gameTitles. This will involve updating our type annotation, adding the argument, and then passing it along to the next function too: 1 2 3 gamesIndex : List String -> Html msg gamesIndex gameTitles = div [ class "games-index" ] [ gamesList gameTitles ] Now, we have to update our gamesList function to accept the argument that we’re passing along from our gamesIndex function. We’ll update the type annotation, add the gameTitles argument, and then we’ll take a look at the map function from the List module: 1 2 3 gamesList : List String -> Html msg gamesList gameTitles = ul [ class "games-list" ] (List.map gamesListItem gameTitles) There’s a lot going on here. We’re accepting a list of strings as the argument to our gamesList function. Then, we’re going to iterate through those game titles one at a time using List.map. Take a look at the documentation for the List.map55 function, and we see that it takes two arguments: a function and a list. 53 https://en.wikipedia.org/wiki/Tony_Hoare http://package.elm-lang.org/packages/elm-lang/core/latest/List#map 55 http://package.elm-lang.org/packages/elm-lang/core/latest/List#map 54 Elm Application 125 We’re passing the list of all our game titles to the List.map function, and it’s splitting them apart and sending the titles one at a time to the gamesListItem function. We need to wrap all this in parentheses because we’re using it as the second argument to our ul function, but don’t get too caught up in understanding everything at once. For now, we just want to get an overall understanding of how to render the list on our page, and we’ll go back later to get a better explanation of how it all fits together. Finishing up with the gamesListItem function will help clarify what’s happening above. Let’s update the function to take a single string argument called gameTitle: 1 2 3 gamesListItem : String -> Html msg gamesListItem gameTitle = li [] [ text gameTitle ] Our gamesListItem function is using the individual strings that are getting passed from the List.map function in gamesList, and it’s rendering them inside a li element. Tying It All Together Let’s update our main function to accept the data from our model, and our application should be back to a fully functioning state. We’ll post the full code example here and then follow the data to get a better understanding of how it flows through our functions and how it all fits together (also note that we removed our firstGameMaybe and firstGameTitle functions now that we’re mapping through all the titles): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module Main exposing (..) import Html exposing (..) import Html.Attributes exposing (..) -- MAIN main : Html msg main = div [] [ h1 [] [ text "Games" ] , gamesIndex model ] Elm Application 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 -- MODEL model model [ , ] : List String = "Platform Game" "Adventure Game" -- VIEW gamesIndex : List String -> Html msg gamesIndex gameTitles = div [ class "games-index" ] [ gamesList gameTitles ] gamesList : List String -> Html msg gamesList gameTitles = ul [ class "games-list" ] (List.map gamesListItem gameTitles) gamesListItem : String -> Html msg gamesListItem gameTitle = li [] [ text gameTitle ] 126 127 Elm Application Games List with Model Data With functional programming, one of the best strategies for improving our understanding is to think in terms of data transformation. We’ll start with the data from our model, and we’ll follow it through our application as we render it onto the page. Our model function returns a list of strings. Each of these strings contains a game title that we’ll use when we display our list of games. In our main function, we pass the data from our model to our view by passing model to gamesIndex. The gamesIndex function gives us a div element as a container for our list of games, but it’s essentially just passing the data through to our gamesList function without performing any alteration to the data. This might seem unnecessary, but we’ll see the value of this approach later as we start building other views for our player index. And it currently enables us to keep our functions small and easy to understand. The gamesList function is where most of the work is getting done. We’re using List.map to iterate through our list of strings. We take in the full list of gameTitles, and we pass them one at a time to the gamesListItem function. This function allows us to render our unordered list, and then pipe the data through to our individual list items. Lastly, the individual strings with game titles are passed to our gamesListItem function. This is where we see how our approach is so much better than our original hard-coded attempt, because our list of games can grow without us having to do any extra work. The number of li elements rendered will just match the number of game title strings that are stored in our model. Elm Application 128 Summary In this chapter, we managed to start what will ultimately become the whole front-end for our platform. We learned how to separate the data model and view for our Elm application, and made things easier to understand as we broke our code up into small functions. We also got a quick introduction to the important concept of Maybe in Elm, and learned how to iterate through data with the List module. Since Elm applications tend to share a standard structure and follow similar patterns, the Elm community has adopted the Elm Architecture. We’ve already seen some of the concepts at play when we separated our code into separate sections for the model and the view, and then pulled them together with the main function. In the next chapter, we’ll take a deeper look at the Elm Architecture as we work towards displaying real game data from our Phoenix JSON API. Elm Architecture In this chapter, we’re going to adapt our existing Elm application so that it follows the standard Elm Architecture. While Elm is a programming language like JavaScript, we can think of the Elm Architecture as a framework for building applications like React or Angular. The Elm Architecture gives us a standard approach for how to structure our Elm applications, and makes it really easy to add our initial features. Adapting Our Existing Elm Application The good news is that our existing Elm application already has the elements we need for the Elm Architecture. We’re just going to adapt the existing code and add a few things that will tie it all together. One way to think of our Elm application is to break it into five sections: • • • • • Main Model Update Subscriptions View Each of these sections will be a function, and the Elm runtime will make everything work by tying them together in the main function. • • • • • Main: main Model: init Update: update Subscriptions: subscriptions View: view Removing the Original Main Function We’re going to be breaking our original Elm application with all the changes we make in this chapter. But we don’t want elm-format to keep warning us of errors while we’re working. Let’s comment out our original main function so we can keep working and using elm-format without it trying to build our full application for us: Elm Architecture 1 2 3 4 5 6 7 130 -- MAIN -- main : Html msg -- main = -div [] -[ h1 [] [ text "Games" ] -, gamesIndex model -] At the end of this chapter, we’ll create a new version of the main function that will pull everything together for us. Starting with the Model The model will be a good place to start as we structure our data. Instead of just displaying our game titles, let’s show both the game title and a brief description. We’re going to be mirroring some of the data we created in our Phoenix JSON API for games. We’ll name this function initialModel, and we’ll use Elm’s “Record” syntax to create our games: 1 2 3 4 5 initialModel = [ { gameTitle = "Platform Game", gameDescription = "Platform game example." } , { gameTitle = "Adventure Game", gameDescription = "Adventure game example.\ " } ] Note that we haven’t added type annotations yet, and we’re working with a new data type. Records allow us to create keys and values for our fields. In this example, we have two records (each one is a game), and each record has two fields (gameTitle and gameDescription). Now that we have some idea of how our data will be structured, let’s add a couple of type annotations for our individual games and our model as a whole: 1 2 3 4 5 6 7 8 9 10 type alias Model = List Game type alias Game = { gameTitle : String , gameDescription : String } Elm Architecture 11 12 13 14 15 16 131 initialModel : Model initialModel = [ { gameTitle = "Platform Game", gameDescription = "Platform game example." } , { gameTitle = "Adventure Game", gameDescription = "Adventure game example.\ " } ] This is our first look at creating type aliases. We’re creating one type alias for our overall Model, which is a list of games. Then, we define the type of our Game records. Each game is a record that will contain a gameTitle field and a gameDescription field, both of which are String types. We also add a type annotation for our initialModel function to ensure that it follows the Model type alias structure that we created above. Let’s make a slight change to help further our understanding of what we’re accomplishing here. We’ll make our initialModel more flexible for adding other fields later by moving our records into a named list: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type alias Model = { gamesList : List Game } type alias Game = { gameTitle : String , gameDescription : String } initialModel : Model initialModel = { gamesList = [ { gameTitle = "Platform Game", gameDescription = "Platform game exampl\ e." } , { gameTitle = "Adventure Game", gameDescription = "Adventure game exam\ ple." } ] } This is essentially the same data we were already working with. We’re just turning our Model into a record so that we can easily add other fields later (for example, we’ll eventually want to add a playersList). Elm Architecture 132 Now we have types that hold data for both our list of games and the individual games themselves. This enables us to ensure that our data is always properly structured and that we’re not producing any errors. Since we commented out our main function, we can also remove our original model function. Here’s what the top of our file should look like for reference: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 module Main exposing (..) import Html exposing (..) import Html.Attributes exposing (..) --------- MAIN main : Html msg main = div [] [ h1 [] [ text "Games" ] , gamesIndex model ] MODEL type alias Model = { gamesList : List Game } type alias Game = { gameTitle : String , gameDescription : String } initialModel : Model initialModel = { gamesList = [ { gameTitle = "Platform Game", gameDescription = "Platform game exampl\ e." } , { gameTitle = "Adventure Game", gameDescription = "Adventure game exam\ ple." } ] } Elm Architecture 133 When we add our new main function later, we’ll want to initialize our model into an init function. But for now let’s move on to the update section so we can introduce the necessary concepts as they arise. Update We’ve seen the model section where we deal with data, and we worked with the view in the last chapter. But how do we update the state of our Elm applications? Let’s create a new comment to delineate the update section, and add a new update function: 1 2 3 4 5 -- UPDATE update msg model = model Our update function takes two arguments. First, it takes a msg that will perform some action or operation in our code. Second, it takes the existing model as an argument. This might sound complicated at first, but it basically just means that the update function takes in our existing model, performs some change to it, and then returns a new version of the model. In other words, we can take some data like our list of games, update it, and then return the updated version of that data. Let’s create the type annotation to clarify what’s happening in the update function: 1 2 3 update : Msg -> Model -> Model update msg model = model The type annotation helps a little because we can see that we take our existing model as the second argument, and that the return type will also be of type Model. But what is Msg? And how do we apply the changes we want to make to our data? Update Messages In our update section, we want to define the different type of actions our users can perform. We do this by creating a new type for our messages above the update function: 1 type Msg = DisplayGamesList | HideGamesList Elm Architecture 134 In this example, we’re going to allow our users to perform two different types of transformations to our data. We’re either going to display the list of games on the page, or we’re going to hide it from view. This feature will start to be a little more obvious when we get to the view for our application. We’ll create a couple of buttons that will allow us to display and hide our list of games. For now, all we need to know is that there are only two actions that a user can perform in our application: DisplayGamesList and HideGamesList. Before we update our model and perform our actions, let’s create a case expression that will allow us to handle our different messages (note that we’re actually just returning the same model that’s being passed in for now, and we’ll perform the actual updates soon): 1 2 3 4 5 6 7 8 9 10 11 12 13 type Msg = DisplayGamesList | HideGamesList update : Msg -> Model -> Model update msg model = case msg of DisplayGamesList -> model HideGamesList -> model We have two types of actions that our users can perform, and we’ll use a case expression to determine which changes get made to our data as a result. Changing the Model Any time we change state in our Elm applications, it will involve an update to the model section. In our case, we want to add a field that indicates whether or not we’re going to display our list of game data. Let’s add a displayGamesList field that will be set to either True or False. We’ll update the type alias for our Model and also set an initial value of False in our initialModel: Elm Architecture 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 135 type alias Model = { gamesList : List Game , displayGamesList : Bool } type alias Game = { gameTitle : String , gameDescription : String } initialModel : Model initialModel = { gamesList = [ { gameTitle = "Platform Game", gameDescription = "Platform game exampl\ e." } , { gameTitle = "Adventure Game", gameDescription = "Adventure game exam\ ple." } ] , displayGamesList = False } Performing the Update When a user performs an action and invokes the DisplayGamesList message, we’ll set the displayGamesList field to a value of True. Alternatively, when a user performs an action and invokes the HideGamesList message, we’ll set the displayGamesList field to a value of False. Here is the syntax we use in Elm to update a value stored in a record: 1 2 3 4 5 6 7 8 update : Msg -> Model -> Model update msg model = case msg of DisplayGamesList -> { model | displayGamesList = True } HideGamesList -> { model | displayGamesList = False } Since our model is a record that contains fields, we can use { model | displayGamesList = True } to change a value. This syntax is basically telling our application to look inside the model record, Elm Architecture 136 find the displayGamesList field, and set it to a new value of True. In the upcoming sections, we’ll add a button to our view that will allow users to invoke the DisplayGamesList message, which will set displayGamesList to True, and then we’ll use that value to display the list of game data in our view. Subscriptions Subscriptions in Elm allow us to subscribe to a stream of events (like the user’s mouse activity or keyboard input). We currently need to create a subscriptions function so we can pull our full application together when we get to the main function. But we’re not going to subscribe to any events yet, so we’re just going to set it to an initial value of Sub.none. Here’s the initial subscriptions function we’ll use, and we’ll cover the concept of subscriptions in greater detail later in the book: 1 2 3 4 5 6 -- SUBSCRIPTIONS subscriptions : Model -> Sub Msg subscriptions model = Sub.none Updating the View Finally, we can update our existing view section to include our new features and display our list of games. Let’s start by adding a new view function that takes our model as an argument and then renders HTML to the page: 1 2 3 4 5 6 -- VIEW view : Model -> Html Msg view model = div [] [] We start with an empty div element, and now we can add two buttons that will conditionally display or hide our list of games. Elm Architecture 1 2 3 4 5 6 7 137 view : Model -> Html Msg view model = div [] [ h1 [ class "games-section" ] [ text "Games" ] , button [ class "btn btn-success" ] [ text "Display Games List" ] , button [ class "btn btn-danger" ] [ text "Hide Games List" ] ] These button elements won’t be fully functioning yet, but the good news is that we finally have enough code to start displaying our application in the browser. Let’s add our main function, and we’ll come back to the view so we can get all our features working. The Main Function Let’s go back to the top of our Main.elm file and add the following main function back in just below the import declarations. This code is how the Elm runtime pulls all the different functions we’ve created together: 1 2 3 4 5 6 7 8 main : Program Never Model Msg main = Html.program { init = init , view = view , update = update , subscriptions = subscriptions } Adding our main function means we’ll need to initialize our model with an init function. Add the following below the initialModel function: 1 2 3 init : ( Model, Cmd Msg ) init = ( initialModel, Cmd.none ) This code tells Elm that we want to start with the initialModel as our data. We don’t need to worry too much about Cmd yet, but it will basically allow us to run a “Command”56 in Elm to fetch additional data. We’re going to cover Elm commands in detail in the next chapter, but for now we can just think of a command as an action that may succeed or fail. For instance, we could perform an HTTP request when we start our Elm application to gather initial data, and we’d have to handle the situations where that HTTP fetch was successful and situations where that might fail to fetch the data. With these changes to our model, we’ll have to change our update function too: 56 http://package.elm-lang.org/packages/elm-lang/core/latest/Platform-Cmd 138 Elm Architecture 1 2 3 4 5 6 7 8 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of DisplayGamesList -> ( { model | displayGamesList = True }, Cmd.none ) HideGamesList -> ( { model | displayGamesList = False }, Cmd.none ) At this point, our main function is pulling everything together and we’re finally able to see our application rendered to the screen in the browser. Our buttons aren’t set up to work yet, but we’re now able to see them rendering on the screen. Working Application Using Elm Architecture Displaying Our List of Games Let’s finish updating our view section. We’ll update our existing functions to display our list of games, and we’ll use our buttons to add some interactivity. First, we’ll update our gamesIndex function to take our model as an argument and then access the list of games with model.gamesList: Elm Architecture 1 2 3 4 5 6 7 8 9 10 11 12 139 view : Model -> Html Msg view model = div [] [ h1 [ class "games-section" ] [ text "Games" ] , button [ class "btn btn-success" ] [ text "Display Games List" ] , button [ class "btn btn-danger" ] [ text "Hide Games List" ] , gamesIndex model ] gamesIndex : Model -> Html msg gamesIndex model = div [ class "games-index" ] [ gamesList model.gamesList ] Now we can update our gamesList and gamesListItem functions: 1 2 3 4 5 6 7 8 9 10 11 gamesList : List Game -> Html msg gamesList games = ul [ class "games-list" ] (List.map gamesListItem games) gamesListItem : Game -> Html msg gamesListItem game = li [ class "game-item" ] [ strong [] [ text game.gameTitle ] , p [] [ text game.gameDescription ] ] We start by passing our full model to the gamesIndex, which passes the gamesList from the model along to the gamesList function. Then we pass those individual games to the gamesListItem function. For each game, we display both the gameTitle (in a strong element) and the gameDescription (in a p element). 140 Elm Architecture Displaying the List of Games We’re now able to render our buttons and the list of games, but how do we get the buttons to work? Handling Events To get our buttons working, we’ll need to add another import at the top of our Main.elm file. We already have our HTML elements and attributes, and we’ll need to import Html.Events to handle events. More specifically, we just want to import the onClick function for our buttons: 1 2 3 import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) Now we can update our view function to handle the click events on our buttons and send them to the right actions in the update function: Elm Architecture 1 2 3 4 5 6 7 8 9 10 141 view : Model -> Html Msg view model = div [] [ h1 [ class "games-section" ] [ text "Games" ] , button [ class "btn btn-success", onClick DisplayGamesList ] [ text "D\ isplay Games List" ] , button [ class "btn btn-danger", onClick HideGamesList ] [ text "Hide \ Games List" ] , gamesIndex model ] The last step to getting our buttons working is to check for the value of the displayGamesList boolean before we render our list of games. When that field is set to True, then we’ll pass the model to the gamesIndex, otherwise we’ll just render an empty div element. Here’s the full View section for our application: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 -- VIEW view : Model -> Html Msg view model = div [] [ h1 [ class "games-section" ] [ text "Games" ] , button [ class "btn btn-success", onClick DisplayGamesList ] [ text "D\ isplay Games List" ] , button [ class "btn btn-danger", onClick HideGamesList ] [ text "Hide \ Games List" ] , if model.displayGamesList then gamesIndex model else div [] [] ] gamesIndex : Model -> Html msg gamesIndex model = div [ class "games-index" ] [ gamesList model.gamesList ] gamesList : List Game -> Html msg gamesList games = ul [ class "games-list" ] (List.map gamesListItem games) 142 Elm Architecture 27 28 29 30 31 32 33 34 gamesListItem : Game -> Html msg gamesListItem game = li [ class "game-item" ] [ strong [] [ text game.gameTitle ] , p [] [ text game.gameDescription ] ] We should be able to click the buttons and see the list of games displayed or hidden based on our interactions: Working Buttons to Display and Hide the List of Games Now that we’re changing state in our application, we can also start using the Elm debugger by clicking the Explore History tab at the bottom right corner of the browser to see changes to our model as they occur over time. Summary We managed to get a fairly detailed look at the Elm Architecture in this chapter as we adapted our original application. Most Elm applications tend to follow this structure, so we can use this knowledge to quickly get a sense how other Elm applications work too. Elm Architecture 143 In the next chapter, we’ll work on fetching JSON data from our Phoenix API so we can render both our games and our players to the page. Elm and API Data Now that we have our Elm front-end application up and running, let’s start integrating it with our Phoenix application data. To start with, we can focus on reading the game data from our API and rendering it on the page. Where Were We? We currently have some hard-coded data in our Elm application that we’re using to display a list of games. 1 2 3 4 5 6 7 8 9 10 initialModel : Model initialModel = { gamesList = [ { gameTitle = "Platform Game", gameDescription = "Platform game exampl\ e." } , { gameTitle = "Adventure Game", gameDescription = "Adventure game exam\ ple." } ] , displayGamesList = False } 145 Elm and API Data Hard-coded Games List Our next goal is to remove this hard-coded data and fetch the game JSON data from our Phoenix API instead. 146 Elm and API Data JSON API Game Data Updating Our Initial Model Let’s start by clearing out the sample data we had been using to display our list of games. We can also remove our buttons that display and hide the games since we’re going to show them by default. So we’re removing the displayGamesList field, and we’re also going to add a new function called initialCommand that we’re going to use soon to fetch our data from the API. Let’s also go ahead and simplify our Game type by changing the field names to title and description, which will help the fields match to the same names that we have in our API. Here is the full updated code for the model section: Elm and API Data 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 147 type alias Model = { gamesList : List Game } type alias Game = { title : String , description : String } initialModel : Model initialModel = { gamesList = [] } initialCommand : Cmd Msg initialCommand = Cmd.none init : ( Model, Cmd Msg ) init = ( initialModel, initialCommand ) Changing the Update We’re also going to remove our DisplayGamesList and HideGamesList from the update section and add a new action called FetchGamesList. For now, we’ll just set the gamesList to an empty list with { model | gamesList = [] }, but we’ll assign the real data soon. 1 2 3 4 5 6 7 8 9 type Msg = FetchGamesList update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of FetchGamesList -> ( { model | gamesList = [] }, Cmd.none ) Elm and API Data 148 Changing the View Lastly, we’ll need to update our view section. We can remove the buttons we were using to display and hide the list of games. And we’ll add a condition to make sure our gamesList is not empty before we render the games section on our page. Here is the updated code for the view: 1 2 3 4 5 6 7 8 9 view : Model -> Html Msg view model = if List.isEmpty model.gamesList then div [] [] else div [] [ h1 [ class "games-section" ] [ text "Games" ] , gamesIndex model ] We’ll also need to make a slight change to our gamesListItem function since we changed the field names in our Game type. 1 2 3 4 5 6 gamesListItem : Game -> Html msg gamesListItem game = li [ class "game-item" ] [ strong [] [ text game.title ] , p [] [ text game.description ] ] Importing Packages Our goal for the rest of the chapter is going to be to make an HTTP request to our Phoenix back-end for our JSON game data (http://0.0.0.0:4000/api/games), and then to decode that JSON into our Elm application. To get started, let’s import the libraries we’ll need to use: • Http57 • Json.Decode58 From the command line inside the assets folder where our elm-package.json file lives, type the following command: 57 58 http://package.elm-lang.org/packages/elm-lang/http/latest http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode Elm and API Data 1 149 $ elm-package install elm-lang/http This should install the Http package for us, and here’s what the output should look like: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ elm-package install elm-lang/http To install elm-lang/http I would like to add the following dependency to elm-package.json: "elm-lang/http": "1.0.0 <= v < 2.0.0" May I add that to elm-package.json for you? [Y/n] Y Some new packages are needed. Here is the upgrade plan. Install: elm-lang/http 1.0.0 Do you approve of this plan? [Y/n] Y Starting downloads... � elm-lang/http 1.0.0 Packages configured successfully! With that completed, we can now add the import declarations to the top of our Main.elm file. Here’s what the top of the file should look like: 1 2 3 4 5 6 7 module Main exposing (..) import import import import import Html exposing (..) Html.Attributes exposing (..) Html.Events exposing (onClick) Http Json.Decode as Decode Note that we’re importing our new Http library to make the HTTP request to our endpoint, and we’re also importing the Json.Decode library from the core and giving it an alias of Decode to make things a little easier on ourselves. Elm and API Data 150 Fetching Games Now that we have our libraries imported, the first step we need to take is to make an HTTP GET request to our endpoint. If we take a look at the documentation for Http.get59 , we see that this function takes a string value for the URL as the first argument and a JSON decoder as the second argument. Keep in mind that the order of your functions doesn’t matter, but I like to add a new section for API functions below the model section. Here we’ll create a fetchGamesList function that make our Http.get request to our "/api/games" route (and we’ll get to the decoding soon). 1 2 fetchGamesList = Http.get "/api/games" decodeGamesList We also want to trigger this request using the Http.send60 function. So we’ll pipe the results along with a new action we’re going to create. There’s a lot going on here, so don’t worry if it’s getting a little bit confusing. It will all make more sense as we finish up with this initial feature and then we’ll do something similar for fetching our player data, which will give us another opportunity to improve our understanding. JSON Decoding Let’s take another look at the structure of the JSON data we’re getting from our endpoint. 1 2 3 4 5 6 7 8 9 10 11 { data: [ { title: "Platformer", thumbnail: "http://via.placeholder.com/300x200", id: 1, featured: true, description: "Platform game example." } ] } It looks like a JSON object that contains a data list, which contains the game data that we’re actually looking for. 59 60 http://package.elm-lang.org/packages/elm-lang/http/latest/Http#get http://package.elm-lang.org/packages/elm-lang/http/latest/Http#send Elm and API Data 151 It’s useful to think of decoding JSON from the inside out. This allows us to focus on decoding the values we want to pull into our application, and then take care of the details afterward. The fields that we’re looking to pull from our API are the title and description. Let’s create a decodeGame function, and we’ll use the map261 function since we’re only looking to decode two fields. Note that we’re also using the Json.Decode package’s field62 function that allows us to safely ensure that the fields will contain the correct type of data we’re looking for (strings in our case). 1 2 3 4 5 decodeGame : Decode.Decoder Game decodeGame = Decode.map2 Game (Decode.field "title" Decode.string) (Decode.field "description" Decode.string) Next, we can pipe the results of our decoded game to the list63 function and the at64 function to finish decoding the full list of games in our API’s data list. 1 2 3 4 5 decodeGamesList : Decode.Decoder (List Game) decodeGamesList = decodeGame |> Decode.list |> Decode.at [ "data" ] Here’s what all the functions should look like together to make the HTTP request to our JSON endpoint and decode the response: 1 2 3 4 5 6 7 8 9 10 11 fetchGamesList : Cmd Msg fetchGamesList = Http.get "/api/games" decodeGamesList |> Http.send FetchGamesList decodeGamesList : Decode.Decoder (List Game) decodeGamesList = decodeGame |> Decode.list |> Decode.at [ "data" ] 61 http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#map2 http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#field 63 http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#list 64 http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#at 62 152 Elm and API Data 12 13 14 15 16 17 18 decodeGame : Decode.Decoder Game decodeGame = Decode.map2 Game (Decode.field "title" Decode.string) (Decode.field "description" Decode.string) FetchGamesList Now we can handle the results of our HTTP request in our update section. The type annotation may look a bit strange at first, but essentially we’re creating our FetchGamesList action and the result will either be successful or an error. When the API fetch is successful, we’ll get back our list of games. When it’s not successful, we’ll get back and Http.Error that we can ignore for now. 1 2 type Msg = FetchGamesList (Result Http.Error (List Game)) Inside the update function, we’ll add a result argument to our FetchGamesList action. And we’ll add a case expression to handle the result. When we get an Ok response, we update the gamesList in our model to contain the list of games from our API. Otherwise, we’ll just leave the model unchanged if we get back and error. 1 2 3 4 5 6 7 8 9 10 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of FetchGamesList result -> case result of Ok games -> ( { model | gamesList = games }, Cmd.none ) Err _ -> ( model, Cmd.none ) Performing the Fetch We’ve managed to make a ton of progress. We have all the functions we need to create our HTTP request, decode the response, and render the results. All that’s left is to add our fetchGamesList as the initialCommand we run when our application is initialized. 153 Elm and API Data 1 2 3 initialCommand : Cmd Msg initialCommand = fetchGamesList When the Elm runtime initializes our application, it takes our initialModel (which contains an empty gamesList) and performs the initialCommand (which will set our gamesList to the list of games from the API). The result is that we’re now fetching our data successfully from the API and rendering it in our Elm application! Successful API Fetch Decoding the Remaining Game Data Now that we’ve successfully rendered our list of games, let’s finish rendering the rest of the game data from our API. We won’t need to show all this data on the page, but it’s a good idea to have the data synced up between our Phoenix API and our Elm application. We can start by updating our Game type with the following: Elm and API Data 1 2 3 4 5 6 7 154 type alias Game = { description : String , featured : Bool , id : Int , thumbnail : String , title : String } Now we’ll need to update our decoder so that we’re properly decoding all the fields with the correct types. The order of fields is important here and has to match up with our type alias. I tend to keep fields in alphabetical order, but keep in mind that the order will need to match up so that the fields are decoded in accordance with the type alias. 1 2 3 4 5 6 7 8 decodeGame : Decode.Decoder Game decodeGame = Decode.map5 Game (Decode.field "description" Decode.string) (Decode.field "featured" Decode.bool) (Decode.field "id" Decode.int) (Decode.field "thumbnail" Decode.string) (Decode.field "title" Decode.string) That should be enough for now. Notice that we haven’t changed our view functions, so we’re not actually rendering the additional data on the page. But now we have all the data we need, so we can selectively choose the fields we want to display as needed. Rendering Our List of Players We managed to get our list of games from the API. Let’s get some additional experience as we do the same for our list of players. 155 Elm and API Data JSON API Player Data We can start by adding to our Model type alias and initialModel: 1 2 3 4 5 6 7 8 9 10 type alias Model = { gamesList : List Game , playersList : List Player } initialModel : Model initialModel = { gamesList = [] , playersList = [] } Then, we’ll add a new Player type alias with all the fields we want from our API: Elm and API Data 1 2 3 4 5 6 156 type alias Player = { displayName : String , id : Int , score : Int , username : String } You might have noticed a slight discrepancy here. Elixir and Phoenix tend to use underscores in field names (display_name) by convention, and Elm tends to use camel case in field names (displayName). I like to stick to these conventions, and as long as we’re consistent everything will still work as intended. Now we can add functions for making the HTTP request to the "/api/players" endpoint and decoding the JSON response. Essentially, we have the same functions that we created before with some slight alterations to work with players instead of games. These functions work the same way, but we’re going to decode the player fields so we can render those in our Elm application. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fetchPlayersList : Cmd Msg fetchPlayersList = Http.get "/api/players" decodePlayersList |> Http.send FetchPlayersList decodePlayersList : Decode.Decoder (List Player) decodePlayersList = decodePlayer |> Decode.list |> Decode.at [ "data" ] decodePlayer : Decode.Decoder Player decodePlayer = Decode.map4 Player (Decode.field "display_name" Decode.string) (Decode.field "id" Decode.int) (Decode.field "score" Decode.int) (Decode.field "username" Decode.string) For our update section, we’ll add a new FetchPlayersList action, and take the same approach we did for our games. We add our type first, and then we fill out the case expression inside the update function with the following: 157 Elm and API Data 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Msg = FetchGamesList (Result Http.Error (List Game)) | FetchPlayersList (Result Http.Error (List Player)) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of FetchGamesList result -> case result of Ok games -> ( { model | gamesList = games }, Cmd.none ) Err _ -> ( model, Cmd.none ) FetchPlayersList result -> case result of Ok players -> ( { model | playersList = players }, Cmd.none ) Err _ -> ( model, Cmd.none ) Refactoring Our View Our Elm application is currently in working order without any compiler errors. But let’s refactor our view function a little bit so that we can render both our games and our list of players. We can start by simplifying our view function to simply render our gamesIndex and playersIndex, and we’ll offload the conditional to check whether or not those lists are empty to the index functions. 1 2 3 4 5 6 view : Model -> Html Msg view model = div [] [ gamesIndex model , playersIndex model ] Next, we can update our gamesIndex function and add a new playersIndex function. Elm and API Data 1 2 3 4 5 6 7 8 9 158 playersIndex : Model -> Html msg playersIndex model = if List.isEmpty model.playersList then div [] [] else div [ class "players-index" ] [ h1 [ class "players-section" ] [ text "Players" ] , playersList model.playersList ] Then, we can go ahead and take the same approach we did for our games by adding two new functions for the playersList and playersListItem. For now, we’re going to use the player’s displayName and score fields to render the data on the page: 1 2 3 4 5 6 7 8 9 10 11 playersList : List Player -> Html msg playersList players = ul [ class "players-list" ] (List.map playersListItem players) playersListItem : Player -> Html msg playersListItem player = li [ class "player-item" ] [ strong [] [ text player.displayName ] , p [] [ text (toString player.score) ] ] Performing Another Fetch Lastly, don’t forget that we need to perform the fetch for our player data when we load the page. Let’s go back to our initialCommand function, and we’ll use Cmd.batch that batches a list of commands to run. This way, we’ll run both fetchGamesList and fetchPlayersList when the page loads and we’ll have all the data we need. 1 2 3 4 5 6 initialCommand : Cmd Msg initialCommand = Cmd.batch [ fetchGamesList , fetchPlayersList ] 159 Elm and API Data We’ve successfully loaded the API data for both our players and our games! Here’s what our Elm application should look like in the browser: Rendering Games and Players Sorting Results Our application isn’t the fanciest thing to look at yet, but we’ll take a look at some layout and styling improvements in the next chapter. Before we move on, let’s figure out how to sort our players by their score data. Sorting a list is simple enough, so let’s take this opportunity to talk about a common refactoring approach you can use for your Elm programs. This is what our playersIndex function currently looks like: Elm and API Data 1 2 3 4 5 6 7 8 9 160 playersIndex : Model -> Html msg playersIndex model = if List.isEmpty model.playersList then div [] [] else div [ class "players-index" ] [ h1 [ class "players-section" ] [ text "Players" ] , playersList model.playersList ] We’re passing the list of players from our model to the playersList function at the bottom. Our first approach to sorting our players will be to use the List.sortBy65 function and pass the .score field. If we look at the documentation, we see that the sorting is from lowest to highest. So we’ll also have to pipe our data to the List.reverse function as well. This works pretty well: 1 2 3 4 5 6 7 8 9 10 11 12 playersIndex : Model -> Html msg playersIndex model = if List.isEmpty model.playersList then div [] [] else div [ class "players-index" ] [ h1 [ class "players-section" ] [ text "Players" ] , model.playersList |> List.sortBy .score |> List.reverse |> playersList ] Our playersIndex function is already getting a little difficult to read through at a quick glance. One thing you can do to clarify what’s happening in your functions is to extract some functionality into a let expression and give it a clear name. Here’s how we can take our same code and refactor it into a let expression: 65 http://package.elm-lang.org/packages/elm-lang/core/latest/List#sortBy Elm and API Data 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 161 playersIndex : Model -> Html msg playersIndex model = let playersSortedByScore = model.playersList |> List.sortBy .score |> List.reverse in if List.isEmpty model.playersList then div [] [] else div [ class "players-index" ] [ h1 [ class "players-section" ] [ text "Players" ] , playersList playersSortedByScore ] This is a little more readable in the sense that the playersSortedByScore name is obvious. Instead of using a let expression, we could extract this into a separate function altogether. With this code, we’re still passing data to the playersList view function. But we’re passing the player data from our model through the playersSortedByScore function first and then piping that backwards to playersList. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 playersIndex : Model -> Html msg playersIndex model = if List.isEmpty model.playersList then div [] [] else div [ class "players-index" ] [ h1 [ class "players-section" ] [ text "Players" ] , playersList <| playersSortedByScore model.playersList ] playersSortedByScore : List Player -> List Player playersSortedByScore players = players |> List.sortBy .score |> List.reverse You can decide for yourself which approach you think is preferable. But this is a good example of how simple it is to refactor with Elm when we have type safety. And it’s a common approach to 162 Elm and API Data extract small pieces of functionality into separate functions to keep things simple and easy to work with. Either way, we managed to successfully sort our list of players and show the top scorer at the top: Sorted List of Players Handling Errors If a problem arises when we fetch data from our API (perhaps due to inconsistent JSON data), we’re currently suppressing errors with the approach we took. Let’s make sure we have a way to view errors when something goes wrong with our API fetch functions. The first step will be to add a new errors field to our existing Model: Elm and API Data 1 2 3 4 5 163 type alias Model = { gamesList : List Game , playersList : List Player , errors : String } Then, we can set a default value by setting errors to an empty string: 1 2 3 4 5 6 initialModel : Model initialModel = { gamesList = [] , playersList = [] , errors = "" } Inside our update function, we’ll need to make a few changes so that we’re not ignoring errors in our FetchGamesList and FetchPlayersList cases. Let’s take a look at what our error case looks like so far: 1 2 Err _ -> ( model, Cmd.none ) This means that any time we get any error from our HTTP request, we’re just ignoring it and not making any changes to our model. Instead of ignoring these errors, we’ll convert the error message to a string and make it viewable in the new errors field we added to our model. 1 2 Err message -> ( { model | errors = toString message }, Cmd.none ) We’ll need to make these changes in both the FetchGamesList and FetchPlayersList cases. Here’s the full update function for reference: 164 Elm and API Data 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of FetchGamesList result -> case result of Ok games -> ( { model | gamesList = games }, Cmd.none ) Err message -> ( { model | errors = toString message }, Cmd.none ) FetchPlayersList result -> case result of Ok players -> ( { model | playersList = players }, Cmd.none ) Err message -> ( { model | errors = toString message }, Cmd.none ) Handling Potentially Missing Data Before we finish up with this chapter, let’s make sure we’re handling our data gracefully when dealing with values that may or may not be there. For example, we know that our players and games will all have id fields because Phoenix creates those fields for us and they won’t ever have a nil value (or null value in the JSON response). The players on our platform are able to create accounts with a required username field, but they may or may not have a display_name field. In other words, when we decode our player data from the API, we’ll need to consider the possibility that not all users will have a display_name string. Let’s start by taking a look at our Player type alias: 1 2 3 4 5 6 type alias Player = { displayName : String , id : Int , score : Int , username : String } Instead of assuming that displayName is a String, we’ll use Maybe to indicate that the value could be a String or a Nothing. Elm and API Data 1 2 3 4 5 6 165 type alias Player = { displayName : Maybe String , id : Int , score : Int , username : String } Now, we just need to account for this in our decodePlayer function. Instead of just using the Decode.field function for the "display_name" field in our JSON API data, let’s use the Decode.maybe function. 1 2 3 4 5 6 7 decodePlayer : Decode.Decoder Player decodePlayer = Decode.map4 Player (Decode.maybe (Decode.field "display_name" Decode.string)) (Decode.field "id" Decode.int) (Decode.field "score" Decode.int) (Decode.field "username" Decode.string) This allows us to extract the string value from the API if it’s there. If the API is giving us a null value, we’ll know that it’s set to Nothing in our Elm application and we can handle it accordingly. We’ll add a let expression to our playersListItem function to account for the different conditions. If the player’s displayName is set to Nothing, then we’ll fall back to displaying the player’s username. If we’re successful in fetching a player’s displayName, then that’s what we’ll display in our list of players. Here’s the updated playersListItem function: 1 2 3 4 5 6 7 8 9 10 11 12 13 playersListItem : Player -> Html msg playersListItem player = let displayName = if player.displayName == Nothing then player.username else Maybe.withDefault "" player.displayName in li [ class "player-item" ] [ strong [] [ text displayName ] , p [] [ text (toString player.score) ] ] Elm and API Data 166 Summary We came a long way in this chapter! We now have a working Phoenix API back-end, and we’re successfully decoding the JSON API data into our Elm application. In the next chapter, let’s take a brief jaunt into improving the layout and design so that our application is a little nicer to look at and use. We won’t need to create something fancy since this isn’t a book about design, but we’ll try to make things nicer to work with and get a sense of how styles work within the Phoenix framework. Layout and Design Although this isn’t a book about design, our application could certainly benefit from an effort to make it more usable and nicer to look at. In this chapter, we’ll do our best to improve the styling and usability for our platform application. Pages Let’s start by taking a look at all the different pages we’ve created for our application. • Home Page (/) • Players – Players Index Page (/players) – Players Index JSON Page (/api/players) – New Player Page (/players/new) – Show Player Page (/players/:id) – Show Player JSON Page (/api/players/:id) – Edit Player Page (/players/:id/edit) • Games – Games Index JSON Page (/api/games) – Games Index Page (/api/games) • Sessions – Player Sign In Page (/sessions/new) Phoenix allows you to see a similar list of routes by running a mix command. Inside the root folder of our platform project, try running the following from the command line: 1 $ mix phx.routes This can be useful to see the paths available, the HTTP requests associated with them, and the controller actions as well. Here’s what the output should look like (note that this output has been trimmed for readability): 168 Layout and Design 1 2 3 4 5 6 7 8 9 10 11 $ mix phx.routes page_path GET player_path GET player_path GET player_path GET player_path GET player_path POST player_path PATCH PUT player_path DELETE # ... / /players /players/:id/edit /players/new /players/:id /players /players/:id /players/:id /players/:id PlatformWeb.PageController :index PlatformWeb.PlayerController :index PlatformWeb.PlayerController :edit PlatformWeb.PlayerController :new PlatformWeb.PlayerController :show PlatformWeb.PlayerController :create PlatformWeb.PlayerController :update PlatformWeb.PlayerController :update PlatformWeb.PlayerController :delete We can simplify the pages we need for our application now that we’re using Elm to display our lists of players and games, but we’ll start with our default Phoenix layout and home page. Phoenix Layout The template that contains our default Phoenix layout is located in the lib/platform_web/templates/layout folder, and it’s called app.html.eex. We worked with this file briefly when we added our initial authentication features in the header section. Let’s open that file and take a look at the contents (note that the content has been trimmed for readability): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!DOCTYPE html> <html lang="en"> <head> <!-- ... --> <title>Platform</title> <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>"> </head> <body> <div class="container"> <header class="header"><!-- ... --></header> <!-- ... --> <main role="main"><!-- ... --></main> </div> <!-- /container --> <script src="<%= static_path(@conn, "/js/app.js") %>"></script> </body> </html> We can see that our <title> and stylesheets ("/css/app.css") are loaded in the <head> section. For the purposes of this book, I’m going to continue using the generic name “Platform” for our application, but feel free to get creative and change the <title> tag as you see fit. 169 Layout and Design 1 <title>Platform</title> You can see the change in the browser tab if you adjust the title and load the page: Platform Title Bootstrap Inside our <body> tag, we create a <div> element with the container class. Phoenix comes preloaded with Bootstrap66 by default, and this class is the reason our application is centered on the page. Let’s change this class to container-fluid so we can use the full width of the page. 66 http://getbootstrap.com 170 Layout and Design 1 2 3 4 5 <body> <div class="container-fluid"> <!-- ... --> </div> </body> Notice how the layout of our application changes as we switch to a fluid container: Fluid Container Logo Instead of displaying the Phoenix logo in our header, let’s display a simple link that we can use to navigate back to our home page. Here’s the line that’s currently being used to apply a logo class and render the default Phoenix logo: 1 <span class="logo"></span> Layout and Design 171 We can replace this with a Phoenix link67 that routes back to our home page with page_path(@conn, :index). We’ll keep it simple with the "Platform" text, and we’re still going to apply the logo class so we can adjust the styles. 1 <%= link "Platform", to: page_path(@conn, :index), class: "logo" %> Next, open up the phoenix.css file inside the assets/css folder and we’ll scroll down to find the /* Custom page header */ section. Let’s remove the .header CSS declaration that’s adding a thin line beneath our header section. Then, we’ll adjust the .logo CSS declaration so we can remove our Phoenix background image and style the "Platform" text in our link: 1 2 3 4 5 6 7 8 9 10 11 /* Custom page header */ .logo { text-decoration: none; font-weight: bold; font-size: 3em; color: #333; } .logo:hover { text-decoration: none; } This gives us a working link we can use to reload the home page: 67 https://hexdocs.pm/phoenix_html/Phoenix.HTML.Link.html#link/2 172 Layout and Design Phoenix Logo Replaced with Link app.css We’ve looked at the default styles that Phoenix gives us in the phoenix.css file. For the rest of this chapter, let’s work with the app.css file that’s also located inside the assets/css folder. In fact, let’s remove all the custom CSS declarations at the bottom of the phoenix.css file and migrate the few we’ll need to app.css. In other words, we’ll leave all the minified Bootstrap CSS code in our phoenix.css file. But we’ll delete all the custom CSS declarations from the bottom, and our app.css file should just look like this: Layout and Design 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 173 /* This file is for your main application css. */ /* Phoenix flash messages */ .alert:empty { display: none; } /* Custom page header */ .logo { text-decoration: none; font-weight: bold; font-size: 3em; color: #333; } .logo:hover { text-decoration: none; } This will allow us to add all of our styles to the app.css file and we won’t have to bounce between files. Featured Section Now that we’ve updated our header section, let’s move below that and back into our Elm application where we handle our game data. Beneath the header, we’re going to add a “featured” section where we can feature a game that stands out from the rest of the content. Let’s open up our Main.elm file in the assets/elm folder, and we’ll update the view function while adding a new featured function just below it: 1 2 3 4 5 6 7 8 9 10 11 12 13 view : Model -> Html Msg view model = div [] [ featured model , gamesIndex model , playersIndex model ] featured : Model -> Html msg featured model = div [ class "row featured" ] [ h1 [] [ text "Featured" ] ] Layout and Design 174 Note that we added a featured class so we can use CSS to style this section. The row class comes from Bootstrap, and allows this section to stretch to the full width of the window. Open up the app.css file where we can add custom CSS for our application, and add the following: 1 2 3 4 5 6 /* Featured section */ .featured { height: 360px; background-color: black; color: white; } Featured Game Data Let’s split our “featured” section into two parts and pull in our game data. On the left side, we’ll display a screenshot of our game using the featured game’s thumbnail field. On the right side, we’ll display the game title, the game description, and a call to action button that says “Play Now!” for users to start playing the game. For the first step, let’s find a featured game to work with. Create a function called featuredGame that takes in a list of games (List Game) and returns the first featured game if there is one (Maybe Game). We can use this to pass in our full list of games and then use List.filter to narrow down to the ones that have .featured set to a True value. Then, we use List.head to return the first game if there is one. 1 2 3 4 5 featuredGame : List Game -> Maybe Game featuredGame games = games |> List.filter .featured |> List.head Now we can use this new function in our featured function to display the data from our featured game. We can use a case expression to show our featured section when our featuredGame function returns a game for us to work with. Otherwise, we’ll just render an empty div element if there are no featured games. This is a great example of where Maybe is so powerful, because our application won’t compile unless we handle both of these possibilities where we may or may not have a featured game to work with. In other languages and frameworks, we might forget to handle situations where we have no featured game data, and we wouldn’t actually find out until our application threw an error. Layout and Design 1 2 3 4 5 6 7 8 9 175 featured : Model -> Html msg featured model = case featuredGame model.gamesList of Just game -> div [ class "row featured" ] [ h1 [] [ text "Featured" ] ] Nothing -> div [] [] Let’s break up our featured section into a featured-img section for the left side and a featured-data section for the right side. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 featured : Model -> Html msg featured model = case featuredGame model.gamesList of Just game -> div [ class "row featured" ] [ div [ class "container" ] [ div [ class "featured-img" ] [ img [ class "featured-thumbnail", src game.thumbnail ]\ [] ] , div [ class "featured-data" ] [ h1 [] [ text "Featured" ] , h2 [] [ text game.title ] , p [] [ text game.description ] , button [ class "btn btn-lg btn-primary" ] [ text "Play\ Now!" ] ] ] ] Nothing -> div [] [] Note that we’re adding quite a few class names so we can use CSS to style each element. And we’re rendering our game data using game.thumbnail, game.title, and game.description. Let’s add a couple of CSS declarations to clean things up. Open the app.css file and we’ll add the following to our featured CSS declarations: Layout and Design 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 176 /* Featured section */ .featured { height: 360px; background-color: black; color: white; } .featured-img { margin-top: 50px; margin-right: 50px; float: left; } .featured-thumbnail { height: 260px; } .featured-data { margin-top: 50px; overflow: hidden; } This applies spacing and sizing to our featured thumbnail image, and floats it to the left so that our text data will appear on the right. We also apply an overflow property to our game data so that it won’t spill out of the featured section on smaller screens. These aren’t the fanciest of styles, but our featured game section works well for now: 177 Layout and Design Featured Section Authentication Section You may have noticed that our authentication information at the top right of the window doesn’t look great. Let’s open our app.html.eex file and we’ll make a few more changes. We’ll change the classes we’re using for our buttons, and we’ll display our “Signed in” text with a new class too: Layout and Design 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 178 <header class="header"> <nav role="navigation"> <ul class="nav nav-pills pull-right"> <%= if @current_user do %> <p class="navbar-text">Signed in as <strong><%= @current_user.username %\ ></strong></p> <%= link "Sign Out", to: player_session_path(@conn, :delete, @current_us\ er), method: "delete", class: "btn navbar-btn btn-danger" %> <% else %> <%= link "Sign Up", to: player_path(@conn, :new), class: "btn navbar-btn\ btn-success" %> <%= link "Sign In", to: player_session_path(@conn, :new), class: "btn na\ vbar-btn btn-primary" %> <% end %> </ul> </nav> <%= link "Platform", to: page_path(@conn, :index), class: "logo" %> </header> We can add a quick CSS rule so our nav elements at the top right of the window look okay and don’t break our layout on small screens: 1 2 3 4 5 .nav { margin-top: 6px; max-height: 50px; overflow: hidden; } Next, let’s allow the currently signed in player to edit their account. Update the nav element with the following: 1 2 3 4 5 6 7 8 9 10 11 <nav role="navigation"> <ul class="nav nav-pills pull-right"> <%= if @current_user do %> <p class="navbar-text"> Signed in as <strong><%= link @current_user.username, to: player_path(@conn, :edit, @\ current_user) %></strong> </p> <span><%= link "Sign Out", to: player_session_path(@conn, :delete, @curren\ t_user), method: "delete", class: "btn navbar-btn btn-danger" %></span> <% else %> 179 Layout and Design 12 13 14 15 16 17 18 <%= link "Sign Up", to: player_path(@conn, :new), class: "btn navbar-btn b\ tn-success" %> <%= link "Sign In", to: player_session_path(@conn, :new), class: "btn navb\ ar-btn btn-primary" %> <% end %> </ul> </nav> This adds a link for the current user to access their Edit Player page and change their account. Keep in mind that we still haven’t restricted access to pages, so users can technically edit each other’s accounts. But we’ll fix this issue soon. Current User Edit Feature User Deletion As long as we’re paying attention to usability in this chapter, we should also allow players to delete their accounts. Let’s add a delete button to the Edit Player page. First, we’ll open the lib/platform_web/templates/player/index.html.eex file and remove the delete button from that page. While we’re here, let’s remove the edit button too since we only want players to edit their own accounts. Here’s what the contents of the <tbody> tag should look like: Layout and Design 1 2 3 4 5 6 7 8 9 10 11 180 <%= for player <- @players do %> <tr> <td><%= player.username %></td> <td><%= player.score %></td> <td class="text-right"> <span><%= link "Show", to: player_path(@conn, :show, player), class: "bt\ n btn-default btn-xs" %></span> </td> </tr> <% end %> In the lib/platform_web/templates/player/edit.html.eex file, we’ll add the delete button to the list of buttons at the bottom of our Edit Player page. 1 2 3 4 5 6 7 8 <div class="form-group"> <%= submit "Submit", class: "btn btn-primary" %> <span><%= link "Delete Account", to: player_path(@conn, :delete, @player), met\ hod: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger" %></span\ > <span><%= link "Back", to: page_path(@conn, :index), class: "btn btn-default" \ %></span> </div> In the screenshot below, we created a newuser account to test that the delete button works. 181 Layout and Design Delete Account Button After verifying that a player wants to delete their account, the deletion should be successful: 182 Layout and Design Successful Player Deletion Authorization You may have noticed a serious issue with our account deletion approach. Players can now delete their accounts, but players could delete the accounts of other players too! For example, the chrismccord account could sign in to the platform, and then use the http://0.0.0.0:4000/players/1/edit URL to deviously delete José Valim’s account. For player authorization, we’re going to take a simple approach similar to the one we used to authenticate players on our platform. Let’s open the lib/platform_web/controllers/player_controller.ex file and take a look. At the bottom of the file, let’s add a new function called authorize/2 that takes in the conn and then decides whether players should be able to continue or if they should see a message indicating that they’re not authorized to access the page. Add the following private function at the bottom of the PlayerController module: Layout and Design 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 183 defp authorize(conn, _opts) do current_player_id = conn.assigns.current_user().id requested_player_id = conn.path_params["id"] |> String.to_integer() if current_player_id == requested_player_id do conn else conn |> put_flash(:error, "Your account is not authorized to access that page.") |> redirect(to: page_path(conn, :index)) |> halt() end end This function will compare the player that’s currently signed in with the player account that is currently being accessed. If an authenticated player is trying to access their own account, we just return the conn and allow them to continue. If a player is trying to access an account that’s not theirs, then we redirect back to the index page and provide the user a message telling them their account is not authorized. To get this working for the Edit Player page, add the following line above the index/2 function in the PlayerController module: 1 plug :authorize when action in [:edit] This is the same simple approach we took in the PageController when authenticating users. Now, when players try to access the Edit Player page, it’ll pipe them through the authorize/2 function. To make the comparison, we get the currently signed in player’s id from the current_user(), and we also get the requested player’s id from the path_params. If those two id values are equal, it means a player is accessing their own account (and they are allowed to continue). Otherwise, it means a player is trying to access an account that doesn’t belong to them (and they are redirected). Here’s an example of how the currently signed in user (chrismccord) can view the Edit Player page for their own account: 184 Layout and Design Authorized Player But if the player tries to access an account that does not belong to them (http://0.0.0.0:4000/players/1/edit), they should be redirected back to the home page and see a flash message: 185 Layout and Design Unauthorized Player Fixing Our Tests The authorize/2 function we created above works well for what we need. But this approach breaks our tests because the test environment won’t have access to the current user. In order to get our tests passing, we can add a quick hack to return the conn when we’re in the Mix.env test environment. 1 2 3 4 5 6 7 8 9 10 defp authorize(conn, _opts) do if Mix.env == :test do conn else current_player_id = conn.assigns.current_user().id requested_player_id = conn.path_params["id"] |> String.to_integer() Layout and Design 11 12 13 14 15 16 17 18 19 20 21 186 if current_player_id == requested_player_id do conn else conn |> put_flash(:error, "Your account is not authorized to access that page.") |> redirect(to: page_path(conn, :index)) |> halt() end end end This isn’t an ideal solution, but it’s a quick way for us to get our tests passing and keep moving. We’ve taken an admittedly reductive approach to authorization, but it works well for our simple application. If you’re looking to build a more involved authorization system, consider using an authorization library. A good approach for finding libraries is to use the search feature on hex.pm68 . For example, you can search for “authorization” on hex.pm and find a list of popular options. List of Games For our list of games, it looks like Bootstrap has a media object component69 that will work well for our needs. We’ll add a media-list class to our list of games, and then we’ll use media-left for the thumbnail and media-body for the text information. Feel free to take a look at the examples in the Bootstrap documentation if you’re interested in tinkering around with the styles for our application. We also want to wrap the each game in a link tag so that the clickable area is large for users to click on and access the game. We don’t have a game to send users to yet, so we’re just adding href "#" for now. 68 69 https://hex.pm https://getbootstrap.com/docs/3.3/components/#media Layout and Design 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 187 gamesList : List Game -> Html msg gamesList games = ul [ class "games-list media-list" ] (List.map gamesListItem games) gamesListItem : Game -> Html msg gamesListItem game = a [ href "#" ] [ li [ class "game-item media" ] [ div [ class "media-left" ] [ img [ class "media-object", src game.thumbnail ] [] ] , div [ class "media-body media-middle" ] [ h4 [ class "media-heading" ] [ text game.title ] , p [] [ text game.description ] ] ] ] Bootstrap takes care of most of the heavy lifting for us, but let’s add a couple of custom styles to the app.css file. Add the following CSS code to style our list of games: 1 2 3 4 5 6 7 8 9 10 /* Games section */ .game-item { margin-bottom: 15px; border: 2px solid black; border-radius: 10px; } .media-object { height: 120px; } List of Players Lastly, let’s style our list of players into a leaderboard. Bootstrap has a panel component70 that we can use to wrap around our player list. Then, we can use the list group component71 to display each player along with their current score. Update the playersList function and playersListItem function with the following and Bootstrap will take care of the styling for us: 70 71 https://getbootstrap.com/docs/3.3/components/#panels https://getbootstrap.com/docs/3.3/components/#list-group Layout and Design 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 188 playersList : List Player -> Html msg playersList players = div [ class "players-list panel panel-info" ] [ div [ class "panel-heading" ] [ text "Leaderboard" ] , ul [ class "list-group" ] (List.map playersListItem players) ] playersListItem : Player -> Html msg playersListItem player = let displayName = if player.displayName == Nothing then player.username else Maybe.withDefault "" player.displayName playerLink = "players/" ++ (toString player.id) in li [ class "player-item list-group-item" ] [ strong [] [ a [ href playerLink ] [ text displayName ] ] , span [ class "badge" ] [ text (toString player.score) ] ] Looks like this works well for our purposes. We’ve got our sorted list of players displaying inside a leaderboard with their display names and scores. And we also added links to the individual player pages, which we can use to track more detailed score data later. 189 Layout and Design Player Leaderboard Summary This book is primarily focused on working with Elixir and Elm, but this chapter was a fun aside into seeing how we can still use a familiar approach to styling with CSS and Bootstrap within the context of a Phoenix application. Layout and Design 190 We have our Phoenix API up and running, and our Elm application is pulling in all the sample data. Let’s move on to creating a game with Elm and pulling everything together! Game Setup We have our platform application layout in place, and we currently have a sample “Platformer” game title to work with. But if we click the link to this game, nothing happens yet because we haven’t wired up the game routes and configuration. In this chapter, we’ll configure our application so we can create new games inside our assets/elm folder, and then load them in the browser through our platform. Creating a Game File Let’s start by creating a new file for our “Platformer” game in the assets/elm folder. We can use the game’s title as our Elm module name, so let’s call the file Platformer.elm and initialize it with the following code so we have something to display on the page: 1 2 3 4 5 6 7 8 module Platformer exposing (..) import Html exposing (..) main : Html msg main = text "Platformer Game" Configuring Elm Brunch Now that we have mutiple Elm source code files, we’ll need to update the elmBrunch section of our brunch-config.js file. First, we’ll add "elm/Platformer.elm" to the list of mainModules. Then, we’ll add an outputFile property to compile all of our Elm source code into a single elm.js file. Game Setup 1 2 3 4 5 6 192 elmBrunch: { mainModules: ["elm/Main.elm", "elm/Platformer.elm"], makeParameters: ["--debug"], outputFile: "elm.js", outputFolder: "../assets/js" } When we run our Phoenix server, our application will compile all the Elm source code and output it to the new elm.js file. We can also include our new output file in the .gitignore file at the root of our project. 1 2 3 # Elm /assets/elm-stuff /assets/js/elm.js Feel free to delete the existing main.js file that’s no longer needed. Updating app.js With our new approach, we can now refactor our assets/js/app.js file. We’ll use a single require() statement to pull in our compiled elm.js output. 1 2 3 4 5 6 // Elm import Elm from "./elm"; const elmContainer = document.querySelector("#elm-container"); if (elmContainer) Elm.Main.embed(elmContainer); With the way we configured our application, we can now use Elm as a top-level namespace, and then embed our code using our module names. In the code above, we’re using Elm.Main, and we can also use Elm.Platformer to work with our new game. Game Setup 1 2 3 4 5 6 7 8 193 // Elm import Elm from "./elm"; const elmContainer = document.querySelector("#elm-container"); const platformer = document.querySelector("#platformer"); if (elmContainer) Elm.Main.embed(elmContainer); if (platformer) Elm.Platformer.embed(platformer); Keep in mind we haven’t created the div element with a #platformer id yet. Once we create that element, we’ll be able to embed our game using Elm.Platformer.embed(platformer). Extending Our GameController We’ve taken care of most of the tedious configuration steps. Now, let’s take a look at our PlatformWeb.GameController module in the lib/platform_web/controllers folder. This file currently contains functions that we use for our JSON API. What we want to do now is to basically add a new version of the “show” function that allows us to display a page in the browser for our games. Below the show/2 function, let’s add a new function called play/2. We’ll take the same approach as we do for showing the JSON associated with a game, but this time we’ll use "show.html" instead. 1 2 3 4 5 6 7 8 9 def show(conn, %{"id" => id}) do game = Products.get_game!(id) render(conn, "show.json", game: game) end def play(conn, %{"id" => id}) do game = Products.get_game!(id) render(conn, "show.html", game: game) end Adding a Route Now that we have our controller action, we can add a line to our router in the lib/platform_web/router.ex file. We’ll route to the play/2 function in our GameController based on the :id for each game: 194 Game Setup 1 2 3 4 5 6 7 8 scope "/", PlatformWeb do pipe_through :browser get "/", PageController, :index get "/games/:id", GameController, :play resources "/players", PlayerController resources "/sessions", PlayerSessionController, only: [:new, :create, :delete] end Creating a Template We also need to add a new template file for our games. Inside the lib/platform_web/templates folder, create a new folder called game. Then, create a file called show.html.eex with the following line: 1 <div id="<%= @game.title |> String.downcase %>"></div> What we’re doing here is using the game’s title field to dynamically create a new div element with an ID that matches that game’s title. That allows us to use the document.querySelector("#platformer") line that we used in our app.js code above. At this point, we should now have everything working well enough to see the “game” being rendered in the browser! When we visit http://0.0.0.0:4000, we see the Elm application we created in Main.elm. And when we visit http://0.0.0.0:4000/games/1, we see the new game we’re going to create in Platformer.elm. Display Game Game Setup 195 Working with Slugs Let’s clean things up a little bit. Rather than working with our game’s title field, we can add a new field called slug. This will allow us to display the game’s slug in the URL with a few changes. First, we’ll need to create a migration to add a new field to our games. Let’s go to the command line and run the following command: 1 $ mix ecto.gen.migration add_slug_to_games We should see the following output (bearing in mind that the timestamp on the migration file will look slightly different on your machine): 1 2 3 $ mix ecto.gen.migration add_slug_to_games * creating priv/repo/migrations * creating priv/repo/migrations/20170913215145_add_slug_to_games.exs Now, open the migration file that was generated in the priv/repo/migrations folder and alter our games table with the new slug field. We’ll also create a unique index since we want slugs to be a unique identifier for our games. 1 2 3 4 5 6 7 8 9 10 11 defmodule Platform.Repo.Migrations.AddSlugToGames do use Ecto.Migration def change do alter table(:games) do add :slug, :string end create unique_index(:games, [:slug]) end end Updating the Schema We can also this field to our games schema. Open the lib/products/game.ex file and update the schema with the following: Game Setup 1 2 3 4 5 6 7 8 9 10 11 196 schema "games" do many_to_many :players, Player, join_through: Gameplay field field field field field :description, :string :featured, :boolean, default: false :slug, :string, unique: true :thumbnail, :string :title, :string timestamps() end At the bottom of that same lib/products/game.ex file, we’ll want to add our new field to the cast/2 and validated_required/1 functions (note that I tend to alphabetize fields, but it’s not entirely necessary). We’ll also add a unique_constraint/1 function to verify that our slug fields are unique. 1 2 3 4 5 6 def changeset(%Game{} = game, attrs) do game |> cast(attrs, [:description, :featured, :slug, :thumbnail, :title]) |> validate_required([:description, :featured, :slug, :thumbnail, :title]) |> unique_constraint(:slug) end Running the Migration and Adding a Slug Let’s run our migration to add our new slug field to our games. 1 $ mix ecto.migrate Now, we should be able to update our existing game record so it has a slug field associated with it. First, we can start an interactive Phoenix server with the following command: 1 $ iex -S mix phx.server Now that we have an interactive console available, let’s find our existing game record: 1 iex(1)> Platform.Products.get_game!(1) We can pipe this to the update_game/2 function to add a slug to our existing game record: Game Setup 1 2 197 iex(2)> Platform.Products.get_game!(1) |> Platform.Products.update_game(%{slug: \ "platformer"}) We should be able to see the results with the updated game record containing a slug field: 1 2 3 4 5 6 7 8 9 iex(2)> Platform.Products.get_game!(1) |> Platform.Products.update_game(%{slug: \ "platformer"}) {:ok, %Platform.Products.Game{__meta__: #Ecto.Schema.Metadata<:loaded, "games">, description: "Platform game example.", featured: true, id: 1, inserted_at: ~N[2017-12-08 20:37:44.080271], players: #Ecto.Association.NotLoaded<association :players is not loaded>, slug: "platformer", thumbnail: "http://via.placeholder.com/300x200", title: "Platformer", updated_at: ~N[2017-12-08 20:37:44.080277]}} Fixing the Tests Since we added a new field, we’ll need to update our tests as well. At the top of the test/platform/products/products_test.exs file, we can add our new slug field to the attributes with the following: 1 2 3 4 5 6 @valid_attrs %{description: "some description", featured: true, thumbnail: "some\ thumbnail", title: "some title", slug: "some-slug"} @update_attrs %{description: "some updated description", featured: false, thumbn\ ail: "some updated thumbnail", title: "some updated title", slug: "some-slug"} @invalid_attrs %{description: nil, featured: nil, thumbnail: nil, title: nil, sl\ ug: nil} Similarly, we can update test/platform_web/controllers/game_controller_test.exs with the following: 1 2 3 4 5 6 @create_attrs %{description: "some description", featured: true, thumbnail: "som\ e thumbnail", title: "some title", slug: "some-slug"} @update_attrs %{description: "some updated description", featured: false, thumbn\ ail: "some updated thumbnail", title: "some updated title", slug: "some-slug"} @invalid_attrs %{description: nil, featured: nil, thumbnail: nil, title: nil, sl\ ug: nil} We should now be able to run our tests again and see them all passing! Game Setup 1 2 3 4 5 6 7 198 $ mix test ................................................ Finished in 0.4 seconds 48 tests, 0 failures Randomized with seed 444371 Using Our New Slug Field Now that we have our new slug field to work with, we can refactor our game routing features. Let’s go back to the lib/platform_web/templates/game/show.html.eex file and update the contents with the following code: 1 <div id="<%= @game.slug %>"></div> This means that we’re creating a new div element on the page with the game’s slug as the id attribute. Pretty URLs To finish getting this working, we’ll need to refactor a couple more things. We’ll start with our lib/platform/products/products.ex file, where we can add a new function to fetch games by their slug field. Below the get_game!/1 function, let’s add a new function called get_game_by_slug!/1. 1 2 def get_game!(id), do: Repo.get!(Game, id) def get_game_by_slug!(slug), do: Repo.get_by!(Game, slug: slug) Now in the lib/platform_web/controllers/game_controller.ex file, we can update our existing play/2 function to handle slugs instead of ids. 1 2 3 4 def play(conn, %{"slug" => slug}) do game = Products.get_game_by_slug!(slug) render(conn, "show.html", game: game) end Lastly, we’ll update our lib/platform_web/router.ex file with the following: 199 Game Setup 1 2 3 4 5 6 7 8 scope "/", PlatformWeb do pipe_through :browser get "/", PageController, :index get "/games/:slug", GameController, :play resources "/players", PlayerController resources "/sessions", PlayerSessionController, only: [:new, :create, :delete] end This may seem like a lot of work (and it’s admittedly tedious), but it’s good to get practice with adding fields and making changes. It’s also great news, because we now have the ability to access our games via the slug field in the URL! Display Game with Slug This will make things much easier on us as we decide to add new games to our platform application. Working Links You may have noticed that we still haven’t accomplished our original goal from the beginning of the chapter. Most of the hard work is taken care of, and now we can make a few more changes on the front-end to get things running smoothly. We can start by adding our new slug field to the JSON response for games. In the lib/platform_web/game_view.ex file, update the function at the bottom with the following: Game Setup 1 2 3 4 5 6 7 8 200 def render("game.json", %{game: game}) do %{id: game.id, description: game.description, featured: game.featured, slug: game.slug, thumbnail: game.thumbnail, title: game.title} end This update to the JSON response will require some quick updates to our tests again. Open the test/platform_web/controllers/game_controller_test.exs file and update the describe blocks for "create game" and "update game". We only need to add keys and values for our new slug field to the JSON responses, but here are the full blocks of code for context: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 describe "create game" do test "renders game when data is valid", %{conn: conn} do conn = post conn, game_path(conn, :create), game: @create_attrs assert %{"id" => id} = json_response(conn, 201)["data"] conn = get conn, game_path(conn, :show, id) assert json_response(conn, 200)["data"] == %{ "id" => id, "description" => "some description", "featured" => true, "thumbnail" => "some thumbnail", "title" => "some title", "slug" => "some-slug"} end test "renders errors when data is invalid", %{conn: conn} do conn = post conn, game_path(conn, :create), game: @invalid_attrs assert json_response(conn, 422)["errors"] != %{} end end describe "update game" do setup [:create_game] test "renders game when data is valid", %{conn: conn, game: %Game{id: id} = ga\ me} do conn = put conn, game_path(conn, :update, game), game: @update_attrs assert %{"id" => ^id} = json_response(conn, 200)["data"] Game Setup 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 201 conn = get conn, game_path(conn, :show, id) assert json_response(conn, 200)["data"] == %{ "id" => id, "description" => "some updated description", "featured" => false, "thumbnail" => "some updated thumbnail", "title" => "some updated title", "slug" => "some-slug"} end test "renders errors when data is invalid", %{conn: conn, game: game} do conn = put conn, game_path(conn, :update, game), game: @invalid_attrs assert json_response(conn, 422)["errors"] != %{} end end Decoding Slug Data in Elm Now, we can go back to our main Elm front-end application in assets/elm/Main.elm and decode this JSON. First, we’ll add to our Game type at the top: 1 2 3 4 5 6 7 8 type alias Game = { description : String , featured : Bool , id : Int , slug: String , thumbnail : String , title : String } And now we can update our game decoder function to use map6 and decode the slug field as a string: Game Setup 1 2 3 4 5 6 7 8 9 202 decodeGame : Decode.Decoder Game decodeGame = Decode.map6 Game (Decode.field "description" Decode.string) (Decode.field "featured" Decode.bool) (Decode.field "id" Decode.int) (Decode.field "slug" Decode.string) (Decode.field "thumbnail" Decode.string) (Decode.field "title" Decode.string) Lastly, we can finally update our gamesListItem function that contains the link to our game. All we need to update in this function is our href attribute. 1 2 3 4 5 6 7 8 9 10 11 12 13 gamesListItem : Game -> Html msg gamesListItem game = a [ href <| "games/" ++ game.slug ] [ li [ class "game-item media" ] [ div [ class "media-left" ] [ img [ class "media-object", src game.thumbnail ] [] ] , div [ class "media-body media-middle" ] [ h4 [ class "media-heading" ] [ text game.title ] , p [] [ text game.description ] ] ] ] It works! We now have working links in our list of games, and you can hover over the game link in the browser to see that the link points to the URL with the slug. 203 Game Setup Working Link to Game Featured Game Link While we’re here, let’s go ahead and update the featured function in our Elm application so that the link to our featured game works as well. This is just a one line change to conver the button element to a link with an href attribute, but here’s the full featured function for context: Game Setup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 204 featured : Model -> Html msg featured model = case featuredGame model.gamesList of Just game -> div [ class "row featured" ] [ div [ class "container" ] [ div [ class "featured-img" ] [ img [ class "featured-thumbnail", src game.thumbnail ]\ [] ] , div [ class "featured-data" ] [ h1 [] [ text "Featured" ] , h2 [] [ text game.title ] , p [] [ text game.description ] , a [ class "btn btn-lg btn-primary", href <| "games/" +\ + game.slug ] [ text "Play Now!" ] ] ] ] Nothing -> div [] [] Now our “Play Now!” button in the featured section should work as well to access our featured game. Summary This was a relatively long chapter to set up the games on our platform, but we managed to make a ton of progress. And the good news is that now we can create games and add them to our platform with a relatively flexible approach. When creating new games, we’ll add a new Elm file that contains the source code. Then, we update the brunch configuration to compile it. Then, we embed the game in the app.js file, and the rest should be taken care of for us. Now that we have our platform running and a working configuration for games, we can finally dive into creating our first game in the next chapter! Our First Game Let’s start creating our first minigame with Elm. We want to begin with something small and simple that still has the characteristics we want for all of our games. Our initial game should be small, self-contained, interactive, and fun. And we’ll also want to add a simple scoring mechanism so we can work towards tracking player scores and sending that data to our back-end platform. We have our platform running with a sample game file to work with, now we can finally focus on writing the actual game. In the next sections, we’re going to set up our Elm application, add SVG for our game’s background, create a little character, and wire up the keyboard for interaction. Base Application for Our Game We’ve already got some experience in the preceding chapters with the Elm Architecture, so we’re not going to cover it in great detail here. Instead, we’re going to start by pasting in the following code, which will give us some starter code to work with. Add the following to the Platformer.elm file: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 module Platformer exposing (..) import Html exposing (Html, div, text) -- MAIN main : Program Never Model Msg main = Html.program { init = init , view = view , update = update , subscriptions = subscriptions } -- MODEL Our First Game 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 206 type alias Model = {} initialModel : Model initialModel = {} init : ( Model, Cmd Msg ) init = ( initialModel, Cmd.none ) -- UPDATE type Msg = NoOp update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of NoOp -> ( model, Cmd.none ) -- SUBSCRIPTIONS subscriptions : Model -> Sub Msg subscriptions model = Sub.none -- VIEW view : Model -> Html Msg view model = div [] [ text "Elm Game Base" ] There are different conventions for setting up Elm applications, but this is a minimal base application that I like to start with. Our First Game 207 We have all the things we need to get started: • • • • minimal imports. a main function to wire everything together. an empty Model type and init function. a default update function with a “NoOp” placeholder case that allows us to perform no operation. This may seem confusing at first, but it’s helpful to have a default message to work with before we start updating our model. • an intitial subscriptions function that starts with Sub.none. • a simple view with a container div and some text that we can see to ensure that our application is working. Creating a Game Canvas Before we can add our game character, we need to create a window where our hero (or heroine) can live. Let’s start using SVG to create our game world. In order to work with Elm’s SVG library, we’ll need to install the package and import it into our project. From the command line, let’s switch to the assets folder and run the following command: 1 $ elm-package install elm-lang/svg After agreeing to install the package by entering the Y key, here’s the output we should see: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ elm elm-package install elm-lang/svg To install elm-lang/svg I would like to add the following dependency to elm-package.json: "elm-lang/svg": "2.0.0 <= v < 3.0.0" May I add that to elm-package.json for you? [Y/n] Y Some new packages are needed. Here is the upgrade plan. Install: elm-lang/svg 2.0.0 Do you approve of this plan? [Y/n] Y Starting downloads... Our First Game 16 17 18 19 208 � elm-lang/svg 2.0.0 Packages configured successfully! Now that we have the package installed, let’s import it at the top of our Platformer.elm file. We’ll import all the Svg functions along with all the Svg.Attributes functions since we’ll be using many of them. Update the top of your Platformer.elm file with the following code (also note that we’re removing the text import from the Html package to avoid ambiguity): 1 2 3 4 5 module Platformer exposing (..) import Html exposing (Html, div) import Svg exposing (..) import Svg.Attributes exposing (..) Setting Up a Game Window Now we can create a small window to use for our game. We’re going to create a rectangle that has a 600px width and a 400px height. The rectangle will reside inside our SVG element, and that will reside inside our HTML code. You don’t need to type this code in, but here’s an HTML visualization that might help conceptualize how we’ll lay out the elements on our page: 1 2 3 4 5 6 7 8 9 <html> <body> <div> <svg> <rect /> <!-- We'll create our game inside this rectangle. --> </svg> </div> </body> </html> Even though we’re using SVG for our game, we still want to nest it inside an HTML document with a container div element. The reason for this is that we might want to add HTML elements outside the game, or we could even add multiple minigames to a single page if we wanted to. Also, don’t worry too much if you’re unfamiliar with SVG. If you have experience working with HTML elements and attributes and values, then it’s easy to pick up. There are some quirks to working with SVG, but we’ll learn what we need to learn to start creating our Elm games so Our First Game 209 we can keep moving. Feel free to take a look online, though, because there are some great SVG learning materials and courses available. The SVG documentation from MDN72 is always helpful as a searchable reference, and the package documentation73 for elm-lang/svg is also helpful. Let’s add our SVG code to our Elm view, and we’ll walk through how it all fits together. At the bottom of the Platformer.elm file, add the following: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 view : Model -> Html Msg view model = div [] [ viewGame ] viewGame : Svg Msg viewGame = svg [ version "1.1", width "600", height "400" ] [ viewGameWindow ] viewGameWindow : Svg Msg viewGameWindow = rect [ width "600" , height "400" , fill "none" , stroke "black" ] [] We start with a div element at the top, which will contain our svg element. The svg element will contain the rect element that serves as our small game window. Note that we need to add the same width and height for both of these elements, or else we’d run the risk that the size of our rectangle might exceed the size of our surrounding svg element. Lastly, we add a stroke attribute to see a "black" line around our game window, and a fill attribute with an initial value of "none". 72 73 https://developer.mozilla.org/en-US/docs/Web/SVG http://package.elm-lang.org/packages/elm-lang/svg/latest/Svg 210 Our First Game Empty Game Window Adding the Sky and the Ground Now that we have an empty canvas to work with, let’s add a sky and a ground for our game world. Just like we did for our game window rectangle, we’ll use two more rect elements to represent a beautiful blue sky and some green grass for the ground. Add the following code to see our game world start taking shape: Our First Game 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 viewGame : Svg Msg viewGame = svg [ version "1.1", width "600", height "400" ] [ viewGameWindow , viewGameSky , viewGameGround ] viewGameWindow : Svg Msg viewGameWindow = rect [ width "600" , height "400" , fill "none" , stroke "black" ] [] viewGameSky : Svg Msg viewGameSky = rect [ x "0" , y "0" , width "600" , height "300" , fill "#4b7cfb" ] [] viewGameGround : Svg Msg viewGameGround = rect [ x "0" , y "300" , width "600" , height "100" , fill "green" ] [] 211 212 Our First Game Game Window with Sky and Ground Note that the width for all of our rectangles is the same. But we adjust the height so that the ground takes up the bottom 25% of the window and the sky takes up the top 75% of the window. Then we use the x and y attributes to move our SVG shapes to the correct placement. We haven’t added our character or interaction yet, but our tiny game world is starting to come to life! Creating Our Character We have a few options for adding a small character to our game: • Create a small SVG rectangle shape that represents the character. This option isn’t as fun, but it’s a quick way to get something on the page and keep moving. • Use an existing character image from the web. Check out free sites like opengameart.org for assets, or if you’re not planning on releasing your game publicly then you can search Google for character sprites from your favorite games and use those. If you choose this option, be sure to look for characters that have a transparent background. And you may need to resize the images you find to fit properly within the game we’ll be creating. 213 Our First Game • Draw your own character with piskelapp.com. It’s a lot of fun, but it’s also difficult and it may be more worthwhile to use something else for now while we build our initial minigame. Keep in mind that we’re using a static, motionless character image for now, and we don’t need to add running or jumping animations yet. In other words, we’ll still want to add keyboard interaction soon so that we can move our character around the screen, but the character itself won’t be animating while we move it around. While writing this chapter, I ended up experimenting with creating a pixel art character using piskelapp.com. You’re welcome to use this asset, which is available in the GitHub repository74 for this book. Feel free to create your own asset, or choose another one from the web. Just make sure you download the image file you want to use, and we’ll import it into our application next. 2D Pixel Art Character Importing Our Character Now that we have our character asset, let’s import it into our game. When using Phoenix, we can add images to the assets/static/images folder and they’ll be available from anywhere in our application (this is how the phoenix.png image was displayed on the default start page when we first started our Phoenix server). Let’s move our character.gif file inside that folder. This will make our character image available at /images/character.gif. At the bottom of our Game.elm file, we can now add to the view to render our new game character: 74 https://github.com/elixir-elm-tutorial/elixir-elm-tutorial-book/tree/master/manuscript/images/our_first_game/character.gif Our First Game 1 2 3 4 5 6 7 8 9 10 214 viewCharacter : Svg Msg viewCharacter = image [ xlinkHref "/images/character.gif" , x "1" , y "300" , width "50" , height "50" ] [] And we’ll need to update our viewGame function so that it invokes our new view function: 1 2 3 4 5 6 7 8 viewGame : Svg Msg viewGame = svg [ version "1.1", width "600", height "400" ] [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter ] With the viewCharacter function, we’re creating an SVG image element with a width and height of "50" pixels. We’re also hard-coding the x and y attributes to set the initial character position inside the grass area of our game window. We can use the x attribute to move our character left and right, and we can use the y attribute to move our character up and down. 215 Our First Game Game Character Rendered Changing the Character Position Let’s refactor our viewCharacter function to make it easier to work with the character position. The SVG attributes need to work with strings as values, but for our purposes it’s more helpful to think of the character’s position in number values. We can use Elm’s let expressions to refactor. Instead of manually setting the x attribute to a value of "1", let’s hoist that value up to a let expression assignment: 1 2 3 4 5 6 7 8 9 10 viewCharacter : Svg Msg viewCharacter = let characterPositionX = 1 in image [ xlinkHref "/images/character.gif" , x (toString characterPositionX) , y "300" 216 Our First Game 11 12 13 14 , width "50" , height "50" ] [] Our code works the same as it did before, but now we can start thinking about our characterPositionX value in numbers instead of strings. Let’s go ahead and do the same for the y attribute: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 viewCharacter : Svg Msg viewCharacter = let characterPositionX = 1 characterPositionY = 300 in image [ xlinkHref "/images/character.gif" , x (toString characterPositionX) , y (toString characterPositionY) , width "50" , height "50" ] [] Our code is still working the same way, we’re just moving things around to make things easier to work with and reason about. Updating the Model Since our character position is something that will change, let’s move it to our model instead of hard-coding it in our view functions. Let’s update the type alias for our Model as well as the values in the initialModel: Our First Game 1 2 3 4 5 6 7 8 9 10 11 217 type alias Model = { characterPositionX : Int , characterPositionY : Int } initialModel : Model initialModel = { characterPositionX = 1 , characterPositionY = 300 } Now, we’ll have to pass our model through the view functions so we can access those values in the right places. Our view function already takes model as an argument, so we can pass that along to the viewGame function like this: 1 2 3 view : Model -> Html Msg view model = div [] [ viewGame model ] Then, we’ll update both the type annotation and function declaration for viewGame so it takes model as an argument and passes it along to the viewCharacter function at the bottom. 1 2 3 4 5 6 7 8 viewGame : Model -> Svg Msg viewGame model = svg [ version "1.1", width "600", height "400" ] [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter model ] This works well for our needs, because it means we can pass the model data as it changes to the viewCharacter function, which allows us to keep changing the character’s position. Let’s go ahead and update the viewCharacter function. We’ll accept model as an argument, and that means we’ll be able to access any value directly from our model. That also simplifies our code, because we won’t need the let expression anymore. Update the viewCharacter function with the following: Our First Game 1 2 3 4 5 6 7 8 9 10 218 viewCharacter : Model -> Svg Msg viewCharacter model = image [ xlinkHref "/images/character.gif" , x (toString model.characterPositionX) , y (toString model.characterPositionY) , width "50" , height "50" ] [] At this point, we have working data from the model that’s being rendered to the view. Let’s move our character slightly to the right on the screen by increasing the characterPositionX value to 50 in the initialModel: 1 2 3 4 5 initialModel : Model initialModel = { characterPositionX = 50 , characterPositionY = 300 } 219 Our First Game Altering Character Position Adding an Item Our character is looking pretty lonely in our minigame world. Let’s add an item to our world, and then we’ll work towards having our character be able to pick up the item in the next chapter. We’re going to follow many of the same steps we did for our character image, so we’ll move quickly in this section. First, let’s add a coin.svg image to our project to use as our item. The coin.svg asset we’ll be using is available in the GitHub repository75 for this book. Let’s move our coin.svg file inside the assets/static/images folder just like we did for our character image, which will make it available at /images/coin.svg. Now we can update our model to set a position for the new item: 75 https://github.com/elixir-elm-tutorial/elixir-elm-tutorial-book/tree/master/manuscript/images/our_first_game/coin.svg Our First Game 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 220 type alias Model = { characterPositionX : Int , characterPositionY : Int , itemPositionX : Int , itemPositionY : Int } initialModel : Model initialModel = { characterPositionX = 50 , characterPositionY = 300 , itemPositionX = 500 , itemPositionY = 300 } Then, we can update our viewGame function and create a new viewItem function that takes in a model argument and returns some SVG code to render: 1 2 3 4 5 6 7 8 9 viewGame : Model -> Svg Msg viewGame model = svg [ version "1.1", width "600", height "400" ] [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter model , viewItem model ] Keep in mind that the order of our view functions doesn’t matter, but it’s still a good idea to group similar functions. Add the following below the viewCharacter function: 1 2 3 4 5 6 7 8 9 10 viewItem : Model -> Svg Msg viewItem model = image [ xlinkHref "/images/coin.svg" , x (toString model.itemPositionX) , y (toString model.itemPositionY) , width "20" , height "20" ] [] 221 Our First Game We essentially took the same approach for our character and our new coin item. The only difference is that we moved the coin item to the right of the screen and altered the width and height attributes so that the item is smaller than our character. This is what our results should look like so far: Character and Item Rendered Summary We managed to accomplish a lot in this chapter. We created a space for our first game, set up an initial SVG game world, added a character, and added an item. But keep in mind that we’ve been hard-coding a lot of values in our quest to get something up and running. In the next chapter, we’ll be taking a look at Elm subscriptions as a way to handle keyboard input and add interaction to our game. Adding Interaction In the last chapter, we managed to set up our first game. Now, we can start adding interactivity to make it come to life. We’re going to allow our players to interact with our character via keyboard input, and we’ll learn about Elm subscriptions along the way. Subscriptions When working with the Elm Architecture, subscriptions allow us to use streams of data and subscribe to a sequence of events. Keyboard and mouse input from users are a great examples of how this works. For instance, we can “subscribe” to the user’s mouse position, and it will allow to track the mouse location as it changes over time. Don’t worry if it sounds a little confusing, we’ll take a look at how we can subscribe to keyboard input now. Importing the Keyboard Package In order to work with keyboard input, we’ll need to start by importing the Elm Keyboard76 package. From the command line, let’s switch to the assets folder and run the following command: 1 $ elm-package install elm-lang/keyboard After agreeing to install the package by entering the Y key, here’s the output we should see: 1 2 3 4 5 6 7 8 9 10 11 $ elm-package install elm-lang/keyboard To install elm-lang/keyboard I would like to add the following dependency to elm-package.json: "elm-lang/keyboard": "1.0.1 <= v < 2.0.0" May I add that to elm-package.json for you? [Y/n] Y Some new packages are needed. Here is the upgrade plan. Install: 76 http://package.elm-lang.org/packages/elm-lang/keyboard/latest/Keyboard Adding Interaction 12 13 14 15 16 17 18 19 20 21 223 elm-lang/dom 1.1.1 elm-lang/keyboard 1.0.1 Do you approve of this plan? [Y/n] Y Starting downloads... � elm-lang/keyboard 1.0.1 � elm-lang/dom 1.1.1 Packages configured successfully! Now that we have the package installed, let’s import it at the top of our Platformer.elm file. We’ll need to import KeyCodes along with the downs function. Each key on your keyboard is represented by an integer. The Elm core library comes with functions called fromCode and toCode to convert back and forth between keyboard keys and their related integer representations. As an example, we’re going to want our character to move right when we press the right arrow key on the keyboard. That key is represented by the integer 39 (you can use keycode.info77 to type on your keyboard and see the related integer value, but these values are easy to look up online, so we don’t need to memorize them). In our application, we’ll be able to determine that users are pressing the right arrow key, and adjust our character’s position accordingly. Let’s update the top of our Platformer.elm file with the following: 1 2 3 4 5 6 module Platformer exposing (..) import import import import Html exposing (Html, div) Keyboard exposing (KeyCode, downs) Svg exposing (..) Svg.Attributes exposing (..) Tracking Key Presses In order to track key presses, we’ll need to make two changes: • subscribe to key presses in our subscriptions function • handle the key presses in our update function Let’s start with the subscriptions function. Instead of Sub.none, let’s subscribe to key presses with the following code: 77 http://keycode.info Adding Interaction 1 2 3 224 subscriptions : Model -> Sub Msg subscriptions model = Sub.batch [ downs KeyDown ] One thing to keep in mind is that this code won’t work until we add KeyDown to our update function. The Sub.batch function allows us to batch together different subscriptions, so we could also subscribe to mouse input if we needed to. For now, all we need to know is that we’re using the Elm Architecture to subscribe to keyboard input via the downs function, and we’re going to handle these presses with the KeyDown message in the update function. As an initial way to get keyboard input working, we’re going to set things up so that any key press will move the character slightly to the right on the screen (towards the coin item). To accomplish this, we’ll start by creating our new KeyDown update message, that takes a KeyCode as an argument: 1 2 3 type Msg = NoOp | KeyDown KeyCode We have two update actions, the first is to perform no operation with NoOp, and the second will perform an update to the model based on KeyDown actions from the user. Let’s finish getting things working again with a big change to our update function: 1 2 3 4 5 6 7 8 9 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of NoOp -> ( model, Cmd.none ) KeyDown keyCode -> ( { model | characterPositionX = model.characterPositionX + 15 }, Cm\ d.none ) There’s a lot going on here, so don’t worry if it seems a little overwhelming at first. The best way to think about the update function is that it takes in the existing model as an argument, applies an update, and returns the new updated version of the model. In this case, we start with our character at the initial starting position, and with each key press we’re going to update that position and see that value change through the model. The key part to focus on for now is Elm’s record update syntax, which looks like this: 1 { model | characterPositionX = model.characterPositionX + 15 } 225 Adding Interaction We set up our model as a record with several fields in it. With this syntax, we can change the value of the characterPositionX field in the model. It takes some getting used to, but for now all we need to know is that we’re setting a new value to characterPositionX every time we press a key on the keyboard. In the initialModel, we set our characterPositionX value to 50. Now with every key press we’re increasing that value by 15. In other words, if you press any key four times, the character will move to the right by a total of 60 pixels. Setting the Correct Keys It’s exciting to see our character moving around the screen, but we want to be able to change direction based on the specific key we’re pressing. In order to accomplish this, let’s add a case expression inside our update function. We only want our character to move to the right when we press the right arrow key (which has the key code 39) on our keyboard. We’re already passing the keyCode to our KeyDown message, so we can use our case expression to check which key is being pressed and respond with the following: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of NoOp -> ( model, Cmd.none ) KeyDown keyCode -> case keyCode of 39 -> ( { model | characterPositionX = model.characterPositionX + \ 15 }, Cmd.none ) _ -> ( model, Cmd.none ) Our game is basically performing the same way it was before, where the character can move to the right. But now that action should only be triggered when we press down on the right arrow key. In other words, we’re adding 15 to the characterPositionX value every time we press the right arrow key on the keyboard. And the _ part of the case expression allows us to handle all other scenarios (any other key presses). We’ll make no change to the model when any other key than the right arrow is pressed. One of the reasons that Elm is such a strong language and offers so many guarantees is that it forces us to account for all possibilities. So when we define the behavior we want for the right arrow key, it Adding Interaction 226 makes sure that we don’t forget all the other keys and wants us to think about what other behavior we would want when other keys are pressed. In this case, it’s fairly straightforward because we want the character to move right when the right arrow key is pressed and for no action to happen with any other keys. As an aside, it’s normally considered good practice to explicitly account for possibilities. So if we were creating a case expression that only had a few possibilities, we’d try to add a separate conditions for each one to handle them thoughtfully. But in the case of a keyboard we’re only hoping to use a couple of the keys for now, so we’re going to default to no action when most of the keys are pressed. Changing Direction Let’s go ahead and add the ability for our character to move in the left direction as well. This will involve some familiar changes to our KeyDown message, where we’re going to add a case for the left arrow key (37) and subtract from the character’s horizontal position value: 1 2 3 4 5 6 7 8 9 10 11 12 KeyDown keyCode -> case keyCode of 37 -> ( { model | characterPositionX = model.characterPositionX - 15 }, Cm\ d.none ) 39 -> ( { model | characterPositionX = model.characterPositionX + 15 }, Cm\ d.none ) _ -> ( model, Cmd.none ) Character Direction We now have the ability to move our character to the left and the right on the screen. Let’s add a new Direction union type so that the character can face the correct direction when moving left and right. 1 2 3 type Direction = Left | Right This allows us to set a direction with only two possible values. Similarly, the definition for Bool in Elm allows us to select only from True and False as values: Adding Interaction 1 2 3 227 type Bool = True | False Let’s add a characterDirection field to our Model, and then set its initial value to Right in the initialModel. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type alias Model = { characterDirection : Direction , characterPositionX : Int , characterPositionY : Int , itemPositionX : Int , itemPositionY : Int } initialModel : Model initialModel = { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 , itemPositionX = 500 , itemPositionY = 300 } Now, we can update the KeyDown case in our update function so that the character’s direction will change when the left and right arrow keys are pressed. The format of the record update syntax may look a little different than what we’ve been used to seeing so far, but we’re still doing the same thing. When players press the left arrow key (37), the character will move 15 pixels to the left and the characterDirection will be set to Left. Similarly, when players press the right arrow key (39), the character will move 15 pixels to the right and the characterDirection will be set to Right. 228 Adding Interaction 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 KeyDown keyCode -> case keyCode of 37 -> ( { model | characterDirection = Left , characterPositionX = model.characterPositionX - 15 } , Cmd.none ) 39 -> ( { model | characterDirection = Right , characterPositionX = model.characterPositionX + 15 } , Cmd.none ) _ -> ( model, Cmd.none ) Now that we’re going to have our character change direction, we’ll also want to update the assets so that the character looks like he or she is facing in the correct direction too. Let’s create two new copies of the character.gif file in the /assets/static/images folder. We’ll create one called characterright.gif which will be exactly the same as character.gif. And we’ll also create characterleft.gif, which is the same image flipped along the horizontal axis. If you’re using macOS, you can open the file in Preview and click the Tools > Flip Horizontal option. character-left.gif 229 Adding Interaction character-right.gif This assets are also available in the reposotiry for this book if you’d like to download them from GitHub. • character-left.gif78 • character-right.gif79 Once we have these two files available in our assets/static/images folder, we just need to update our viewCharacter function and everything should work as intended. We’ll use a let expression to determine which image to load based on the value of model.characterDirection. Here’s our updated viewCharacter function: 1 2 3 4 5 6 7 8 9 10 11 12 13 viewCharacter : Model -> Svg Msg viewCharacter model = let characterImage = case model.characterDirection of Left -> "/images/character-left.gif" Right -> "/images/character-right.gif" in image [ xlinkHref characterImage 78 79 https://github.com/elixir-elm-tutorial/elixir-elm-tutorial-book/tree/master/manuscript/images/adding_interaction/character-left.gif https://github.com/elixir-elm-tutorial/elixir-elm-tutorial-book/tree/master/manuscript/images/adding_interaction/character-right.gif 230 Adding Interaction 14 15 16 17 18 19 , x (toString model.characterPositionX) , y (toString model.characterPositionY) , width "50" , height "50" ] [] And with that change, we should be able to see our character changing direction successfully in the browser: Working Direction Change Collecting Items We can move our character to the left and right, and we already added an item that our character will be able to pick up. Currently, our character can move over to the item, but nothing happens when we get there. What we’d like to do at this point is to be able to move the character to the item, increment our score, and spawn a new item. One way that we can start thinking about this is to figure out what we want to do when the character reaches the item. Let’s add a characterFoundItem function below our update function that will Adding Interaction 231 return a boolean value about whether or not the character has discovered the item. We’ll use the position of both the character and the item to see if they match, and then we’ll return True if the character’s position matches the item’s position. 1 2 3 characterFoundItem : Model -> Bool characterFoundItem model = model.characterPositionX == model.itemPositionX This seems like a good idea initially, but it uncovers some limitations of our current approach. To see this in action, let’s temporarily update the viewItem function to account for our new characterFoundItem function. If the character has found the item, we’ll simply return an empty svg element to effectively hide the coin image. Otherwise, we’ll continue showing the item if the character has not found the item. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 viewItem : Model -> Svg Msg viewItem model = case characterFoundItem model of True -> svg [] [] False -> image [ xlinkHref "/images/coin.svg" , x (toString model.itemPositionX) , y (toString model.itemPositionY) , width "20" , height "20" ] [] If you try this out in the browser, you’ll see that it roughly accomplishes our goal of having the character be able to “find” the item and have it disappear. But you’ll notice there’s a small problem where the character needs to arrive at a particular position to find the item. 232 Adding Interaction Item Found Successfully 233 Adding Interaction Item Found Issue We could spend time looking for an ideal long-term fix for this issue, but for now let’s keep in mind that our current goal is to just make the game playable and to track the player’s score. So let’s find a workable solution that will involve giving the item a range instead of an exact position. And we’ll use this opportunity to learn to use a few new functions from the List80 module. Instead of using the exact model.itemPositionX value like we did above, we want to add a lower and upper bound for where the character should be able to find the item. We’ll use a let expression inside our characterFoundItem function to set values for the approximateItemLowerBound and approximateItemUpperBound. Then we’ll use the List.range81 function to create a range of numbers where the character can discover the item. After we create a range of values where our character can find the item, we use the List.member82 function to determine whether or not the character position is currently somewhere inside the item’s range. Let’s update our characterFoundItem function to see how this works: 80 http://package.elm-lang.org/packages/elm-lang/core/latest/List http://package.elm-lang.org/packages/elm-lang/core/latest/List#range 82 http://package.elm-lang.org/packages/elm-lang/core/latest/List#member 81 Adding Interaction 1 2 3 4 5 6 7 8 9 10 11 12 13 234 characterFoundItem : Model -> Bool characterFoundItem model = let approximateItemLowerBound = model.itemPositionX - 35 approximateItemUpperBound = model.itemPositionX approximateItemRange = List.range approximateItemLowerBound approximateItemUpperBound in List.member model.characterPositionX approximateItemRange This is generally not great programming practice to use a “magic number” value like 35 here, which is just a rough approximation of where the character position meets the item position. But tinkering with these values in the browser looks like it’s just good enough to keep moving since the character is able to discover the item at the correct position, and should improve our gameplay until we can find a better approach. Spawning Items In order to animate our scene, we’ll need to start by importing the Elm AnimationFrame83 package. From the command line, let’s switch to the assets folder and run the following command: 1 $ elm-package install elm-lang/animation-frame After agreeing to install the package by entering the Y key, here’s the output we should see: 1 2 3 4 5 6 7 8 9 10 $ elm-package install elm-lang/animation-frame To install elm-lang/animation-frame I would like to add the following dependency to elm-package.json: "elm-lang/animation-frame": "1.0.1 <= v < 2.0.0" May I add that to elm-package.json for you? [Y/n] Some new packages are needed. Here is the upgrade plan. 83 http://package.elm-lang.org/packages/elm-lang/animation-frame/latest/AnimationFrame Adding Interaction 11 12 13 14 15 16 17 18 19 235 Install: elm-lang/animation-frame 1.0.1 Do you approve of this plan? [Y/n] Starting downloads... � elm-lang/animation-frame 1.0.1 Packages configured successfully! We now have mechanisms for moving our character around the screen, and we managed to add some initial code for the character to find items. But we don’t just want to hide the item when the character finds it. We’d like to create new coins in new locations, and this is an opportunity to start making our game come to life. Let’s get started by importing a handful of new packages that we’ll need for our game at the top of our file. We’ll import AnimationFrame84 , Random85 , and Time86 (note that I tend to sort my imports in alphabetical order): 1 2 3 4 5 6 7 8 9 module Platformer exposing (..) import import import import import import import AnimationFrame exposing (diffs) Html exposing (Html, div) Keyboard exposing (KeyCode, downs) Random Svg exposing (..) Svg.Attributes exposing (..) Time exposing (Time) The AnimationFrame library will be helpful for our game, because it enables us to render smooth animations and subscribe to differences over time. We can start by updating our subscriptions function to use the diffs87 function from the AnimationFrame library, and we’ll pass a new message that we’ll create momentarily. Update subscriptions with the following: 84 http://package.elm-lang.org/packages/elm-lang/animation-frame/latest/AnimationFrame http://package.elm-lang.org/packages/elm-lang/core/latest/Random 86 http://package.elm-lang.org/packages/elm-lang/core/latest/Time 87 http://package.elm-lang.org/packages/elm-lang/animation-frame/latest/AnimationFrame#diffs 85 Adding Interaction 1 2 3 4 5 6 236 subscriptions : Model -> Sub Msg subscriptions model = Sub.batch [ downs KeyDown , diffs TimeUpdate ] This means we’ll need to add a new TimeUpdate message along with a change to our update function: 1 2 3 4 5 6 7 8 9 10 11 12 13 type Msg = NoOp | KeyDown KeyCode | TimeUpdate Time update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of -- ... TimeUpdate time -> ( model, Cmd.none ) This may not seem like a big deal, but it is. We now have the ability to change things over time in our game, so we could have items and enemies moving around as time moves forward. Let’s update our TimeUpdate message to perform some action. We want to change the position of our item when our character finds it, so let’s use our characterFoundItem function to make changes to the model: 1 2 3 4 5 TimeUpdate time -> if characterFoundItem model then ( { model | itemPositionX = model.itemPositionX - 100 }, Cmd.none ) else ( model, Cmd.none ) Now that we’re using our characterFoundItem condition in the update function, we can simplify the viewItem function we had temporarily changed before: 237 Adding Interaction 1 2 3 4 5 6 7 8 9 10 viewItem : Model -> Svg Msg viewItem model = image [ xlinkHref "/images/coin.svg" , x (toString model.itemPositionX) , y (toString model.itemPositionY) , width "20" , height "20" ] [] This basically allows the player to move the character to the item’s location, and it will give the appearance that a new coin is being “spawned” in a new location 100 pixels to the left. Item Shifting Position After Found Working with Randomness Instead of manually moving the coin to the left, let’s take a look at the Random library to move it to a random new location on the x-axis. Adding Interaction 238 To accomplish this, we’ll start by changing our TimeUpdate message, and then we’ll add a new SetNewItemPositionX message to change the position of the item in the model. First, let’s change our TimeUpdate message to remove the manual shifting of the coin, and replace it with a new random number generator: 1 2 3 4 5 TimeUpdate time -> if characterFoundItem model then ( model, Random.generate SetNewItemPositionX (Random.int 50 500) ) else ( model, Cmd.none ) We’re using two different functions from the Random library here. We’ll use Random.int, which takes two integer values and gives us a random number in between those values. Then we use the Random.generate function, which takes another message (which we’ll use to update the model) along with the random integer we’re creating. It’s admittedly confusing, but keep in mind that generating a random number is actually an effect, because a function that returns different values depending on the inputs is by nature impure. Working with random numbers this way allows us to have managed effects and make sure they don’t wreak havoc on our application (we’ll learn more about this elsewhere in the book so don’t worry too much for now). Now that we have a new value, we can use it to update the model with the SetNewItemPositionX message. Here are the changes in context with the message types and full update function: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type Msg = NoOp | KeyDown KeyCode | SetNewItemPositionX Int | TimeUpdate Time update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of -- ... TimeUpdate time -> if characterFoundItem model then ( model, Random.generate SetNewItemPositionX (Random.int 50 500)\ ) else ( model, Cmd.none ) 239 Adding Interaction 20 21 SetNewItemPositionX newPositionX -> ( { model | itemPositionX = newPositionX }, Cmd.none ) Item Shifting to Random Position After Found Summary These may not be the most fun game mechanics ever, but we’ve come a long way towards building our first game. This is a good stopping point to reflect on what we’ve accomplished so far. We learned about Elm subscriptions and handling keyboard input, allowing the player to adjust the position of the character on the screen. Then, we imported new libraries to allow our character to “collect” items and spawn new ones in different locations. What’s missing is the ability to track the number of items we’re collecting. In the next chapter, we’ll cover some basics for tracking game data and rendering it inside our game window. Displaying Game Data Let’s start thinking about how we want to display game data to our players. We’ll add text to our game window to indicate the player’s score and the number of items collected. We’ll also start working towards adding the concept of time to our game. Scoring with Item Collection Our player’s score and the number of items collected are values that will change, and any time we’re dealing with changing values it’s a good sign that we want to track those values in our model. Let’s get started by adding a playerScore to our model. We start with an initial value of 0, and the player will increase this value while collecting items. Now would also be a good time to add another field to track the number of items the character has collected. Let’s go ahead and add playerScore and the itemsCollected field to our model: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type alias Model = { characterDirection : Direction , characterPositionX : Int , characterPositionY : Int , itemPositionX : Int , itemPositionY : Int , itemsCollected : Int , playerScore : Int } initialModel : Model initialModel = { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 , itemPositionX = 500 , itemPositionY = 300 , itemsCollected = 0 , playerScore = 0 } Displaying Game Data 241 Rendering Text Data Now that we have some initial values for our player’s score and the number of items collected, we can add a score indicator to the view for our game. First, let’s start by adding a view helper function that we can use to set some standards for how we want our text to look. We’ll create a function called viewGameText that will allow us to render text in the game window, and we can use arguments to indicate the position and the string of text that we want to display: 1 2 3 4 5 6 7 8 9 10 viewGameText : Int -> Int -> String -> Svg Msg viewGameText positionX positionY str = Svg.text_ [ x (toString positionX) , y (toString positionY) , fontFamily "Courier" , fontWeight "bold" , fontSize "16" ] [ Svg.text str ] The first thing to note about this function is that we’re using Svg.text_, which is the SVG <text> element (whereas Svg.text is to create the actual string of text we’re going to display). Then we set some simple font attributes so our text will look nice. This function will allow us to move text around easily without having to duplicate all this code over and over again as we add new text indicators. We want to start by displaying our player’s score in the upper left area of the game window. To accomplish this, we’ll create a new viewGameScore function that takes in the current model and return the SVG element with the text we want to display. There may be a handful of unfamiliar things that we’re not accustomed to seeing here, but what we’re accomplishing is fairly straightforward. We want to start with the playerScore integer value from the model (which starts out with a value of 0). We use a let expression to convert this into a string with the toString function since we need to display it as text in our game window. Then, we use the String.padLeft function from the String module to display leading zero characters in our score. This isn’t strictly necessary, but helps make our score display look a little nicer. Lastly, we take the currentScore value from our let expression, and we put that inside a group of viewGameText functions that we’ll use to display everything. As for how the position values were determined, it was mainly just tinkering with those numbers after adding this to the page and experimenting with what looks good. Here’s the viewGameScore function: Displaying Game Data 1 2 3 4 5 6 7 8 9 10 11 12 242 viewGameScore : Model -> Svg Msg viewGameScore model = let currentScore = model.playerScore |> toString |> String.padLeft 5 '0' in Svg.svg [] [ viewGameText 25 25 "SCORE" , viewGameText 25 40 currentScore ] We can call this new function at the bottom of the viewGame function, and we should be able to see the score rendered in the browser: 1 2 3 4 5 6 7 8 9 10 viewGame : Model -> Svg Msg viewGame model = svg [ version "1.1", width "600", height "400" ] [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter model , viewItem model , viewGameScore model ] 243 Displaying Game Data Displaying Score Text Displaying Items Collected We’ll take a similar approach to show the number of coins the character has collected. Similar to our viewGameScore function, we’ll create a new viewItemsCollected function that takes in the model and returns the SVG element we’re looking to render for the player. We start out with the model.itemsCollected value (initialized to 0), and then we convert it to a string that we’ll display in the upper center location of the game window. We use an SVG image element to display a small image of the item, and then we’ll show the number of items in text alongside it. Here’s the full viewItemsCollected function: Displaying Game Data 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 244 viewItemsCollected : Model -> Svg Msg viewItemsCollected model = let currentItemCount = model.itemsCollected |> toString |> String.padLeft 3 '0' in Svg.svg [] [ image [ xlinkHref "/images/coin.svg" , x "275" , y "18" , width "15" , height "15" ] [] , viewGameText 300 30 ("x " ++ currentItemCount) ] Now we can add this to our viewGame function to see it rendered to the game screen. I also tinkered with the positioning for this one in hopes of making it look nice for players to see. 1 2 3 4 5 6 7 8 9 10 11 viewGame : Model -> Svg Msg viewGame model = svg [ version "1.1", width "600", height "400" ] [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter model , viewItem model , viewGameScore model , viewItemsCollected model ] 245 Displaying Game Data Displaying the Item Count Displaying Time Let’s take the same approach so we can add a timer to our game and give our players a sense of urgency. In the upper right corner of the game window, we’ll display the time remaining. The first step will be to add a new field to our model like we did previously, and we’ll call this one timeRemaining: 1 2 3 4 5 6 7 8 9 10 11 type alias Model = { characterDirection : Direction , characterPositionX : Int , characterPositionY : Int , itemPositionX : Int , itemPositionY : Int , itemsCollected : Int , playerScore : Int , timeRemaining : Int } Displaying Game Data 12 13 14 15 16 17 18 19 20 21 22 23 246 initialModel : Model initialModel = { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 , itemPositionX = 500 , itemPositionY = 300 , itemsCollected = 0 , playerScore = 0 , timeRemaining = 0 } Then, we’ll add another function to our view called viewGameTime that will convert the time in seconds to a string so we can render it. 1 2 3 4 5 6 7 8 9 10 11 12 viewGameTime : Model -> Svg Msg viewGameTime model = let currentTime = model.timeRemaining |> toString |> String.padLeft 4 '0' in Svg.svg [] [ viewGameText 525 25 "TIME" , viewGameText 525 40 currentTime ] Lastly, we can add it to the bottom of our viewGame function to see it rendered in the browser: 1 2 3 4 5 6 7 8 9 10 11 12 viewGame : Model -> Svg Msg viewGame model = svg [ version "1.1", width "600", height "400" ] [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter model , viewItem model , viewGameScore model , viewItemsCollected model , viewGameTime model ] 247 Displaying Game Data Displaying Time Remaining Keep in mind that we’ve managed to add text content to our game window that will allow players to see changes in game data, but these fields don’t actually reflect the score, item collection count, and time yet. Let’s take care of that next. Updating the Player Score Thankfully our game mechanics are simple, and we’ve already laid the foundation for how we’ll handle incrementing the player’s item count and score. In the current version of our game, we’re using the characterFoundItem function to determine when the character has stumbled upon a coin. And that gets triggered in our TimeUpdate message so we can spawn a new coin. We can use this existing feature to start updating our model. When the character arrives at a coin, we want to increment our itemsCollected field by a value of 1, and we’ll award 100 points to the playerScore field at the same time. The scoring is arbitrary, but in the future we could always add items with different scoring values. Let’s adjust the code in our TimeUpdate message. The syntax here will look a little unfamiliar since it’s broken up on different lines, but it’s the same record update syntax we’ve been using to update fields in the model. 248 Displaying Game Data 1 2 3 4 5 6 7 8 9 10 TimeUpdate time -> if characterFoundItem model then ( { model | itemsCollected = model.itemsCollected + 1 , playerScore = model.playerScore + 100 } , Random.generate SetNewItemPositionX (Random.int 50 500) ) else ( model, Cmd.none ) Now when the characterFoundItem function returns True, we’re not only generating a random integer to spawn a new coin item, we’re also incrementing the itemsCollected value and the playerScore value simultaneously. Check it out in the browser and it looks like it works! Updating Player Score and Item Count Implementing a Countdown Timer We have our player score and item counter working. Let’s take a look at how we can add a countdown timer to our game. This part will involve updating several pieces of our application, but thankfully they’re all simple changes. Displaying Game Data 249 Below our TimeUpdate message, let’s add a new one called CountdownTimer with the following: 1 2 3 4 5 6 type Msg = NoOp | CountdownTimer Time | KeyDown KeyCode | TimeUpdate Time | SetNewItemPositionX Int Then, in the update function, we’ll set up a new case below the TimeUpdate with the following code (we’re not changing the model yet, we’re just setting up our message for now so we can use it momentarily). 1 2 CountdownTimer time -> ( model, Cmd.none ) Now let’s go ahead and import a few new functions from the Time library that we’ll want to use so we can subscribe to time and track each passing second. Change the Time import at the top of the file to look like this: 1 import Time exposing (Time, every, second) And now we can update our subscriptions function to trigger the CountdownTimer message that we created for every second that passes: 1 2 3 4 5 6 7 subscriptions : Model -> Sub Msg subscriptions model = Sub.batch [ downs KeyDown , diffs TimeUpdate , every second CountdownTimer ] Incorporating Time Now that we’re subscribing to time with every passing second, we can think think about how we want to add this to our game. Let’s try thinking of our game as having small levels where players have to collect ten coins in ten seconds to advance. We’ll start by updating our initialModel so that the timeRemaining field starts with an initial value of 10: Displaying Game Data 1 2 3 4 5 6 7 8 9 10 11 250 initialModel : Model initialModel = { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 , itemPositionX = 500 , itemPositionY = 300 , itemsCollected = 0 , playerScore = 0 , timeRemaining = 0 } And now in the update function we can subtract one second from the timer for as long as the timeRemaining field has a value of more than zero: 1 2 3 4 5 CountdownTimer time -> if model.timeRemaining > 0 then ( { model | timeRemaining = model.timeRemaining - 1 }, Cmd.none ) else ( model, Cmd.none ) Granted, nothing will happen when the timer counts down to zero, but it looks like it should be working at this point in terms of counting from 10 down to 0. 251 Displaying Game Data Working Timer Display Summary We managed to accomplish our goal of displaying game data in this chapter. Our game is inching its way closer to being fun to play, but we haven’t created much structure for our game yet. In the next chapter, we’ll start handling different states for our game and working towards improving gameplay. Handling Game States This isn’t necessarily a book about game programming, so we won’t have time to delve too deeply into the topic of game design. But we still want our minigames to be fun, and one way to do this is to be thoughtful about gameplay. Game State Let’s begin by thinking about how we want our game to start and how we want it to end. It looks like our game is going to be a simple race against the clock to collect items. We don’t have some amazing plot to add yet, because we really want to focus on the core gameplay being fun. The starting state should be pretty straightforward. We just want a simple text screen that appears with basic instructions, and an indicator of which key to press to start the game. We’ll also want to add states for when the game is currently being played, a success state for when the player wins, and a game over screen. Union Types This is a great opportunity to talk about union types in Elm. Types can be a helpful way to think about something that has a limited set of possible states. In our case, we want to create a GameState type that handles all the scenarios we mentioned above. You can add this type right above the Model type alias: 1 type GameState = StartScreen | Playing | Success | GameOver I like to think of union types in this way on a single line. We’re creating a GameState type that has four possibilities, and it’s easy to reason about. Once the code gets formatted, it should look like this instead: 1 2 3 4 5 type GameState = StartScreen | Playing | Success | GameOver We can use this new type in our model to indicate the game’s current state. We’ll initialize the state to the game’s StartScreen, which we’ll create soon. Handling Game States 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 253 type alias Model = { characterDirection : Direction , characterPositionX : Int , characterPositionY : Int , gameState : GameState , itemPositionX : Int , itemPositionY : Int , itemsCollected : Int , playerScore : Int , timeRemaining : Int } initialModel : Model initialModel = { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 , gameState = StartScreen , itemPositionX = 500 , itemPositionY = 300 , itemsCollected = 0 , playerScore = 0 , timeRemaining = 10 } Adjusting the View Now we want to update our viewGame function to account for the different game states that we’ve created. But before we make changes to that function, let’s create a new function called viewGameState below it that will handle our cases. The idea is that we’ll take in the model as an argument, and then we’ll display a list of SVG content based on the current state of the game. Handling Game States 1 2 3 4 5 6 7 8 9 10 11 12 13 14 254 viewGameState : Model -> List (Svg Msg) viewGameState model = case model.gameState of StartScreen -> [] Playing -> [] Success -> [] GameOver -> [] We started with empty lists for each of these cases, but let’s go ahead and fill in the Playing state since we already had the content for that one. While users are actively playing the game, we’ll want them to be able to see all the relevant view functions we created previously: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 viewGameState : Model -> List (Svg Msg) viewGameState model = case model.gameState of StartScreen -> [] Playing -> [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter model , viewItem model , viewGameScore model , viewItemsCollected model , viewGameTime model ] Success -> [] GameOver -> [] Handling Game States 255 Now we can update our viewGame function to reference this new function that we just created. This might seem a bit confusing at first, but the idea is that we’re keeping the main svg element for our game, and then we’re using the viewGameState function to populate it with elements that are relevant to the current state. Here’s the new viewGame function: 1 2 3 4 viewGame : Model -> Svg Msg viewGame model = svg [ version "1.1", width "600", height "400" ] (viewGameState model) Starting Screen For the start screen, we’ll still show our basic game window with the sky, the ground, the character, and the item. And we also want to display some text to the player to give an idea of what to do. Let’s add a viewStartScreenText function that will display some introductory text: 1 2 3 4 5 6 viewStartScreenText : Svg Msg viewStartScreenText = Svg.svg [] [ viewGameText 140 160 "Collect ten coins in ten seconds!" , viewGameText 140 180 "Press the SPACE BAR key to start." ] Inside the viewGameState function, we’ll add the contents of our StartScreen state: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 viewGameState : Model -> List (Svg Msg) viewGameState model = case model.gameState of StartScreen -> [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter model , viewItem model , viewStartScreenText ] Playing -> -- ... 256 Handling Game States 16 17 18 19 20 Success -> -- ... GameOver -> -- ... The structure should be getting a little clearer now. We’re going to keep using the viewGameState function as a way to selectively show different components of the game. In this case, we want players to start with some brief instructions, and they’ll be able to see the character and the item they’ll be pursuing. Start Screen Space Bar to Start The instructions mention using the space bar key to start the game, so let’s add that feature now. It will also be useful as a way to reset to default values when we need to. We already have a KeyDown message that we’ve been using to track keyboard input, so we can use that to handle space bar key presses with 32 as the key code. Update the contents of the KeyDown action with the following: Handling Game States 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 257 KeyDown keyCode -> case keyCode of 32 -> ( { model | gameState = Playing } , Cmd.none ) 37 -> ( { model | characterDirection = Left , characterPositionX = model.characterPositionX - 15 } , Cmd.none ) 39 -> ( { model | characterDirection = Right , characterPositionX = model.characterPositionX + 15 } , Cmd.none ) _ -> ( model, Cmd.none ) Clean Starting State If you tried things out in the browser, you might have noticed some issues with our approach so far. The space bar works to begin our game, but the timer starts on the StartScreen state instead of the Playing state. And players are currently able to move the character around and collect items before the Playing state is set, which could result in a confusing game experience. Let’s restrict character motion until the gameState is set to Playing. We can handle this in the KeyDown section of our update function. We’ll add a conditional to check that we’re in the Playing state before the left and right arrow keys become usable. Handling Game States 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 258 KeyDown keyCode -> case keyCode of 32 -> ( { model | gameState = Playing } , Cmd.none ) 37 -> if model.gameState == Playing then ( { model | characterDirection = Left , characterPositionX = model.characterPositionX - 15 } , Cmd.none ) else ( model, Cmd.none ) 39 -> if model.gameState == Playing then ( { model | characterDirection = Right , characterPositionX = model.characterPositionX + 15 } , Cmd.none ) else ( model, Cmd.none ) _ -> ( model, Cmd.none ) We also don’t want our timer to start counting down until our players are in the active Playing state. Let’s update the conditional check in our CountdownTimer section: Handling Game States 1 2 3 4 5 259 CountdownTimer time -> if model.gameState == Playing && model.timeRemaining > 0 then ( { model | timeRemaining = model.timeRemaining - 1 }, Cmd.none ) else ( model, Cmd.none ) Success State If a player has managed to collect ten coins before the time has expired, we can consider the level completed and display a success message. Let’s create a new function called viewSuccessScreenText with the following content: 1 2 3 4 5 viewSuccessScreenText : Svg Msg viewSuccessScreenText = Svg.svg [] [ viewGameText 260 180 "Success!" ] It would be fun to add some SVG fireworks or animations here for the game’s success state. If you have time, feel free to get creative with the SVG in our game, but for now we’ll just add some text like we did for the starting screen. Let’s update the viewGameState function with the following: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 viewGameState : Model -> List (Svg Msg) viewGameState model = case model.gameState of StartScreen -> -- ... Playing -> -- ... Success -> [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter model , viewItem model , viewSuccessScreenText ] Handling Game States 18 19 20 260 GameOver -> -- ... In order to trigger the success screen, we’ll need to create another condition in our TimeUpdate for when the user’s score arrives at a value of 10. Inside the update function, adapt the code with the following: 1 2 3 4 5 6 7 8 9 10 11 12 TimeUpdate time -> if characterFoundItem model then ( { model | itemsCollected = model.itemsCollected + 1 , playerScore = model.playerScore + 100 } , Random.generate SetNewItemPositionX (Random.int 50 500) ) else if model.itemsCollected >= 10 then ( { model | gameState = Success }, Cmd.none ) else ( model, Cmd.none ) Restarting We also want our players to be able to restart the game without having to refresh the page in the browser. Let’s update the viewSuccessScreenText function we just created to include some more text about restarting the game: 1 2 3 4 5 6 viewSuccessScreenText : Svg Msg viewSuccessScreenText = Svg.svg [] [ viewGameText 260 160 "Success!" , viewGameText 140 180 "Press the SPACE BAR key to restart." ] And now we can update the space bar case in our update function so that users can restart the game from the Success state. In fact, we can use this opportunity to reset the values in the model to their initial values when we restart the game. If the user isn't currently in the Playing‘ state, they can use the space bar to start the game from a clean state: Handling Game States 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 261 KeyDown keyCode -> case keyCode of 32 -> if model.gameState /= Playing then ( { model | characterDirection = Right , characterPositionX = 50 , itemsCollected = 0 , gameState = Playing , playerScore = 0 , timeRemaining = 10 } , Cmd.none ) else ( model, Cmd.none ) -- ... At this point, we should have working states for the start screen, the active playing state, and the success screen. 262 Handling Game States Success Screen Game Over State We still haven’t figured out what to do when the timer reaches zero and the player hasn’t collected enough coins. In this case, we’ll take a similar approach to the one we used for the success state in that we want to show some “Game Over” text along with an option for the player to restart the game. We’ll start with a simple viewGameOverScreenText function for the text we want to display: 1 2 3 4 5 6 viewGameOverScreenText viewGameOverScreenText Svg.svg [] [ viewGameText , viewGameText ] : Svg Msg = 260 160 "Game Over" 140 180 "Press the SPACE BAR key to restart." Now we can finish up our viewGameState function. We’re only updating the GameOver case, but the full code is posted here for a review of what we’re selectively displaying for each state. Handling Game States 1 2 3 4 5 6 7 8 9 10 11 12 13 263 viewGameState : Model -> List (Svg Msg) viewGameState model = case model.gameState of -- ... GameOver -> [ viewGameWindow , viewGameSky , viewGameGround , viewCharacter model , viewItem model , viewGameOverScreenText ] We’ll also need to add another condition for when the timer reaches 0 and the player has collected less than 10 coins. Let’s update the TimeUpdate message to set the gameState to GameOver when those conditions arise during gameplay: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 TimeUpdate time -> if characterFoundItem model then ( { model | itemsCollected = model.itemsCollected + 1 , playerScore = model.playerScore + 100 } , Random.generate SetNewItemPositionX (Random.int 50 500) ) else if model.itemsCollected >= 10 then ( { model | gameState = Success }, Cmd.none ) else if model.itemsCollected < 10 && model.timeRemaining == 0 then ( { model | gameState = GameOver }, Cmd.none ) else ( model, Cmd.none ) Users are now able to reach the GameOver screen when time runs out, and they can restart the game by pressing the space bar key. 264 Handling Game States Game Over Screen Summary Our game is starting to take shape! We now have working game states, so players can see a start screen, play the game, view a success state, and also restart from the game over screen. Ideally we’d like to add a lot more features for our game. But now that we have a game with a working scoring mechanism, let’s switch gears and work on integrating our game with the Phoenix back-end. Phoenix Channel Setup We have our game platform up and running, where users can sign in and play a simple Elm game that tracks a score. Now, let’s work towards syncing the Elm front-end of our application with the Phoenix back-end. We’ll learn about Phoenix channels with the goal of being able to communicate the score from games back to a player’s account in real-time. Channels Essentially, Phoenix channels give us a way to send and receive messages. A chatroom application is a common example of how channels work. When users enter the chatroom, they “join” the channel. Then, we can “broadcast” the chat messages to all users that have joined the channel. We’re going to use Phoenix channels for a similar purpose. We’re going to take the playerScore field that we’re tracking in our Elm application, and we’re going to broadcast that value to other users so all players can see scores for a particular game being tracked in real-time. We’ll also take a look at how to store the scores as records in our database using the gameplays table we configured earlier in the book. Score Channel To get started, we can create a file for our new channel in the lib/platform_web/channels folder. Let’s use score_channel.ex as the filename, and add the following content: 1 2 3 defmodule PlatformWeb.ScoreChannel do use PlatformWeb, :channel end To get our new channel up and running, let’s open the user_socket.ex file in the same lib/platform_web/channels folder. At the top, we’ll see comments that show how to add a new channel. Phoenix Channel Setup 1 2 3 4 5 6 7 8 266 defmodule PlatformWeb.UserSocket do use Phoenix.Socket ## Channels # channel "room:*", PlatformWeb.RoomChannel # ... end We’ll replace the commented example with the channel we’re going to be working with. We’re setting things up to work with the PlatformWeb.ScoreChannel that we just created. The "score:*" part refers to “topic” and “subtopic”. That means we can use "score:platformer" to track scores for our Platformer game, or something like "score:racer" to track scores for a different game. The "*" character means that we’re joining the "score" topic and matching any subtopics. 1 2 3 4 5 6 7 8 defmodule PlatformWeb.UserSocket do use Phoenix.Socket ## Channels channel "score:*", PlatformWeb.ScoreChannel # ... end Joining the Channel Let’s add a join/3 function to the score_channel.ex file. This function will take three arguments: • The Topic: We’re joining "score:platformer" to track scores for our Platformer game. • The Payload: We’re using _payload to ignore this for now, but this will contain the data that we pass over the socket. • The Socket: This is the WebSocket connection that we return in the body of the function. Phoenix Channel Setup 1 2 3 4 5 6 7 267 defmodule PlatformWeb.ScoreChannel do use PlatformWeb, :channel def join("score:platformer", _payload, socket) do {:ok, socket} end end Now, let’s add another function that will allow us to handle incoming messages from the client. We’ll use the handle_in/3 function, and we’ll listen for a "save_score" message to trigger it. 1 2 3 4 5 6 7 8 9 10 11 12 defmodule PlatformWeb.ScoreChannel do use PlatformWeb, :channel def join("score:platformer", _payload, socket) do {:ok, socket} end def handle_in("save_score", payload, socket) do broadcast(socket, "save_score", payload) {:noreply, socket} end end This will allow us to listen for a "save_score" message that we’ll send from our Elm client. Inside the handle_in/3 function, we use broadcast/388 , which will relay the results to all players on the channel. We haven’t configured our front-end to work with the channel yet, but we’ve managed to take care of the initial channel setup on the back-end. elm-phoenix-socket While channel features come bundled with the Phoenix framework, but we’ll need to import a new library on the Elm side. To enable communication between the front-end and back-end, let’s use elm-phoenix-socket89 . To get started, let’s move to the assets folder in our project, and run the following command to install the package: 88 89 https://hexdocs.pm/phoenix/Phoenix.Channel.html#broadcast/3 https://github.com/fbonetti/elm-phoenix-socket Phoenix Channel Setup 1 268 $ elm-package install fbonetti/elm-phoenix-socket This will also import the elm-lang/websocket package, and we should see the following output: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ elm-package install fbonetti/elm-phoenix-socket To install fbonetti/elm-phoenix-socket I would like to add the following dependency to elm-package.json: "fbonetti/elm-phoenix-socket": "2.2.0 <= v < 3.0.0" May I add that to elm-package.json for you? [Y/n] Y Install: elm-lang/websocket 1.0.2 fbonetti/elm-phoenix-socket 2.2.0 Do you approve of this plan? [Y/n] Y Starting downloads... � elm-lang/websocket 1.0.2 � fbonetti/elm-phoenix-socket 2.2.0 Packages configured successfully! Configuring elm-phoenix-socket Now, we can work through the elm-phoenix-socket README90 to configure everything on the Elm side of our application. We’ll start by importing the necessary modules. Let’s update the imports at the top of our Platformer.elm file to include three new Phoenix modules: 1 2 3 4 5 6 7 8 9 module Platformer exposing (..) import import import import import import import 90 AnimationFrame exposing (diffs) Html exposing (Html, div) Keyboard exposing (KeyCode, downs) Phoenix.Channel Phoenix.Push Phoenix.Socket Random https://github.com/fbonetti/elm-phoenix-socket/blob/master/README.md Phoenix Channel Setup 10 11 12 269 import Svg exposing (..) import Svg.Attributes exposing (..) import Time exposing (Time, every, second) Next, we can add a phxSocket field to our model. We’ll update our Model type first, and then provide an initial value in our initialModel that points to a new function we’ll call initialSocket. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 type alias Model = { characterDirection : Direction , characterPositionX : Int , characterPositionY : Int , gameState : GameState , itemPositionX : Int , itemPositionY : Int , itemsCollected : Int , phxSocket : Phoenix.Socket.Socket Msg , playerScore : Int , timeRemaining : Int } initialModel : Model initialModel = { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 , gameState = StartScreen , itemPositionX = 500 , itemPositionY = 300 , itemsCollected = 0 , phxSocket = initialSocket , playerScore = 0 , timeRemaining = 10 } Here’s the new initialSocket function we can use to initialize the socket connection with Phoenix.Socket.init. When our Phoenix server is running, we’ll be able to use the devSocketServer value for the WebSocket connection that’s being served as the default from Phoenix. Phoenix Channel Setup 1 2 3 4 5 6 7 8 270 initialSocket : Phoenix.Socket.Socket Msg initialSocket = let devSocketServer = "ws://localhost:4000/socket/websocket" in Phoenix.Socket.init devSocketServer |> Phoenix.Socket.withDebug We also pipe the results to Phoenix.Socket.withDebug so we can take a look at the DevTools Console and inspect the data once we get things up and running. The Update Function Before we have a working socket connection, we’ll need to add a new message to our update section. Still following along with the elm-phoenix-socket README file, we see that we can create a new message type at the bottom to handle Phoenix socket messages with PhoenixMsg. 1 2 3 4 5 6 7 type Msg = NoOp | CountdownTimer Time | KeyDown KeyCode | PhoenixMsg (Phoenix.Socket.Msg Msg) | SetNewItemPositionX Int | TimeUpdate Time And we can add the following inside the case expression of our update function: 1 2 3 4 5 6 7 PhoenixMsg msg -> let ( phxSocket, phxCmd ) = Phoenix.Socket.update msg model.phxSocket in ( { model | phxSocket = phxSocket } , Cmd.map PhoenixMsg phxCmd ) This enables us to track changes in state using the phxSocket field in our model. Socket Subscription Lastly, we can add to our subscriptions function to subscribe to changes over time. 271 Phoenix Channel Setup 1 2 3 4 5 6 7 8 subscriptions : Model -> Sub Msg subscriptions model = Sub.batch [ downs KeyDown , diffs TimeUpdate , every second CountdownTimer , Phoenix.Socket.listen model.phxSocket PhoenixMsg ] At this point, we should have a working socket connection when we visit the http://0.0.0.0:4000/games/platform route in our application. You may need to restart your local Phoenix server to get things up and running, but if you load the page and wait a few moments, you should be able to see a “heartbeat” of WebSocket messages in the DevTools Console. Working Channel 272 Phoenix Channel Setup Using the DevTools If you’re not familiar with the Chrome DevTools, you should be able to press Command + Option + J on your keyboard to pull up the JavaScript Console that displays the messages we’re looking for. For now, our payload is empty since we haven’t explicitly sent any data yet, but we can tell that it’s working with the status = "ok" indicator and see that it changes over time because it increments the Just <number> indicator. This is what the messages should look like in the DevTools Console: 1 2 3 4 5 6 Phoenix message: "ok", response = Phoenix message: "ok", response = Phoenix message: "ok", response = { event = {} }, ref { event = {} }, ref { event = {} }, ref "phx_reply", topic = "phoenix", payload = { status = \ = Just 0 } "phx_reply", topic = "phoenix", payload = { status = \ = Just 1 } "phx_reply", topic = "phoenix", payload = { status = \ = Just 2 } Sending Data Over the Socket Now that we have an initial setup, we can start sending data. We want to send the score that we already have available in the Elm model over the socket to the Phoenix back-end. We can start by adding a new message to our update section. Add SaveScoreRequest to our Msg type: 1 2 3 4 5 6 7 8 type Msg = NoOp | CountdownTimer Time | KeyDown KeyCode | PhoenixMsg (Phoenix.Socket.Msg Msg) | SaveScoreRequest | SetNewItemPositionX Int | TimeUpdate Time Then, let’s add the following to our update function: 1 2 3 4 5 6 SaveScoreRequest -> let payload = Encode.object [ ( "player_score", Encode.int model.playerScore ) ] in ( model, Cmd.none ) We’ll need to import Elm’s JSON encoding package, so add this to the imports at the top of the file: Phoenix Channel Setup 1 273 import Json.Encode as Encode With SaveScoreRequest, we’re starting to construct a payload that we’ll use to send our Elm data. We take the playerScore that’s part of our Elm model, and we encode it as a JSON integer with Encode.int. Then, we wrap that up in a JSON object that we can use to send it to the Phoenix backend. Keep in mind that we’re still using (model, Cmd.none) so far, so we’re not actually pushing data over the socket yet. Phoenix.Push To continue, we’ll use the Phoenix.Push module that we imported at the top of our file. We want to initialize using the "score:platformer" channel that we created on the Phoenix side, and we’ll use the payload we constructed to send along the relevant data. We’ll also use the pipe operator to pass data along and handle the success and failure cases. 1 2 3 4 5 6 7 8 9 10 11 12 SaveScoreRequest -> let payload = Encode.object [ ( "player_score", Encode.int model.playerScore ) ] phxPush = Phoenix.Push.init "save_score" "score:platformer" |> Phoenix.Push.withPayload payload |> Phoenix.Push.onOk SaveScore |> Phoenix.Push.onError SaveScoreError in ( model, Cmd.none ) We’ll need to scaffold out new messages for SaveScore and SaveScoreError for the success and failure cases, respectively. We can add these to our Msg type first, and they’ll both take an Encode.Value as an argument: Phoenix Channel Setup 1 2 3 4 5 6 7 8 9 10 274 type Msg = NoOp | CountdownTimer Time | KeyDown KeyCode | PhoenixMsg (Phoenix.Socket.Msg Msg) | SaveScore Encode.Value | SaveScoreError Encode.Value | SaveScoreRequest | SetNewItemPositionX Int | TimeUpdate Time And we can add cases at the bottom of our update function to get our code back to a state with no errors, and we’re one step closer to connecting our front-end with our back-end. 1 2 3 4 5 6 SaveScore value -> ( model, Cmd.none ) SaveScoreError message -> Debug.log "Error sending score over socket." ( model, Cmd.none ) Executing the Push In our SaveScore case, we’re going to connect everything together by sending data over the phxSocket. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 SaveScoreRequest -> let payload = Encode.object [ ( "player_score", Encode.int model.playerScore ) ] phxPush = Phoenix.Push.init "save_score" "score:platformer" |> Phoenix.Push.withPayload payload |> Phoenix.Push.onOk SaveScore |> Phoenix.Push.onError SaveScoreError ( phxSocket, phxCmd ) = Phoenix.Socket.push phxPush model.phxSocket in ( { model | phxSocket = phxSocket } , Cmd.map PhoenixMsg phxCmd ) Phoenix Channel Setup 275 Joining the Score Channel Now we can join the "score:platformer" channel and broadcast the player’s score. Below our initialSocket function, let’s create a new function called initialChannel. 1 2 3 initialChannel : Phoenix.Channel.Channel msg initialChannel = Phoenix.Channel.init "score:platformer" We’re going to adjust the initialSocket function so that it returns a tuple. The first item in that tuple is what we’ll use for the phxSocket field in our initialModel, and then second item in the tuple will be used as the initial command in our init function. Here is the full code for our initialModel, initPhxSocket, and init functions: 1 2 3 4 5 6 7 8 9 10 initialSocket : ( Phoenix.Socket.Socket Msg, Cmd (Phoenix.Socket.Msg Msg) ) initialSocket = let devSocketServer = "ws://localhost:4000/socket/websocket" in Phoenix.Socket.init devSocketServer |> Phoenix.Socket.withDebug |> Phoenix.Socket.on "save_score" "score:platformer" SaveScore |> Phoenix.Socket.join initialChannel We can create two new functions to identify which parts of the tuple we need: 1 2 3 4 5 6 7 8 9 10 initialSocketJoin : Phoenix.Socket.Socket Msg initialSocketJoin = initialSocket |> Tuple.first initialSocketCommand : Cmd (Phoenix.Socket.Msg Msg) initialSocketCommand = initialSocket |> Tuple.second Now, we can set the phxSocket field in our initialModel to initialSocketJoin: Phoenix Channel Setup 1 2 3 4 5 6 7 8 9 10 11 12 13 276 initialModel : Model initialModel = { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 , gameState = StartScreen , itemPositionX = 500 , itemPositionY = 300 , itemsCollected = 0 , phxSocket = initialSocketJoin , playerScore = 0 , timeRemaining = 10 } And we can update our init function with the initialSocketCommand to get things up and running: 1 2 3 init : ( Model, Cmd Msg ) init = ( initialModel, Cmd.map PhoenixMsg initialSocketCommand ) This is a lot to process, but let’s keep moving for now so we can get things working, and we’ll work towards a deeper understanding as we gain more familiarity with the code we’re working with. Triggering SaveScore Finally, we just need to trigger the SaveScoreRequest message whenever we want to send our score over the socket. We’ll set it up so that we can click a button, and then check the DevTools console to view the payload that’s being sent over the socket. At the top of our file, let’s import the onClick functionality from Html.Events. While we’re here, we also want to make a quick change to the Html import so we can use the button element. Here are the Html imports: 1 2 import Html exposing (Html, button, div) import Html.Events exposing (onClick) Adding a Button to the View Now, we can create a new viewSaveScoreButton function and add it to our main view to trigger the SaveScoreRequest message. Phoenix Channel Setup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 277 view : Model -> Html Msg view model = div [] [ viewGame model , viewSaveScoreButton ] viewSaveScoreButton : Html Msg viewSaveScoreButton = div [] [ button [ onClick SaveScoreRequest , class "btn btn-primary" ] [ text "Save Score" ] ] Testing Out the Socket We’ve got everything configured, and we should be able to test out our working socket payload. Start the Phoenix server with mix phx.server and load the game in the browser. Collect a few coins to increment the player’s score, and then click the new “Save Score” button. The DevTools console will show an initial message when the page loads, and this let’s us know that our "score:platformer" topic was initialized with an "ok" status. 1 2 Phoenix message: { event = "phx_reply", topic = "score:platformer", payload = { \ status = "ok", response = {} }, ref = Just 0 } We also see a message triggered in the DevTools console when we click the score text. It shows that we triggered a "save_score" event, which means we can broadcast the data over the socket. This is the behavior we want, because we’ll want all the player scores to be visible and update in real-time on the Phoenix page where we list out all the players. In this example, you can see that we collected three coins for a score of 300, and that is reflected in the payload with { score = 300 }. 1 2 Phoenix message: { event = "save_score", topic = "score:platformer", payload = {\ player_score = 300 }, ref = Nothing } 278 Phoenix Channel Setup Working Socket Payload Summary We made it a long way in this chapter, but we still haven’t accomplished our goal of syncing data between the Elm front-end and Phoenix back-end. We created a new Phoenix channel, we installed elm-phoenix-socket, and we configured our game to send a payload. But we still need to handle the data that’s being sent from the front-end. Let’s take a look at these topics in the next chapter. Syncing Score Data We created our ScoreChannel and set up elm-phoenix-socket in the last chapter, but we haven’t really worked with Phoenix channels much outside of the initial configuration. In this chapter, we’ll take that data from our Elm front-end game, and find a good way to handle it with our Phoenix back-end. Planning Out Our Approach Our game is tracking a playerScore field on the Elm side as the character collects items. And we managed to configure elm-phoenix-socket in the last chapter, so we have the score being sent over the ScoreChannel as the payload value. But we’d like to display player score updates in real-time, and be able to save scores to the database as Gameplay records. We’ll use the “Save Score” button to save our player scores to the database, and to broadcast score changes to all players connected to the socket. When we create new new database records for the score, we’ll need to include the game_id, the player_id, and the player_score values. Then, we can work towards displaying recent player scores below the game. Updating our ScoreChannel Let’s make some changes to our lib/platform_web/channels/score_channel.ex file. This is what we have so far: 1 2 3 4 5 6 7 8 9 10 11 12 defmodule PlatformWeb.ScoreChannel do use PlatformWeb, :channel def join("score:platformer", _payload, socket) do {:ok, socket} end def handle_in("save_score", payload, socket) do broadcast(socket, "save_score", payload) {:noreply, socket} end end Instead of manually coding the name of the game in our topic, we’ll destructure the slug by pattern matching with the <> string concatenation operator. Syncing Score Data 1 2 3 280 def join("score:" <> game_slug, _payload, socket) do {:ok, socket} end Our Platformer.elm file is already set up to join the "score:platformer" topic. But this change means that we’ll be able to use this same channel for other games in the future, and the slug field will allow us to differentiate between the games. The reason this is important is that we need to know which game is being played and the current player in order to save their score. Tracking Data Over the Socket Let’s make some additional changes to the join/3 function we worked with in the last section. We’re going to learn how to assign values to the socket that we can use to work with data in our channel. Now, we can find the game that’s currently being played, and assign the game_id as a value we can work with in the socket. In our join/3 function, let’s make the following changes: 1 2 3 4 5 def join("score:" <> game_slug, _payload, socket) do game = Platform.Products.get_game_by_slug!(game_slug) socket = assign(socket, :game_id, game.id) {:ok, socket} end The Products.get_game_by_slug!/1 function is something we created in the Game Setup chapter. We can use it here to find the game record, and then find the game.id. Then, we use the assign(socket, :game_id, game.id) syntax to assign that value and make it accessible when working with our socket. In other words, we’ll be able to use socket.assigns.game_id to access this value in any of our handle functions now. This will be useful in the next section. Creating the Payload In order to save our score records to the database, we’ll also need to grab the player_score value. Let’s deconstruct that value from the payload argument in our handle_in/3 function. Syncing Score Data 1 2 3 4 5 281 def handle_in("save_score", %{"player_score" => player_score} = payload, socket)\ do broadcast(socket, "save_score", payload) {:noreply, socket} end We’re pattern matching the player_score from the payload so the value is accessible inside our function. Now, let’s construct our payload and create our new Gameplay record inside this function. We have the player_score value available, and the game_id is accessible in the socket. We’re also going to hard-code the player_id value (using 3 for the chrismccord account we’ve been seeing in all the screenshots) for now. We’ll take a look at socket authentication and tracking the current user in our channel soon. 1 2 3 4 5 6 7 8 9 10 11 12 def handle_in("save_score", %{"player_score" => player_score} = payload, socket)\ do payload = %{ player_score: player_score, game_id: socket.assigns.game_id, player_id: 3 } Platform.Products.create_gameplay(payload) broadcast(socket, "save_score", payload) {:noreply, socket} end Creating Gameplays Our channel features should now work using the create_gameplay/1 function from our lib/platform/products.ex file. To save new records in our "gameplays" table, we’ll be passing a game_id, a player_id, and a player_score to create_gameplay/1. 1 2 3 4 5 def create_gameplay(attrs \\ %{}) do %Gameplay{} |> Gameplay.changeset(attrs) |> Repo.insert() end 282 Syncing Score Data When we load our game in the browser, we should now be able to click the “Save Score” button and create records in the database. For the output and screenshot below, we collected a few coins and then clicked the “Save Score” button. We can see that the "save_score" message was triggered for the "score:platformer" topic. And the payload contains the data we set up in the handle_in/3 function from our ScoreChannel. We have the player_score field being tracked from the game, and we’re taking the game_id from the value we grabbed in the join/3 function. We can also see the hard-coded value of 3 that we set for the player_id field, and we’ll take care of socket authentication soon. 1 2 Phoenix message: { event = "save_score", topic = "score:platformer", payload = {\ player_score = 300, player_id = 3, game_id = 1 }, ref = Nothing } Working Channel Payload Syncing Score Data 283 Viewing Gameplay Records At this point, we should have a working button to save our player scores to the database. But we don’t have any way of viewing them yet. Let’s add a list of all the existing Gameplay records below the game. In our Platformer.elm file, we’ll start out by adding a new type alias for our Gameplays: 1 2 3 4 5 type alias Gameplay = { gameId : Int , playerId : Int , playerScore : Int } Then, we can update our Model and initialModel to start out with an empty list of Gameplay records: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 type alias Model = { characterDirection : Direction , characterPositionX : Int , characterPositionY : Int , gameplays : List Gameplay , gameState : GameState , itemPositionX : Int , itemPositionY : Int , itemsCollected : Int , phxSocket : Phoenix.Socket.Socket Msg , playerScore : Int , timeRemaining : Int } initialModel : Model initialModel = { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 , gameplays = [] , gameState = StartScreen , itemPositionX = 500 , itemPositionY = 300 , itemsCollected = 0 , phxSocket = initialSocketJoin Syncing Score Data 27 28 29 284 , playerScore = 0 , timeRemaining = 10 } Now, we can reuse some of the view functions we created in the Main.elm file to view the Gameplay records being shared over the socket. We’ll start out by updating the import statements at the top of the file for the Html modules. Keep in mind that we have to be pretty specific with the way we refer to our Html functions, because we did a global import for Svg that could cause collisions. Update the imports at the top of the file with the following: 1 2 import Html exposing (Html, button, div, li, span, strong, ul) import Html.Attributes We can now add our view functions below the viewSaveScoreButton we added previously. We’ll start with a viewGameplaysIndex function that allows us to render our model.gameplays if any exist: 1 2 3 4 5 6 7 8 viewGameplaysIndex : Model -> Html Msg viewGameplaysIndex model = if List.isEmpty model.gameplays then div [] [] else div [ Html.Attributes.class "players-index" ] [ viewGameplaysList model.gameplays ] Next, we’ll add a viewGameplaysList function that renders the panel component we got from Bootstrap. And we’ll use this to display our list of player scores: 1 2 3 4 5 6 7 viewGameplaysList : List Gameplay -> Html Msg viewGameplaysList gameplays = div [ Html.Attributes.class "players-list panel panel-info" ] [ div [ Html.Attributes.class "panel-heading" ] [ text "Player Scores" ] , ul [ Html.Attributes.class "list-group" ] (List.map viewGameplayItem g\ ameplays) ] Then, we can add our inidividual gameplays as list items with a viewGameplayItem function. Each gameplay record will be displayed with the playerId field and the corresponding playerScore: Syncing Score Data 1 2 3 4 5 6 7 285 viewGameplayItem : Gameplay -> Html Msg viewGameplayItem gameplay = li [ Html.Attributes.class "player-item list-group-item" ] [ strong [] [ text (toString gameplay.playerId) ] , span [ Html.Attributes.class "badge" ] [ text (toString gameplay.playe\ rScore) ] ] Finally, we can add this to our main view function: 1 2 3 4 5 6 7 view : Model -> Html Msg view model = div [] [ viewGame model , viewSaveScoreButton , viewGameplaysIndex model ] Our view is all set, but we won’t see anything rendering in the browser yet, because we still need to decode the gameplays into our Elm application. In other words, we’re successfully sending our gameplays over the socket when we click the “Save Score” button, but we’re not receiving score changes yet. Receiving and Rendering Score Changes To display gameplays, we’ll start by adding a new case to our update function. First, we’ll add the ReceiveScoreChanges type to our update messages: 1 2 3 4 5 6 7 8 9 10 11 type Msg = NoOp | CountdownTimer Time | KeyDown KeyCode | PhoenixMsg (Phoenix.Socket.Msg Msg) | ReceiveScoreChanges Encode.Value | SaveScore Encode.Value | SaveScoreError Encode.Value | SaveScoreRequest | SetNewItemPositionX Int | TimeUpdate Time We can now add the following case to decode score changes and append them to the model. When a gameplay is successfully decoded (with an Ok response), we’re taking that value and using the cons operator :: to add it to the list of gameplays stored in model.gameplays. Syncing Score Data 1 2 3 4 5 6 7 8 286 ReceiveScoreChanges raw -> case Decode.decodeValue gameplayDecoder raw of Ok scoreChange -> ( { model | gameplays = scoreChange :: model.gameplays }, Cmd.none ) Err message -> Debug.log "Error receiving score changes." ( model, Cmd.none ) Note that this will require us to add another import at the top of our file to go along with our JSON encoder: 1 2 import Json.Decode as Decode import Json.Encode as Encode Just like we did with our decoders in the Main.elm file, we’ll add a decoder for our Gameplay type: 1 2 3 4 5 6 gameplayDecoder : Decode.Decoder Gameplay gameplayDecoder = Decode.map3 Gameplay (Decode.field "game_id" Decode.int) (Decode.field "player_id" Decode.int) (Decode.field "player_score" Decode.int) We were previously able to construct a payload that we were sending over the socket with our score data using Json.Encode. Now, we’re also able to decode data that comes from the socket and add it to our Elm application. Displaying the Results We now have everything we need to display the scores that are being saved. To get this working, let’s update our initialSocket function with the ReceiveScoreChanges message so that we can start receiving score changes when the socket is initialized: Syncing Score Data 1 2 3 4 5 6 7 8 9 10 11 12 287 initialSocket : ( Phoenix.Socket.Socket Msg, Cmd (Phoenix.Socket.Msg Msg) ) initialSocket = let devSocketServer = "ws://localhost:4000/socket/websocket" in Phoenix.Socket.init devSocketServer |> Phoenix.Socket.withDebug |> Phoenix.Socket.on "save_score" "score:platformer" SaveScore |> Phoenix.Socket.on "save_score" "score:platformer" ReceiveScoreCha\ nges |> Phoenix.Socket.join initialChannel We should now be able to reload the game in the browser and try things out. We’ll move the character around to collect a few coins, and then click the “Save Score” button to send the data over the channel. Not only will we be creating database records for our gameplays, we’re also broadcasting the data over the socket and displaying it at the bottom of the page! 288 Syncing Score Data Successfully Rendering Channel Scores In the screenshot above, we can see that the player was able to successfully collect ten coins, click the “Save Score” button, and then see the score rendered on the page with the playerId and playerScore. Syncing Score Data 289 Summary In the last two chapters, we’ve managed to successfully configure our Phoenix channel, save records to the database, send data over the socket, and render the results. But one of the key features for our games will be to track scores for individual players. Remember that we hard-coded the player_id value in our ScoreChannel, so we’re storing all of our Gameplay records for a single player. In the next chapter, we’ll tackle socket authentication, which will allow us to track scores for different players as they join the channel. Socket Authentication In the previous chapters, we’ve gotten our Phoenix channel configured and it’s now up and running. We’re able to send data over the socket, but we currently don’t have a way to differentiate between players. In this chapter, we’re going to make some key changes to the way our Elixir and Elm applications are configured. It will be worth it at the end, becuase we’ll be able to have multiple players play a game at the same time, and they’ll all be able to see score changes getting broadcast over the socket to each other! Phoenix.Token In the Phoenix authentication chapter, we looked at how to handle user authentication for the players on our platform. For our channel features, we’ll be taking a similar approach, but we’ll use Phoenix.Token91 for authentication. There are several steps involved to getting this working, but at the end we’ll be able to access the current user over the socket. User Tokens Let’s open up our router.ex file in the lib/platform_web folder. We can create a new private function at the bottom of the file called put_user_token/2, which will check if there’s a user currently signed in. If the user is not authenticated, we simply return the connection with conn. Otherwise, we create a new token with Phoenix.Token.sign/492 and assign it to the connection so we can access it elsewhere. 91 92 https://hexdocs.pm/phoenix/Phoenix.Token.html https://hexdocs.pm/phoenix/Phoenix.Token.html#sign/4 Socket Authentication 1 2 3 4 5 6 7 8 291 defp put_user_token(conn, _) do if current_user = conn.assigns[:current_user] do token = Phoenix.Token.sign(conn, "user socket", current_user.id) assign(conn, :user_token, token) else conn end end At the top of the router.ex file, we’ll include this as a plug in our browser pipeline. 1 2 3 4 5 6 7 8 9 pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers plug PlatformWeb.PlayerAuthController, repo: Platform.Repo plug :put_user_token end Passing the Token to JavaScript To make this token available throughout our platform application, we’ll add it to the bottom of our layout template. Let’s open our lib/platform_web/templates/layout/app.html.eex file, and we’ll add a new <script> tag above the existing one in the <body>. 1 2 3 4 5 6 7 <!-- ... --> </div> <!-- /container --> <script>window.userToken = encodeURIComponent("<%= assigns[:user_token] %>")\ ;</script> <script src="<%= static_path(@conn, "/js/app.js") %>"></script> </body> </html> Note that the order of the <script> tags here is important. If you set the window.userToken in the wrong order, it won’t be available to the Elm application when we need it. This will take the token we created in our router and assigned to the conn, and it will encode it before storing it in the window.userToken variable. Now, when we have authenticated users accessing games, we’ll be able to use window.userToken to identify them. Socket Authentication 292 Verifying the Token Next, we’ll verify the token when user’s connect to the socket. Let’s open up the lib/platform_web/channels/user_socket.ex file and take a look. We’ll create a new clause for our connect/2 function that works when there’s a token present. 1 2 3 4 5 6 7 8 9 10 11 12 13 def connect(%{"token" => token}, socket) do case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do {:ok, current_user_id} -> socket = assign(socket, :player_id, current_user_id) {:ok, socket} {:error, _} -> :error end end def connect(_params, socket) do {:ok, socket} end With this code, we’re using the Phoenix.Token.verify/493 function to check the user’s token. Also, keep in mind that this is admittedly overwhelming at first, but the Phoenix documentation is really useful for checking out other examples. What’s happening here is that we’re deconstructing the token value when one is present, and when we successfully verify the token, we us that to assign the current_user_id to the player_id in the socket. We also fall back to the original connect/2 clause for anonymous users. This works out great, because we already assigned the game_id to the socket in our score_channel.ex file. And now we’re able to assign the player_id to the socket with the code above. Sending the Token to Elm This part can be a little tedious since we’re going to change the format of our Elm application. But it’s more tedious than difficult, and at the end we’ll be able to test out multiple users playing our game simultaneously. To send our user token to the Platformer Elm application, let’s open up the assets/js/app.js file. We’re going to update the line at the bottom to pass along the window.userToken: 93 https://hexdocs.pm/phoenix/Phoenix.Token.html#verify/4 Socket Authentication 1 2 3 4 5 6 7 8 293 // Elm import Elm from "./elm"; const elmContainer = document.querySelector("#elm-container"); const platformer = document.querySelector("#platformer"); if (elmContainer) Elm.Main.embed(elmContainer); if (platformer) Elm.Platformer.embed(platformer, { token: window.userToken }); If you’re still running the Phoenix server, you’ll notice that the Elm application for our game no longer works. Let’s open our assets/elm/Platformer.elm file and fix things up. ProgramWithFlags At the top of our Platformer.elm file, we’ve been using Html.program in our main function. We’re going to make a slight change to our application by converting it to programWithFlags and adding a new type alias above: 1 2 3 4 5 6 7 8 9 10 11 12 13 type alias Flags = { token : String } main : Program Flags Model Msg main = Html.programWithFlags { init = init , view = view , update = update , subscriptions = subscriptions } Flags allow JavaScript and Elm to communicate. When we’re embedding our Elm application on the page with JavaScript, we’re passing along some data that we’ll need to use. In this case, we’re sending our window.userToken value to the Elm application, where we’ll be able to access it as a token string. Configuring Flags Now, we just need to update our existing functions to work with our Flags. We’ll start by updating our init function with the following: Socket Authentication 1 2 3 294 init : Flags -> ( Model, Cmd Msg ) init flags = ( initialModel flags, Cmd.map PhoenixMsg (initialSocketCommand flags) ) We’ll also need to pass our flags through the initialModel as well: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 initialModel : Flags -> Model initialModel flags = { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 , gameplays = [] , gameState = StartScreen , itemPositionX = 500 , itemPositionY = 300 , itemsCollected = 0 , phxSocket = initialSocketJoin flags , playerScore = 0 , timeRemaining = 10 } Then, we can update our initialSocketJoin and initialSocketCommand function: 1 2 3 4 5 6 7 8 9 10 initialSocketJoin : Flags -> Phoenix.Socket.Socket Msg initialSocketJoin flags = initialSocket flags |> Tuple.first initialSocketCommand : Flags -> Cmd (Phoenix.Socket.Msg Msg) initialSocketCommand flags = initialSocket flags |> Tuple.second Lastly, we update our initialSocket function with the key changes. We’re going to take the value from flags.token, and append it to the URL for the WebSocket server. Socket Authentication 1 2 3 4 5 6 7 8 9 10 11 12 13 295 initialSocket : Flags -> ( Phoenix.Socket.Socket Msg, Cmd (Phoenix.Socket.Msg Ms\ g) ) initialSocket flags = let devSocketServer = "ws://localhost:4000/socket/websocket?token=" ++ flags.token in Phoenix.Socket.init devSocketServer |> Phoenix.Socket.withDebug |> Phoenix.Socket.on "save_score" "score:platformer" SaveScore |> Phoenix.Socket.on "save_score" "score:platformer" ReceiveScoreCha\ nges |> Phoenix.Socket.join initialChannel We’re done with the updates to our Elm application! Now, when players connect to the socket, the channel will be able to identify the current user by their token. Finishing the Score Channel Let’s go back to our score_channel.ex file in the lib/platform_web/channels folder, and we’ll remove the hard-coded value for our player_id. Update the handle_in/3 function with the following, which takes the player_id value from socket.assigns.player_id: 1 2 3 4 5 6 7 8 9 10 11 12 def handle_in("save_score", %{"player_score" => player_score} = payload, socket)\ do payload = %{ player_score: player_score, game_id: socket.assigns.game_id, player_id: socket.assigns.player_id } Platform.Products.create_gameplay(payload) broadcast(socket, "save_score", payload) {:noreply, socket} end This means we are now successfully constructing the payload with all the data we need for our scores! We’re tracking the player_score from our game, and the player_id and game_id are being assigned to the socket. 296 Socket Authentication We can finally reload our game in the browser and test things out. As a demonstration, we can open two separate incognito windows in Google Chrome, and authenticate with a different user in each window. In the screenshot below, we see that chrismccord is logged in on the left, and evancz is logged in on the right. Each player collects a few coins and saves their score, and the scores are displayed on each other’s screens in real-time! Multiplayer Score Syncing Summary We’ve finally managed to accomplish our goal of syncing our player scores over Phoenix channels. Our Phoenix back-end and Elm front-end are now able to talk to each other. We’re now able to identify the current users for our channel features. That means we could coneivably have dozens of users all playing a game at the same time, and the scores would all be tracked in real-time. In the next chapter, we’ll go over some of the features that didn’t make it into the v1 release for this book. Readers are encouraged to tinker with the foundation we’ve set, and there’s plenty of room to create new features! What’s Next? The original plan for this book included additional features and more minigame ideas. However, the priority shifted towards shipping a solid, stable v1.0 release instead of an exhaustive resource. Rather than having a dozen half-written and half-implemented features, the hope is that this release will be a stable foundational resource. But what features do we have to look forward to for an upcoming v2.0 release? Additional Platformer Features The minimum viable product for the Platformer game was to: • move the character around the screen with keyboard interaction • allow players to collect items • increment a player score This worked well for having a small game with a scoring mechanism we could use to implement a score syncing feature. But there’s also a lot more that we could do with the Platformer game example. Features like running, jumping, and scrolling would be great additions, and these are likely to be added in a future release. More Minigames The idea of adding interactive minigames was perhaps the most fun part of the Elixir and Elm Tutorial, and probably the biggest differentiator between this book and many of the other great resources available. The intention was that the games would be fun and interactive, involving keyboard or mouse input as opposed to games like hangman or guessing games or memory games. The original plan was to implement tiny games where players could score points simply by moving a character around or pressing a certain key. But the initial game example grew to increase the fun factor. It would be great to add at least one more game to the book. And the next game would likely incorporate mouse interaction, so it would be fun to implement a simple flying or driving game. Future versions of this book will almost certainly contain additional minigame chapters. If you can’t wait and you’d love to see some amazing examples of games built with Elm, then be sure to check out this list: What’s Next? • • • • • • 298 404 Elm Street94 Asteroids95 Elm Pong96 Elm Shooter97 Elm Snake98 Vessel99 Multiplayer Another feature that would be great for the game platform and any minigames would be to incorporate multiplayer. After authentication, we could potentially use Phoenix channels to build multiplayer features. This is likely to be a bit more difficult to implement than some of the other features, but there’s a great example available here if you’re interested in pursuing it further: • Phoenix Elm Battleship100 Flexible Game Creation The book’s content currently covers the creation of a single minigame example, and the process of adding it to the platform could conceivably be much easier. The Platformer game example could also be improved by extracting common game elements into a separate module. A global Game module could mean having a common game interface with player information, game information, and scores displayed, and the game itself could be imported within that structure. Additionally, if the process of contributing a game were straightforward, then it would be amazing to encourage all readers of this book to share their own creations and create a small library of games to try out. Chat A player chatroom should be relatively simple to add to the main page of the platform. And this may be a good opportunity to provide additional practice and familiarity with GenServer concepts. 94 https://github.com/zalando/elm-street-404 https://github.com/justinmimbs/asteroids 96 https://github.com/ElmOrlando/ElmPong 97 https://github.com/sporto/elm-shooter 98 https://github.com/tibastral/elm-snake 99 https://github.com/slawrence/vessel 100 https://github.com/bigardone/phoenix-elm-battleship 95 What’s Next? 299 OTP Content OTP is such a big part of what makes Elixir amazing, and it’s a travesty not to have some introductory content in the book. The next release will likely extract the scoring features into a small GenServer service. Summary Although we didn’t get to cover everything we hoped in this book, we managed to make a ton of progress and accomplish our goal of building a real-world project together. The plan is that this book will be continually updated to stay relevant with new versions of Elixir, Elm, and Phoenix. And it will be exciting to continually hone the content and add additional material. Outline The table of contents at the beginning of this book is available as a cursory overview of all the chapters and sections in the book. The outline below is an attempt to break down the topics and concepts covered within each chapter. • Introduction – Metadata about Elixir, Elm, and functional programming. – Prerequisites and acknowledgements. – Information about the demo application. • Diving In – Quick-paced practical introduction to the Phoenix framework. – Building the initial Platform demo application. – Configuring the PostgreSQL database, running the server, and routing. – Generating the HTML resources for players. • Elixir Introduction – Create a temporary Elixir application to compare of Elixir and Phoenix projects. – Brief background on mix, folder structure, modules, functions, documentation, tests, and interactive environment. – Introduction to concepts on piping, arity, pattern matching, and guards. • Phoenix Testing and Deployment – Running Phoenix tests. – Working with Git and GitHub. – Configuring the application and deploying to Heroku. • Phoenix Sign Up – Extending existing resources with new fields. – Generating and running migrations. – Basic queries with IEx. – Updating templates and working with forms. • Phoenix Authentication – Importing Hex dependencies. – Working with changesets. – Building an authentication plug. – Adding sign in and session features. • Phoenix API – Generating a JSON API for games. – Routing and scopes. – Ecto relationships, migrations, and schemas. 301 Outline • • • • • • • • • • – Adding JSON API features for players. Elm Introduction – Brief introduction to the Elm language and tooling. – Covers modules, functions, types, formatting, and refactoring. Elm Setup – Introduction to Brunch and Phoenix assets. – Configuring Phoenix to work with Elm. Elm Application – Starting our Elm front-end application. – Importing and using Elm packages. – Working with familiar HTML as a bridge to learning Elm. – Breaking up code into small, pure functions. – Working with the concept of Maybe in Elm. – Iterating through lists of data. Elm Architecture – Structure of the Elm Architecture. – Working with the Record data type in Elm. – Adding the Model, Update, Subscriptions, and View. – Pulling the application together with the Main function. – Adding initial interactivity with Html.Events. Elm API Data – Reading JSON API data from Phoenix. – Decoding JSON and working with game data in Elm. Layout and Design – Viewing application routes. – Working with CSS and working with Bootstrap classes. – Authorizing actions for player account features. – Styling for the lists of players and games. Game Setup – Creating a new Elm file for our first game. – Configuring Phoenix to compile multiple Elm applications. – Adding a slug field for working with games. Our First Game – Working with SVG to create a small game canvas. – Adding a small game character and item. – Refactoring Elm code with let expressions and small functions. Adding Interaction – Working with Elm subscriptions and keyboard input. – Adjusting character position. – Spawning and collecting items. – Working with randomness. Displaying Game Data 302 Outline • • • • • – Rendering text in the game window. – Displaying the player score, items collected, and time remaining. – Working with time. Handling Game States – Introducing union types for game states. – Rendering different elements depending on current game states. – Creating start, success, and game over states. Syncing Score Data – Getting started with Phoenix channels. – Configuring elm-phoenix-socket. What’s Next? – Additional features and game ideas that didn’t make it into the v1.0 release. – Planning for future versions of this book. Appendix – Quick installation instructions. Contact – Congrats and plea for feedback and ideas. – Access to Slack for live help and sharing. Appendix Quick Install This book is intended for developers with some previous experience, so installing these languages and tools shouldn’t be overly difficult or time-consuming. Having said that, it’s easy to get tripped up with installation and configuration steps, so feel free to create a GitHub issue101 if you think there’s an easier approach to setting things up. The intention for this chapter is that we want to get everything we’ll need installed quickly so we can start creating Phoenix projects. These instructions assume that you’re running macOS, but instructions can also be found online for installing these tools on Linux. Elixir First, let’s install Elixir102 with Homebrew103 . This command will also install the latest version of Erlang104 as a dependency: 1 $ brew install elixir You can verify that Elixir has been installed properly by running the following command: 1 2 3 $ elixir -v Erlang/OTP 20 Elixir 1.5.2 Any trouble with this step? Check out the Elixir install page105 or the Elixir section of Stack Overflow106 . Hex Hex107 is the package manager for the Elixir and Erlang ecosystems. Once you have Elixir installed, it’s easy to install Hex with the following command: 101 https://github.com/elixir-elm-tutorial/elixir-elm-tutorial-book/issues https://elixir-lang.org 103 https://brew.sh 104 https://www.erlang.org 105 https://elixir-lang.org/install.html 106 https://stackoverflow.com/questions/tagged/elixir 107 https://hex.pm 102 Appendix 1 304 $ mix local.hex Any trouble with this step? Check out the Hex section of Stack Overflow108 . Phoenix Phoenix109 is a web application framework built with the Elixir language. You can install the latest version with the following command: 1 2 $ mix archive.install https://github.com/phoenixframework/archives/raw/master/ph\ x_new.ez You can verify that Phoenix has been installed properly by running the mix help command, and you should be able to see a mix phx.new task that will allow us to create new Phoenix applications. Any trouble with this step? Check out the Phoenix installation docs110 or the Phoenix section of Stack Overflow111 . PostgreSQL We’ll be using PostgreSQL112 for our database. The easiest way to get started if you’re new to PostgreSQL is to use Postgres.app113 . It’s a macOS application that makes it really simple to get PostgreSQL up and running, and also creates a postgres user that Phoenix uses as a default when creating databases. Any trouble with this step? Check out the PostgreSQL detailed installation guides114 or the PostgreSQL section of Stack Overflow115 . Working with Versions The steps above should be all that’s required to get started. If you’re interested in working with multiple versions different languages, check out the asdf version manager116 . 108 https://stackoverflow.com/questions/tagged/hex-pm http://phoenixframework.org 110 https://hexdocs.pm/phoenix/installation.html 111 https://stackoverflow.com/questions/tagged/phoenix-framework 112 https://www.postgresql.org 113 https://postgresapp.com 114 https://wiki.postgresql.org/wiki/Detailed_installation_guides 115 https://stackoverflow.com/questions/tagged/postgresql 116 https://github.com/asdf-vm/asdf 109 Appendix 305 Recommended Tools In this book, we opted for a simple approach that would allow us to learn about Elixir, Phoenix, and Elm as we put together our demo application. As you start to develop more involved projects, it’s a good idea to review existing tools and services that can make your life easier. The hex.pm117 package manager is an invaluable tool for finding useful libraries for your projects. For example, if you want to allow your users to write Markdown syntax, you can look up “Markdown” on hex.pm and find that the Earmark118 package works really well for this. Listed below are additional tools for your consideration. Authentication Want to build more robust authentication features for your application? Consider checking out the following options: • ueberauth119 • guardian120 Authorization We briefly touched on authorizing actions in our demo application. If you need to work with additional authorization policies, take the following into consideration: • BodyGuard121 Code Quality Credo is a code quality tool that performs static analysis on your Elixir code and provides helpful tips and feedback. It’s really helpful as a way to learn solid Elixir conventions and keep the code throughout your project consistent. • Credo122 117 https://hex.pm https://github.com/pragdave/earmark 119 https://github.com/ueberauth/ueberauth 120 https://github.com/ueberauth/guardian 121 https://github.com/schrockwell/bodyguard 122 https://github.com/rrrene/credo 118 Appendix 306 Documentation Elixir has amazing documentation tools, which explains why the docs are so fantastic. Check out the Elixir guide on writing documentation123 and consider using ExDoc to generate docs for your project. • ExDoc124 Continuous Integration Early versions of this book included material on Continuous Integration and Continuous Delivery. Although it ended up being outside the scope of our content, it’s essential to have a CI server to automatically run your tests. Check out the following options, and consider hooking them into your GitHub repository to automatically deploy your application. • CircleCI125 • Semaphore126 Monitoring Once your project is deployed to production, it’s a good idea to monitor the performance and watch for errors. AppSignal is a good option for tracking this data and keeping your application running smoothly. • AppSignal127 Testing Because we used the Phoenix generators to scaffold out our initial features, our demo application came with quite a few tests. So we have examples of how to work with ExUnit in our project, but Wallaby is a great option for writing highly readable integration tests concurrently. • Wallaby128 123 https://hexdocs.pm/elixir/writing-documentation.html https://github.com/elixir-lang/ex_doc 125 https://circleci.com 126 https://semaphoreci.com 127 https://appsignal.com/elixir 128 https://github.com/keathley/wallaby 124 Contact Congrats Congrats on making it all the way to the end of the book. I’d really love to hear from you! We managed to accomplish our goal of building a fun project together and learn a lot about functional web programming along the way. Feedback Were there additional concepts you’d like to have seen covered in the material? Were there parts of the book that felt confusing? Was there something we could have done differently in our approach? If you have any feedback about the book’s content or the demos contained within, then feel free to reach out. One simple way to provide feedback is to email the author directly at bijanbwb@gmail.com. Alternatively, feel free to create GitHub issues in either the book’s content repository or the demo repository: • Book Content Issues129 • Platform Demo Issues130 Slack As a bonus for making it this far, you can access the book’s Slack group! There are admittedly too many different Slack communities these days, but I’d like to be able to provide live support for the book’s content. Get an invite131 to say hello, get help, and provide suggestions! 129 https://github.com/elixir-elm-tutorial/elixir-elm-tutorial-book/issues https://github.com/elixir-elm-tutorial/platform/issues 131 https://join.slack.com/t/elixirelmtutorial/shared_invite/enQtMjg2NjYyNzI1NDA4LTFjZjZiZDg4ZDE2MjczOWI3YzJkNmI3NGI5ZWI2YmJkNTA5OTAzYzk2MTBmM 130