Um clone do Twitter com Phoenix e Vue.js

Um clone do Twitter com Phoenix e Vue.js

Slides da palestra que apresentei na Ruby Conf Brasil de 2016 (e depois no ElugSP #7), onde mostro como criar um MVP do Twitter com Phoenix e Vue.js.
O código-fonte do projeto está em https://github.com/philss/sabiah

A69ccd99c8ef0be30b5dc870d7c8e9f8?s=128

Philip Sampaio

September 23, 2016
Tweet

Transcript

  1. 8.
  2. 9.
  3. 10.
  4. 11.
  5. 12.
  6. 14.
  7. 22.
  8. 23.
  9. 25.
  10. 26.
  11. 28.
  12. 31.
  13. 34.
  14. 35.
  15. 36.
  16. 41.

    scope "/", Sabiah do pipe_through :browser # Use the default

    browser stack get "/", TimelineController, :index end web/router.ex
  17. 43.

    defmodule Sabiah.TimelineController do use Sabiah.Web, :controller def index(conn, _params) do

    render conn, "index.html" end end web/controllers/timeline_controller.ex
  18. 44.

    defmodule Sabiah.TimelineController do use Sabiah.Web, :controller def index(conn, _params) do

    render conn, "index.html" end end web/controllers/timeline_controller.ex
  19. 46.

    <h2>Timeline</h2> <div id='timeline-app'> <input type='text' v-model='newTweet' placeholder='What is happening?'> <button

    @click='postTweet' class='button'>Tweet</button> <ul class='timeline'> <li class='tweet' v-for='tweet in tweets'> <span class='tweet__content'>{{ tweet }}</span> </li> </ul> </div> web/templates/timeline/index.html.eex
  20. 47.
  21. 48.
  22. 49.
  23. 50.

    <!doctype html> <html> <body> <div id='app'> <input type='text' v-model='message'> {{

    message }} </div> <script src=‘https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.7/vue.min.js'></script> <script> new Vue({ el: '#app', data: { message: 'Hello world' } }); </script> </body> </html> priv/examples/vue.html
  24. 51.
  25. 53.
  26. 55.
  27. 56.
  28. 57.

    <h2>Timeline</h2> <div id='timeline-app'> <input type='text' v-model='newTweet' placeholder='What is happening?'> <button

    @click='postTweet' class='button'>Tweet</button> <ul class='timeline'> <li class='tweet' v-for='tweet in tweets'> <span class='tweet__content'>{{ tweet }}</span> </li> </ul> </div> web/templates/timeline/index.html.eex
  29. 61.
  30. 62.

    import 'phoenix_html'; import Vue from 'vue/dist/vue'; new Vue({ el: '#timeline-app',

    data: { newTweet: '', tweets: [] }, methods: { postTweet: function() { this.tweets.unshift(this.newTweet); this.newTweet = ''; } } }); web/static/js/app.js
  31. 63.

    <h2>Timeline</h2> <div id='timeline-app'> <input type='text' v-model='newTweet' placeholder='What is happening?'> <button

    @click='postTweet' class='button'>Tweet</button> <ul class='timeline'> <li class='tweet' v-for='tweet in tweets'> <span class='tweet__content'>{{ tweet.content }}</span> </li> </ul> </div> web/templates/timeline/index.html.eex
  32. 65.
  33. 66.
  34. 67.
  35. 71.
  36. 74.
  37. 80.

    import socket from './socket'; const userId = 42; let channel

    = socket.channel(`timeline:${userId}`, {}); channel.join() .receive('ok', resp => { console.log('Joined successfully', resp) }) .receive('error', resp => { console.log('Unable to join', resp) }); // The app ... web/static/js/app.js
  38. 82.

    defmodule Sabiah.TimelineChannel do use Sabiah.Web, :channel def join("timeline:", _payload, _socket)

    do {:error, %{reason: "User id is missing."}} end def join("timeline:" <> user_id, _payload, socket) do socket = assign(socket, :user_id, user_id) {:ok, socket} end def handle_in("new_tweet", payload, socket) do broadcast socket, "new_tweet", payload {:noreply, socket} end end web/channels/timeline_channel.ex
  39. 84.

    defmodule Sabiah.TimelineChannel do use Sabiah.Web, :channel def join("timeline:", _payload, _socket)

    do {:error, %{reason: "User id is missing."}} end def join("timeline:" <> user_id, _payload, socket) do socket = assign(socket, :user_id, user_id) {:ok, socket} end def handle_in("new_tweet", payload, socket) do broadcast socket, "new_tweet", payload {:noreply, socket} end end web/channels/timeline_channel.ex
  40. 86.

    let app = new Vue({ el: '#timeline-app', data: { newTweet:

    '', tweets: [] }, methods: { postTweet: function() { channel.push('new_tweet', { tweet: this.newTweet }); this.newTweet = ''; } } }); channel.on('new_tweet', (payload) => { app.tweets.unshift(payload.tweet); }); web/static/js/app.js
  41. 88.

    defmodule Sabiah.TimelineChannel do use Sabiah.Web, :channel def join("timeline:", _payload, _socket)

    do {:error, %{reason: "User id is missing."}} end def join("timeline:" <> user_id, _payload, socket) do socket = assign(socket, :user_id, user_id) {:ok, socket} end def handle_in("new_tweet", payload, socket) do broadcast socket, "new_tweet", payload {:noreply, socket} end end web/channels/timeline_channel.ex
  42. 90.

    let app = new Vue({ el: '#timeline-app', data: { newTweet:

    '', tweets: [] }, methods: { postTweet: function() { channel.push('new_tweet', { tweet: this.newTweet }); this.newTweet = ''; } } }); channel.on('new_tweet', (payload) => { app.tweets.unshift(payload.tweet); }); web/static/js/app.js
  43. 91.
  44. 94.
  45. 97.
  46. 98.

    defmodule Sabiah.Tweet do use Sabiah.Web, :model schema "tweets" do field

    :content, :string belongs_to :user, Sabiah.User timestamps() end @doc """ Builds a changeset based on the `struct` and `params`. """ def changeset(struct, params \\ %{}) do struct |> cast(params, [:user_id, :content]) |> validate_required([:user_id, :content]) end end web/models/tweet.ex
  47. 100.

    alias Sabiah.Tweet def handle_in("new_tweet", %{ "content" => content } =

    payload, socket) do user_id = socket.assigns[:user_id] tweet_changeset = Tweet.changeset(%Tweet{}, %{user_id: user_id, content: content}) Repo.insert!(tweet_changeset) broadcast socket, "new_tweet", payload {:noreply, socket} end web/channels/timeline_channel.ex
  48. 103.

    alias Sabiah.Tweet def handle_in("new_tweet", %{ "content" => content } =

    payload, socket) do user_id = socket.assigns[:user_id] tweet_changeset = Tweet.changeset(%Tweet{}, %{user_id: user_id, content: content}) Repo.insert!(tweet_changeset) broadcast socket, "new_tweet", payload {:noreply, socket} end web/channels/timeline_channel.ex
  49. 105.

    def changeset(struct, params \\ %{}) do struct |> cast(params, [:user_id,

    :content]) |> validate_required([:user_id, :content]) end web/models/tweet.ex
  50. 110.
  51. 111.
  52. 112.
  53. 113.
  54. 116.
  55. 118.

    <%= for user <- @users do %> <tr> <td><%= user.name

    %></td> <td><%= user.username %></td> <td> <%= form_for follower_changeset(user.id),
 follower_path(@conn, :create), fn f -> %> <%= hidden_input f, :followed_user_id, value: user.id %> <%= submit "+ follow", class: "button" %> <% end %> </td> </tr> <% end %> web/templates/user/index.html.eex
  56. 120.

    defmodule Sabiah.UserView do use Sabiah.Web, :view alias Sabiah.Follower def follower_changeset(user_id)

    do Follower.changeset(%Follower{followed_user_id: user_id}) end end web/views/user_view.ex
  57. 122.

    def create(conn, %{"follower" => params}) do user_id = get_session(conn, :user_id)

    params = Map.put(params, "user_id", user_id) changeset = Follower.changeset(%Follower{}, params) case Repo.insert(changeset) do {:ok, follow} -> conn |> put_flash(:info, "You are now following #{follow.followed_user_id}") |> redirect(to: user_path(conn, :index)) {:error, changeset} -> IO.inspect(changeset) conn |> put_flash(:error, "You seems to be logged out. Create an user to continue.") |> redirect(to: user_path(conn, :new)) end end web/views/user_view.ex
  58. 123.

    &

  59. 124.

    sabiah ⏳ 1. tweetar ✅ 2. seguir ✅ 3. ver

    timeline com tweets de quem sigo
  60. 127.

    def handle_in("new_tweet", %{"content" => content}, socket) do user_id = socket.assigns[:user_id]

    tweet = save_tweet!(user_id, content) response_payload = decorate_payload(user_id, %{"content" => content}) broadcast socket, "new_tweet", response_payload TweetBroadcaster.broadcast_to_followers(tweet, response_payload) {:noreply, socket} end web/channels/timeline_channel.ex
  61. 128.

    def handle_in("new_tweet", %{"content" => content}, socket) do user_id = socket.assigns[:user_id]

    tweet = save_tweet!(user_id, content) response_payload = decorate_payload(user_id, %{"content" => content}) broadcast socket, "new_tweet", response_payload TweetBroadcaster.broadcast_to_followers(tweet, response_payload) {:noreply, socket} end web/channels/timeline_channel.ex
  62. 130.

    defmodule Sabiah.TweetBroadcaster do alias Sabiah.{Endpoint, Repo} import Ecto.Query def broadcast_to_followers(tweet,

    payload) do followers_ids_query = from f in "followers", where: f.followed_user_id == ^tweet.user_id, select: f.user_id followers_ids = Repo.all(followers_ids_query) Enum.map(followers_ids, fn(user_id) -> Endpoint.broadcast!("timeline:#{user_id}", "new_tweet", payload) end) end end web/channels/tweet_broadcaster.ex
  63. 132.

    sabiah ⏳ 1. tweetar ✅ 2. seguir ✅ 3. ver

    timeline com tweets de quem sigo
  64. 135.
  65. 138.
  66. 139.
  67. 140.
  68. 141.
  69. 146.