elixerandelm

advertisement
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
Download