Functional Web Development with Elixir, OTP, and Phoenix (Pragprog)

How much Phoenix does it cover Michael? Could you jump straight into this without first having read Programming Phoenix?

The section on adding a phoenix interface is very well written. It has the best examples of channels and presence that I’ve seen so far, but it’s not heavy on phoenix. The way he does it, you don’t have to worry about how phoenix works, yet in two chapters you get light javascript clients talking to phoenix channels and a stateful backend api; all the essential components of a modern web app. And, you get to see some of the CRDT (conflict-free replicated datatype) features in phoenix!

4 Likes

Thanks @axelclark! Great to hear that you both enjoyed the book and implemented another frontend! <3 <3 <3

Thank you so much @oldpond! I thought a lot about what material was really necessary to get people up and running as well as the best way to present it. Great to hear that you appreciated the approach! <3 <3 <3

You’re welcome. That moment when you added a few lines of javascript to the default phoenix page and we were talking to the back end was the highlight of the entire book for me.

1 Like

If the book were a novel, that would be the climax of the plot!

The book was great and its a cool project to learn other frontends. I just finished converting the React frontend you sent with the book to React Native using Create React Native App.

I can now have one player playing on the IOS simulator and the second playing in the browser.

The biggest differences were learning the mobile components, using PanResponder/Animated for Drag/Drop, and application of styles.

Here is the main file if anyone is interested:
IslandsMobile

My drag & drop implementation is a little buggy but the basic functionality is there to play the game.

5 Likes

I just finished the book and thought I’d add my thoughts on it. Overall I’d recommend this book to be on the reading list for anyone who is trying to learn Elixir and/or Phoenix (like me).

You probably need to have a basic understanding of Elixir before you start this book. I think you can get through the book without knowing what Phoenix is though. It gives you a good taste of Phoenix.

One of the topics in the book I really liked was explaining how you can recover from a crashed process and the different levels of recovery you could use. It also does a good job explaining supervision.

But I do I think there is some room for improvement within the book:

No tests.
In the first part of the book when you are building the game logic, many times the book instructs you to go back to iex and manually test that things are still working. Some of these manual tests take quite a bit of code to setup and become tedious after a while.

Perhaps the author did not want to introduce tests since they were out of the scope of this book. I believe a very brief intro to testing and writing these manual tests as ExUnit tests instead would have saved a lot of time and be a better way to do things.

JavaScript in part three.
In the third part where you are adding the Phoenix layer, the book instructs you to write the JavaScript to work with the Phoenix Channels directly in the JS console of the web dev tools of the browser. I wish the book instead instructed you how to write the JavaScript within Phoenix.

While going through this part you will have to re-write (or copy/paste from the book) the same JS code quite a few times into the JS console. At one point you have 3 browser windows open and have to copy the same JS code 3 times. Which again becomes as tedious as writing the manual tests as noted above.

I feel that by instead showing us where we can write JS within Phoenix would have eliminated us having to write so much JS over and over and also would have taught us where we can write JS within Phoenix.

The book does not teach you how to build a web interface.
The book advertises that it will show you how to build a web interface with Phoenix. In reality you build a text interface using the JS console, it does not instruct you on how to build a web interface. Although the book mentions that the sample code provided with the book includes sample code for a web interface written in React, I did not see any instructions on how to add this code (perhaps I missed them but so far cannot find them). So I was disappointed on this aspect of the book

If those three points were addressed it would make the book much better in my opinion.

2 Likes

I just started doing that on my own (before the rewrite last year). I modified web/templates/layout/app.html.eex to include:

    </div> <!-- /container -->
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
    <!-- BEGIN Demo code for "B2.0 7. Create Persistent Connections With Phoenix Channels" -->
    <script type="text/javascript">
     var Demo = (function () {

         var demo = {
             connect: connect,
             new_channel: new_channel,
             join: join,
             leave: leave,
             say_hello: say_hello,
             new_game: new_game,
             add_player: add_player,
             set_island_coordinates: set_island_coordinates,
             set_islands: set_islands,
             guess_coordinate: guess_coordinate,
             p1_155: p1_155,
             p1_157: p1_157,
             p1_159: p1_159,
             p1_161: p1_161,
             p2_162: p2_162,
             p1_163: p1_163,
             p1_165: p1_165,
             p1_167: p1_167,
             p2_167: p1_167,
             p2_167b: p2_167b,
             p1_169: p1_169,
             p2_170: p2_170,
             p1_171: p1_171,
             p2_171: p1_171,
             p1_172: p1_172,
             p2_172: p2_172,
             p1_173: p1_173,
             p2_173: p1_173,
             p1_174: p1_174,
             p2_174: p2_174,
             p1_178: () => p1_178("moon"),
             p2_178: () => p1_178("diva"),
             p1_179: p1_179,
             p2_179: p1_179,
             p1_181: p1_181,
             p2_181: p1_181,
             p3_181: p1_181,
             p1_181b: () => p1_181b("moon"),
             p2_181b: () => p1_181b("diva"),
             p3_181b: () => p1_181b("nope"),
         }

       return demo;

       //---
       function connect() {
         // B2.0 p.155
         console.log('> var phoenix = require("phoenix")');
         demo.phoenix = require("phoenix");
         console.log('> var socket = new phoenix.Socket("/socket", {})');
         demo.socket = new demo.phoenix.Socket("/socket", {});
         console.log('> socket.connect()');
         demo.socket.connect();
       }

       // B2.0 p.155
       function new_channel(player, screen_name) {
         return demo.socket.channel("game:" + player, {screen_name: screen_name});
       }

       // B2.0 p.156
       function join(channel) {
         channel.join()
           .receive("ok", response => {
             console.log("Joined successfully!", response);
           })
           .receive("error", response => {
             console.log("Unable to join", response);
           });
       }

       // B2.0 p.157
       function leave(channel) {
        channel.leave()
          .receive("ok", response => {
            console.log("Left successfully", response);
          })
          .receive("error", response => {
            console.log("Unable to leave", response);
          })
       }

       // B2.0 p.159
       function say_hello(channel, greeting) {
         channel.push("hello", {"message": greeting})
           .receive("ok", response => {
             console.log("Hello", response.message);
           })
           .receive("error", response => {
             console.log("Unable to say hello to the channel.", response.message);
           })
       }

       // B2.0 p.165
       function new_game(channel) {
         channel.push("new_game")
           .receive("ok", response => {
             console.log("New Game!", response);
           })
           .receive("error", response => {
             console.log("Unable to start a new game.", response);
           })
       }

       // B2.0 p.167
       function add_player(channel, player) {
         channel.push("add_player", player)
           .receive("error", response => {
             console.log("Unable to add new player: " + player, response)
           })
       }

       // B2.0 p.169
       function set_island_coordinates(channel, player, island, coordinates) {
         params = {"player": player, "island": island, "coordinates": coordinates};
         channel.push("set_island_coordinates", params)
           .receive("ok", response => {
             console.log("New coordinates set!", response);
           })
           .receive("error", response => {
             console.log("Unable to set new coordinates.", response)
           })
       }

       // B2.0 p.171
       function set_islands(channel, player) {
         channel.push("set_islands", player)
           .receive("error", response => {
             console.log("Unable to set islands for: " + player, response);
           })
       }

       // B2.0 p.173
       function guess_coordinate(channel, player, coordinate) {
         params = {"player": player, "coordinate": coordinate}
         channel.push("guess_coordinate", params)
           .receive("error", response => {
             console.log("Unable to guess a coordinate: " + player, response);
           })
       }

       function p1_155() {
         demo.connect();
         console.log('> var game_channel = new_channel("moon", "moon")');
         demo.game_channel = demo.new_channel("moon","moon");
         console.log(demo.game_channel)
       }

       function p1_157() {
         console.log('> join(game_channel)');
         demo.join(demo.game_channel);
       }

       // use for B2.0 159, 160, 161
       function p1_159() {
         console.log('> say_hello(game_channel, "World!")');
         demo.say_hello(demo.game_channel, "World!");
       }

       function p1_161() {
         console.log('game_channel.on("said_hello", response => {...})');
         demo.game_channel.on("said_hello", response => {
           console.log("Returned Greeting:", response.message)
         });
         console.log('> say_hello(game_channel, "World!")');
         demo.say_hello(demo.game_channel, "World!");
       }

       function p2_162() {
         demo.connect();
         console.log('> var game_channel = new_channel("moon", "diva")');
         demo.game_channel = demo.new_channel("moon","diva");
         console.log('> join(game_channel)');
         demo.join(demo.game_channel);
         console.log('> game_channel.on("said_hello", response => {...})');
         demo.game_channel.on("said_hello", response => {
           console.log("Returned Greeting:", response.message)
         });
       }

       function p1_163() {
         console.log('> say_hello(game_channel, "World!")');
         demo.say_hello(demo.game_channel, "World!");
       }

       function p1_165() {
         console.log('> new_game(game_channel)');
         demo.new_game(demo.game_channel);
       }

       // use for player2 (p.167) as well
       function p1_167() {
         console.log('> game_channel.on("player_added", response => {...})');
         demo.game_channel.on("player_added", response => {
           console.log("Player Added", response)
         });
       }

       function p2_167b() {
         console.log('> add_player(game_channel, "diva")');
         demo.add_player(demo.game_channel, "diva");
       }

       function p1_169() {
         console.log('> set_island_coordinates(game_channel, "player1", "atoll", ["a1"])');
         demo.set_island_coordinates(demo.game_channel, "player1", "atoll", ["a1"]);
         console.log('> set_island_coordinates(game_channel, "player1", "dot", ["a1"])');
         demo.set_island_coordinates(demo.game_channel, "player1", "dot", ["a1"]);
         console.log('> set_island_coordinates(game_channel, "player1", "l_shape", ["a1"])');
         demo.set_island_coordinates(demo.game_channel, "player1", "l_shape", ["a1"]);
         console.log('> set_island_coordinates(game_channel, "player1", "s_shape", ["a1"])');
         demo.set_island_coordinates(demo.game_channel, "player1", "s_shape", ["a1"]);
         console.log('> set_island_coordinates(game_channel, "player1", "square", ["a1"])');
         demo.set_island_coordinates(demo.game_channel, "player1", "square", ["a1"]);
       }

       function p2_170() {
         console.log('> set_island_coordinates(game_channel, "player2", "atoll", ["c10"])');
         demo.set_island_coordinates(demo.game_channel, "player2", "atoll", ["c10"]);
       }

       // use for player2 (p.171) as well
       function p1_171() {
         console.log('> game_channel.on("player_set_islands", response => {...})');
         demo.game_channel.on("player_set_islands", response => {
           console.log("Player Set Islands", response);
         });
       }

       function p1_172() {
         console.log('> set_islands(game_channel, "player1")');
         demo.set_islands(demo.game_channel, "player1");
       }

       function p2_172() {
         console.log('> set_islands(game_channel, "player2")');
         demo.set_islands(demo.game_channel, "player2");
       }

       // use for player2 (p.173) as well
       function p1_173() {
         console.log('> game_channel.on("player_guessed_coordinate", response => {...})');
         demo.game_channel.on("player_guessed_coordinate", response => {
           console.log("Player Guessed Coordinate: ", response.result);
         });
       }

       function p1_174() {
         console.log('> guess_coordinate(game_channel, "player1", "b10")');
         demo.guess_coordinate(demo.game_channel, "player1","b10");
       }

       function p2_174() {
         console.log('> guess_coordinate(game_channel, "player2", "a1")');
         demo.guess_coordinate(demo.game_channel, "player2","a1");
       }

       function p1_178(name){
         demo.connect();
         console.log(`> var game_channel = new_channel("moon", "${name}")`);
         demo.game_channel = demo.new_channel("moon", name);
         console.log('> game_channel.on("subscribers", response => {...})');
         demo.game_channel.on("subscribers", response => {
           console.log("These players have joined: ", response);
         });
       }

       function p1_179() {
         console.log('> join(game_channel)');
         demo.join(demo.game_channel);
         console.log('> game_channel.push("show_subscribers")');
         demo.game_channel.push("show_subscribers");
       }

       function p1_181(){
         demo.connect();
       }

       function p1_181b(name){
         console.log(`> var game_channel = new_channel("moon", "${name}")`);
         demo.game_channel = demo.new_channel("moon", name);
         console.log('> join(game_channel)');
         demo.join(demo.game_channel);
       }

     }());
    </script>
    <!-- END Demo code for "B2.0 7. Create Persistent Connections With Phoenix Channels" -->
  </body>

Then in the browser console I just fiddled with:

player1 console // Annotation
-- player2 console

Demo.p1_155(); // Section: Establish a Client Connection
Demo.p1_157();
Demo.p1_159(); // Section: Converse Over a Channel: "Hello World!"
Demo.p1_159(); // "We forced this error."
Demo.p1_159(); // "And we get nothing."
Demo.p1_161();
-- Demo.p2_162();
Demo.p1_163();
Demo.p1_165(); // Section: Connect the Channel to the Game - Start a New Game
Demo.p_165();
Demo.p_165(); // "Unable to start a new game."
Demo.p1_167(); // Section: Add a second player
-- Demo.p2_167();
-- Demo.p2_167b();
Demo.p1_169(); // Section: Setting Island Coordinates
-- Demo.p2_170();
Demo.p1_171();
-- Demo.p2_171();
Demo.p1_172();
-- Demo.p2_172();
Demo.p1_173(); // Section: Guessing Coordinates
-- Demo.p2_173();
Demo.p1_174();
-- Demo.p2_174();

player1 console
-- player2 console

Demo.p1_178(); // Section: Phoenix Presence
-- Demo.p2_178();
Demo.p1_179();
-- Demo.p1_179();

player1 console
-- player2 console
--  -- player3 console

Demo.p1_181(); // Section: Authorization
-- Demo.p2_181();
--  -- Demo.p3_181();
Demo.p1_181b();
-- Demo.p2_181b();
--  -- Demo.p3_181b();

Posted about it in the pragprog forums - which unfortunately are inaccessible right now.

6 Likes

Hey @bmitch,

first of all, thx for sharing your thoughts on this book.

What i personally like most about the book is the fact how the project gets structured in different applications and how they work together. (I saw a very similar, maybe even more extrem, approach by Dave Thomas and was also there very fascinated about the structure.)
From the raw Game/Engine, to the Game/GenServe, Game/Supervisor to the Phoenix Interface.
For me it looks like a modern software project could and maybe should be structured that way to be maintainable in a long run.

In defense to the author, i understand the missing explanation of the react implementation.
This would fill another book, but i agree that he could have found a better way to guide us through the implementation instead of the console. Also a basic front-end implementation would be simply more appealing and fun to work through then the console.log messages.
Especially when you start and stop reading these chapters. But like you, i also create a dummy setup implementation of the channel actions to solve the problem of copy pasting from one to another terminal :wink:

The promised react front-end, and other source code for the different chapters of the book can be found here
Not sure if you have seen it, at least it was not mentioned in the end of the book. Or i missed it.

Also i had a hard time with one particular error.
String.to_exisiting_atom("atol") was throwing for me an ** (ArgumentError) argument error :erlang.binary_to_existing_atom("atol", :utf8).

So if anyone is curious about this error, there is an explanation on a closed github issue of elixir.
Helped me to understand my error and learned smth new about elixir, atom-tables and module loading.

So thx for reading my thoughts and thx for the nice book :tada:

3 Likes

Hello,

I bought this book from PragProg.
It is a great book, I learned a lot from it, I enjoyed implementing and testing the game, but starting on page 160, “Establish a Client Connection”, the example can not be followed any more :frowning:
When I try to execute the javascript statement
var phoenix = require("phoenix")
inside browser’s javascript console I get the following error:

var phoenix = require("phoenix")
Uncaught ReferenceError: require is not defined 
at <anonymous>:1:15

Other users have the same issue (see errata #82937 reported in: P1.0 on 21-Mar-18).

It would be nice if you could help us finishing the last 30 pages of this great book.

Thank you,
Mihai

1 Like

Hi Mihai,

Sorry to hear that you’re having an issue with the JavaScript. Which browser are you using? Are you navigating to localhost:4000 before you try running the code in the JavaScript console?

The code from the book works for me in Chrome and Safari with Phoenix 1.3.x and 1.4 projects, so I’m not able to reproduce the problem locally. If you can give me any more information, I’ll try again to reproduce the issue.

1 Like

Just a little hack in case You cannot use require in the browser
 You can add in app.js

window.phoenix = require("phoenix");

This will give You access to phoenix in the console through window object, like this

> var socket = new window.phoenix.Socket("/socket", {})
5 Likes

Hi,

I’m using Chrome Version 72.0.3626.119 (Official Build) (64-bit) on Ubuntu 18.04 and Firefox Version 65.0.2 (64-bit) with Phoenix 1.4
Because other users have the same issue (see errata #82937 reported in: P1.0 on 21-Mar-18), my guess was that you have a browser extension installed.

Thanks for answer,
Mihai

Thanks Le Gorille,

The hack seems to work perfect.
Now I can continue playing with the nice example project from the book :slight_smile:

1 Like

The problem with require is
 it is not meant to run in the browser, but on node.js.

Browser support is probably limited
 but You can use require.js.

1 Like

I think that the hack from your previous reply is just enough for finishing the project from the book.
I enjoyed following the example from this tutorial, so it would have been a pity not to get to the end.

Thanks again for help :slight_smile:

1 Like

So glad that @kokolegorille provided the solution, and that you’re able to keep working through the book!

Interesting, thanks for providing an effective workaround!

2 Likes

Chrome, Mac OS X, recent Phoenix and Elixir version on Nov 20, 2019.

I have the same issue, and not familiar to Phoenix yet. I arrived at this page by googling “var phoenix = require(“phoenix”)”.