{"id":83297,"url":"https://github.com/dwyl/phoenix-liveview-counter-tutorial","name":"phoenix-liveview-counter-tutorial","description":"🤯  beginners tutorial building a real time counter in Phoenix 1.7.14 + LiveView 1.0 ⚡️ Learn the fundamentals from first principals so you can make something amazing! 🚀","projects_count":46,"last_synced_at":"2026-06-04T18:00:20.547Z","repository":{"id":38314793,"uuid":"190408334","full_name":"dwyl/phoenix-liveview-counter-tutorial","owner":"dwyl","description":"🤯  beginners tutorial building a real time counter in Phoenix 1.7.14 + LiveView 1.0 ⚡️ Learn the fundamentals from first principals so you can make something amazing! 🚀","archived":false,"fork":false,"pushed_at":"2026-05-08T13:59:33.000Z","size":1272,"stargazers_count":417,"open_issues_count":4,"forks_count":43,"subscribers_count":89,"default_branch":"main","last_synced_at":"2026-05-15T01:02:34.232Z","etag":null,"topics":["awesome","beginner","beginner-friendly","beginners","counter","elixir","example","how-to","howto","learn","phoenix","phoenix-framework","phoenix-liveview","tutorial"],"latest_commit_sha":null,"homepage":"https://livecount.fly.dev/","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dwyl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-06-05T14:16:45.000Z","updated_at":"2026-05-12T08:56:08.000Z","dependencies_parsed_at":"2023-02-17T06:31:01.942Z","dependency_job_id":"494077d6-1408-40d9-a599-ff9446aa3eed","html_url":"https://github.com/dwyl/phoenix-liveview-counter-tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dwyl/phoenix-liveview-counter-tutorial","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-liveview-counter-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-liveview-counter-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-liveview-counter-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-liveview-counter-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dwyl","download_url":"https://codeload.github.com/dwyl/phoenix-liveview-counter-tutorial/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-liveview-counter-tutorial/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33916320,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"created_at":"2025-01-17T00:00:58.063Z","updated_at":"2026-06-04T18:00:20.547Z","primary_language":null,"list_of_lists":false,"displayable":true,"categories":["Step 4: _Share_ State Between Clients!","Prerequisites: What you Need _Before_ You Start 📝","`LiveView`?","Step 0: Run the _Finished_ Counter App on your `localhost` 🏃‍","Step 2: Create the `counter.ex` File","More Tests!","Bonus Level: Use a `LiveView Component` (Optional)","Moving state out of the `LiveView`","How many `people` are viewing the Counter?","`Phoenix LiveView` for Web Developers Who Don't know `Elixir`"],"sub_categories":["Checkpoint: Run It!","Code Explanation","_Download_ the Dependencies","_Explanation_ of the Code","Recap: Working Counter _Without_ Writing `JavaScript`!","Update the Tests for `GenServer` State","Add More Tests!","Create a `LiveView Component`"],"readme":"\u003cdiv align=\"center\"\u003e\n\n# Phoenix LiveView Counter Tutorial\n\n![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/phoenix-liveview-counter-tutorial/ci.yml?label=build\u0026style=flat-square\u0026branch=main)\n[![codecov.io](https://img.shields.io/codecov/c/github/dwyl/phoenix-liveview-counter-tutorial/master.svg?style=flat-square)](https://codecov.io/github/dwyl/phoenix-liveview-counter-tutorial?branch=master)\n[![Hex.pm](https://img.shields.io/hexpm/v/phoenix_live_view?color=brightgreen\u0026style=flat-square)](https://hex.pm/packages/phoenix_live_view)\n[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/phoenix-liveview-counter-tutorial/issues)\n[![HitCount](https://hits.dwyl.com/dwyl/phoenix-liveview-counter-tutorial.svg)](https://hits.dwyl.io/dwyl/phoenix-liveview-counter-tutorial)\n\n**Build your _first_ App** using **Phoenix LiveView** 🥇\u003cbr /\u003e\nand **_understand_** all the concepts in **10 minutes** or _less_! 🚀 \u003cbr /\u003e\nTry it: [livecount.fly.dev](https://livecount.fly.dev/)\n  \u003cdiv\u003e\n    \u003ca href=\"https://livecount.fly.dev/\"\u003e\n      \u003cimg src=\"https://github.com/dwyl/phoenix-liveview-counter-tutorial/assets/194400/e61cf511-d1d8-4236-83b2-f9f45e06e710\"\u003e\n    \u003c/a\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\u003cbr /\u003e\n\n- [Phoenix LiveView Counter Tutorial](#phoenix-liveview-counter-tutorial)\n- [Why? 🤷‍♀️](#why-️)\n- [What? 💭](#what-)\n  - [`LiveView`?](#liveview)\n- [Who? 👤](#who-)\n  - [Please Star! ⭐](#please-star-)\n  - [Prerequisites: What you Need _Before_ You Start 📝](#prerequisites-what-you-need-before-you-start-)\n- [How? 💻](#how-)\n  - [Step 0: Run the _Finished_ Counter App on your `localhost` 🏃‍](#step-0-run-the-finished-counter-app-on-your-localhost-)\n    - [Clone the Repository](#clone-the-repository)\n    - [_Download_ the Dependencies](#download-the-dependencies)\n    - [_Run_ the App](#run-the-app)\n  - [Step 1: Create the App 🆕](#step-1-create-the-app-)\n    - [Checkpoint 1: _Run_ the _Tests_!](#checkpoint-1-run-the-tests)\n    - [Checkpoint 1b: _Run_ the New Phoenix App!](#checkpoint-1b-run-the-new-phoenix-app)\n  - [Step 2: Create the `counter.ex` File](#step-2-create-the-counterex-file)\n    - [_Explanation_ of the Code](#explanation-of-the-code)\n  - [Step 3: Create the `live` Route in `router.ex`](#step-3-create-the-live-route-in-routerex)\n    - [3.1 Update the Failing Test Assertion](#31-update-the-failing-test-assertion)\n    - [Checkpoint: Run Counter App!](#checkpoint-run-counter-app)\n    - [Recap: Working Counter _Without_ Writing `JavaScript`!](#recap-working-counter-without-writing-javascript)\n  - [Step 4: _Share_ State Between Clients!](#step-4-share-state-between-clients)\n    - [Code Explanation](#code-explanation)\n    - [Checkpoint: Run It!](#checkpoint-run-it)\n  - [Congratulations! 🎉](#congratulations-)\n  - [Tests! 🧪](#tests-)\n    - [Add `excoveralls` to Check/Track Coverage](#add-excoveralls-to-checktrack-coverage)\n    - [Create a `coveralls.json` File](#create-a-coverallsjson-file)\n    - [`DELETE` Unused Files](#delete-unused-files)\n    - [Add More Tests!](#add-more-tests)\n  - [Bonus Level: Use a `LiveView Component` (Optional)](#bonus-level-use-a-liveview-component-optional)\n    - [Create a `LiveView Component`](#create-a-liveview-component)\n  - [Moving state out of the `LiveView`](#moving-state-out-of-the-liveview)\n    - [Update the Tests for `GenServer` State](#update-the-tests-for-genserver-state)\n  - [How many `people` are viewing the Counter?](#how-many-people-are-viewing-the-counter)\n  - [More Tests!](#more-tests)\n- [_Done_! 🏁](#done-)\n- [What's _Next_?](#whats-next)\n- [_Feedback_ 💬 🙏](#feedback--)\n- [Credits + Thanks! 🙌](#credits--thanks-)\n  - [`Phoenix LiveView` for Web Developers Who Don't know `Elixir`](#phoenix-liveview-for-web-developers-who-dont-know-elixir)\n  - [Relevant + Recommended Reading](#relevant--recommended-reading)\n  \n\u003cbr /\u003e\n\n# Why? 🤷‍♀️\n\nThere are several apps around the Internet \nthat use `Phoenix LiveView`\nbut _none_ include **step-by-step instructions**\na _complete_ beginner can follow ... 😕 \u003cbr /\u003e\n_This_ is the **_complete beginner's_ tutorial**\nwe _wish_ we had when **learning `LiveView`**\nand the one _you_ have been searching for! 🎉\n\n# What? 💭\n\nA **_complete beginners_ tutorial** for building\nthe most basic possible `Phoenix LiveView` App\nwith **no prior experience** necessary.\n\n## `LiveView`?\n\nPhoenix `LiveView` allows you to build **rich interactive web apps**\nwith **realtime reactive UI** (_no page refresh when data updates_)\n**without** writing **`JavaScript`**!\nThis enables building **_incredible_ interactive experiences**\nwith **_considerably_ less code**.\n\n`LiveView` pages load instantly because they are rendered on the Server\nand they require far less bandwidth than a similar\nReact, Vue.js, Angular, etc. because only the _bare minimum_\nis loaded on the client for the page to work.\n\nFor a sneak peak of what is possible to build with `LiveView`,\nwatch [@chrismccord](https://github.com/chrismccord)'s **`LiveBeats`** intro:\n\nhttps://user-images.githubusercontent.com/576796/162234098-31b580fe-e424-47e6-b01d-cd2cfcf823a9.mp4\n\n\u003e **Tip**: Enable the **_sound_**. It's a collaborative music listening experience. 🎶\n\u003e Try the `LiveBeats` Demo: \n\u003e [livebeats.fly.dev](https://livebeats.fly.dev/) \n\u003e 😍 🤯 🙏\n\n\u003cbr /\u003e\n\n# Who? 👤\n\nThis tutorial is aimed at `people` who have\nnever built _anything_ in `Phoenix` or `LiveView`.\nYou can _speed-run_ it in **10 minutes**\nif you're already familiar with `Phoenix` or `Rails`.\n\nIf you get stuck at any point\nwhile following the tutorial\nor you have any feedback/questions,\n_please_\n[open an issue on `GitHub`!](https://github.com/dwyl/phoenix-liveview-counter-tutorial/issues)\n\nIf you don't have a lot of time or bandwidth to watch videos,\nthis tutorial will be the _fastest_ way to learn `LiveView`.\n\n## Please Star! ⭐\n\nThis is the tutorial we _wish_ we'd had when we first started using `LiveView` ... \u003cbr /\u003e\nIf you find it useful, please give it a star ⭐ it on `Github` \nso that other `people` will discover it. \n\nThanks! 🙏 \n\n\u003cbr /\u003e\n\n## Prerequisites: What you Need _Before_ You Start 📝\n\nBefore you start working through the tutorial,\nyou will need:\n\n**a.** **`Elixir` installed** on your computer.\nSee: [learn-elixir#**installation**](https://github.com/dwyl/learn-elixir#installation) \u003cbr /\u003e\n\nWhen you run the command:\n\n```sh\nelixir -v\n```\n\nAt the time of writing, you should expect to see output similar to the following:\n\n```elixir\nElixir 1.17.3 (compiled with Erlang/OTP 26)\n```\n\nThis informs us we are using `Elixir version 1.17.3`\nwhich is the _latest_ version at the time of writing.\nSome of the more advanced features of Phoenix 1.7 during compilation time require elixir \n`1.17` although the code will work in previous versions.\n\n\u003cbr /\u003e\n\n**b.** **`Phoenix` installed** on your computer.\nsee: [hexdocs.pm/phoenix/**installation**](https://hexdocs.pm/phoenix/installation.html)\n\nIf you run the following command in your terminal:\n\n```sh\nmix phx.new -v\n```\n\nYou should see something similar to the following:\n\n```sh\nPhoenix installer v1.7.14\n```\n\nIf you have an earlier version,\ndefinitely upgrade to get the _latest_ features! \u003cbr /\u003e\nIf you have a _later_ version of `Phoenix`,\nand you get _stuck_ at any point,\n_please_\n[open an issue on GitHub!](https://github.com/dwyl/phoenix-liveview-counter-tutorial/issues)\nWe are here to help!\n\n\u003cbr /\u003e\n\n**c.** Basic familiarity with **`Elixir` syntax**\nis _recommended_ but _not essential_; \u003cbr /\u003e\nIf you know _any_ programming language,\nyou can pick it up as you go and\n[ask questions](https://github.com/dwyl/phoenix-liveview-counter-tutorial/issues)\nif you get stuck!\nSee: \n[https://github.com/dwyl/**learn-elixir**](https://github.com/dwyl/learn-elixir#what)\n\n\u003cbr /\u003e\n\n# How? 💻\n\nThis tutorial takes you through all the steps\nto build and test a counter in Phoenix LiveView. \u003cbr /\u003e\nWe always\n[\"begin with the end in mind\"](https://en.wikipedia.org/wiki/The_7_Habits_of_Highly_Effective_People#2_-_Begin_with_the_end_in_mind)\nso we recommend running the _finished_ app\non your machine _before_ writing any code.\n\n\u003e 💡 You can also try the version deployed to Fly.io:\n\u003e [livecount.fly.dev](https://livecount.fly.dev)\n\n\u003cbr /\u003e\n\n## Step 0: Run the _Finished_ Counter App on your `localhost` 🏃‍\n\nBefore you attempt to _build_ the counter,\nwe suggest that you clone and _run_\nthe complete app on your `localhost`. \u003cbr /\u003e\nThat way you _know_ it's working\nwithout much effort/time expended.\n\n### Clone the Repository\n\nOn your `localhost`,\nrun the following command to clone the repo\nand change into the directory:\n\n```sh\ngit clone https://github.com/dwyl/phoenix-liveview-counter-tutorial.git\ncd phoenix-liveview-counter-tutorial\n```\n\n### _Download_ the Dependencies\n\nInstall the dependencies by running the command:\n\n```sh\nmix setup\n```\n\nIt will take a few seconds to download the dependencies\ndepending on the speed of your internet connection;\nbe\n[patient](https://user-images.githubusercontent.com/194400/76169853-58139380-6174-11ea-8e03-4011815758d0.png).\n😉\n\n### _Run_ the App\n\nStart the Phoenix server by running the command:\n\n```sh\nmix phx.server\n```\n\nNow you can visit\n[`localhost:4000`](http://localhost:4000)\nin your web browser.\n\n\u003e 💡 Open a _second_ browser window (_e.g. incognito mode_),\n\u003e you will see the the counter updating in both places like magic!\n\nYou should expect to see something similar to the following:\n\n![phoenix-liveview-counter-start](https://github.com/dwyl/phoenix-liveview-counter-tutorial/assets/194400/d473ce40-f0e6-4b30-af2f-d9902921a9ac)\n\nWith the _finished_ version of the App running on your machine\nand a clear picture of where we are headed, it's time to _build_ it!\n\n\u003cbr /\u003e\n\n## Step 1: Create the App 🆕\n\nIn your terminal, \nrun the following `mix` command\nto generate the new Phoenix app:\n\n```sh\nmix phx.new counter --no-ecto --no-mailer --no-dashboard --no-gettext\n```\n\nThe flags after the `counter` (name of the project),\ntell `mix phx.new` generator:\n+ `--no-ecto` - don't create a Database - we aren't storing any data\n+ `--no-mailer`- this project doesn't send `email`\n+ `--no-dashboard` - we don't need a status `dashboard`\n+ `--no-gettext` - no translation required\n  \nThis keeps our counter as simple as possible.\nWe can always add these features _later_ if needed. \n\n\u003e **Note**: Since `Phoenix` `1.6` the `--live` flag\n\u003e is no longer required when creating a `LiveView` app.\n\u003e `LiveView` is included by default in all new `Phoenix` Apps.\n\u003e Older tutorials may still include the flag,\n\u003e everything is _much_ easier now. 😉\n\nWhen you see the following prompt:\n\n```sh\nFetch and install dependencies? [Yn]\n```\n\nType `Y` followed by the `[Enter]` key.\nThat will download all the necessary dependencies.\n\n\u003cbr /\u003e\n\n### Checkpoint 1: _Run_ the _Tests_!\n\nIn your terminal, \ngo into the newly created app folder:\n\n```sh\ncd counter\n```\n\nAnd then run the following `mix` command:\n\n```sh\nmix test\n```\n\nThis will compile the `Phoenix` app \nand will take some time. ⏳ \u003cbr /\u003e\nYou should see output similar to this:\n\n```sh\nCompiling 13 files (.ex)\nGenerated counter app\n.....\nFinished in 0.00 seconds (0.00s async, 0.00s sync)\n5 tests, 0 failures\n\nRandomized with seed 29485\n```\n\nTests all pass. ✅\n\nThis is _expected_ with a `new` app.\nIt's a good way to confirm everything is working.\n\n\u003cbr /\u003e\n\n### Checkpoint 1b: _Run_ the New Phoenix App!\n\nRun the server by executing this command:\n\n```sh\nmix phx.server\n```\n\nVisit\n[`localhost:4000`](http://localhost:4000)\nin your web browser.\n\nYou should see something similar to the following:\n\n![welcome-to-phoenix](https://github.com/dwyl/phoenix-liveview-counter-tutorial/assets/194400/fa8a37e6-9b4d-4f36-b156-33a2e16030ff)\n\n\u003cbr /\u003e\n\n## Step 2: Create the `counter.ex` File\n\nCreate a new file with the path:\n`lib/counter_web/live/counter.ex`\n\nAnd add the following code to it:\n\n```elixir\ndefmodule CounterWeb.Counter do\n  use CounterWeb, :live_view\n\n  def mount(_params, _session, socket) do\n    {:ok, assign(socket, :val, 0)}\n  end\n\n  def handle_event(\"inc\", _, socket) do\n    {:noreply, update(socket, :val, \u0026(\u00261 + 1))}\n  end\n\n  def handle_event(\"dec\", _, socket) do\n    {:noreply, update(socket, :val, \u0026(\u00261 - 1))}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003cdiv\u003e\n    \u003ch1 class=\"text-4xl font-bold text-center\"\u003e The count is: \u003c%= @val %\u003e \u003c/h1\u003e\n\n    \u003cp class=\"text-center\"\u003e\n     \u003c.button phx-click=\"dec\"\u003e-\u003c/.button\u003e\n     \u003c.button phx-click=\"inc\"\u003e+\u003c/.button\u003e\n     \u003c/p\u003e\n     \u003c/div\u003e\n    \"\"\"\n  end\nend\n```\n\n### _Explanation_ of the Code\n\nThe first line instructs Phoenix to use the \n[`Phoenix.LiveView`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html)\n[behaviour](https://elixirschool.com/en/lessons/advanced/behaviours).\nThis just means that we need to implement certain functions\nfor our `LiveView` to work.\n\nThe first function is `mount/3` which,\nas it's name suggests, \n_mounts_ the module\nwith the `_params`, `_session` and `socket` arguments:\n\n```elixir\ndef mount(_params, _session, socket) do\n  {:ok, assign(socket, :val, 0) }\nend\n```\n\nIn our case we are _ignoring_ \nthe `_params` and `_session` arguments,\nhence the prepended underscore.\nIf we were using sessions,\nwe would need to check the `session` variable,\nbut in this simple `counter` example, we just ignore it.\n\n`mount/3` returns a\n[tuple](https://elixir-lang.org/getting-started/basic-types.html#tuples):\n`{:ok, assign(socket, :val, 0)}`\nwhich uses the\n[`assign/3`](https://hexdocs.pm/phoenix/Phoenix.Socket.html#assign/3)\nfunction to assign the `:val` key a value of `0` on the `socket`.\nThat just means the socket will now have a `:val`\nwhich is initialized to `0`.\n\n\u003cbr /\u003e\n\nThe second function is `handle_event/3`\nwhich handles the incoming events received.\nIn the case of the _first_ declaration of\n`handle_event(\"inc\", _, socket)`\nit pattern matches the string `\"inc\"`\nand _increments_ the counter.\n\n```elixir\ndef handle_event(\"inc\", _, socket) do\n  {:noreply, update(socket, :val, \u0026(\u00261 + 1))}\nend\n```\n\n`handle_event/3` (\"inc\")\nreturns a tuple of:\n`{:noreply, update(socket, :val, \u0026(\u00261 + 1))}`\nwhere the `:noreply` just means\n\"do not send any further messages to the caller of this function\". \n\n`update(socket, :val, \u0026(\u00261 + 1))` as it's name suggests,\nwill _update_ the value of `:val` on the `socket`\nto the\n`\u0026(\u00261 + 1)` is a shorthand way of writing `fn val -\u003e val + 1 end`.\nthe `\u0026()` is the same as `fn ... end`\n(_where the `...` is the function definition_).\nIf this inline anonymous function syntax is unfamiliar to you,\nplease read:\nhttps://elixir-lang.org/crash-course.html#partials-and-function-captures-in-elixir\n\nThe _third_ function is _almost identical_ to the one above,\nthe key difference is that it decrements the `:val`.\n\n```elixir\ndef handle_event(\"dec\", _, socket) do\n  {:noreply, update(socket, :val, \u0026(\u00261 - 1))}\nend\n```\n\n`handle_event(\"dec\", _, socket)` pattern matches the `\"dec\"` String\nand _decrements_ the counter using the `\u0026(\u00261 - 1)` syntax.\n\n\u003e In `Elixir` we can have multiple\n\u003e similar functions with the _same_ function name\n\u003e but different matches on the arguments\n\u003e or different \"arity\" (_number of arguments_). \u003cbr /\u003e\n\u003e For more detail on Functions in Elixir,\n\u003e see: \n\u003e [elixirschool.com/functions/#named-functions](https://elixirschool.com/en/lessons/basics/functions/#named-functions)\n\n_Finally_ the _forth_ function `render/1`\nreceives the `assigns` argument which contains the `:val` state\nand _renders_ the template using the `@val` template variable.\n\nThe `render/1` function renders the template included in the function.\nThe `~H\"\"\"` syntax just means\n\"_treat this multiline string as a LiveView template_\"\nThe `~H` \n[`sigil`](https://elixir-lang.org/getting-started/sigils.html)\nis a macro included when the `use Phoenix.LiveView` is invoked\nat the top of the file.\n\n`LiveView` will invoke the `mount/3` function\nand will pass the result of `mount/3` to `render/1` behind the scenes.\n\nEach time an update happens (e.g: `handle_event/3`)\nthe `render/1` function will be executed\nand updated data (_in our case the `:val` count_)\nis sent to the client.\n\n\u003e 🏁 At the end of Step 2 you should have a file similar to:\n\u003e [`lib/counter_web/live/counter.ex`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/6aeb1b53b4c22b14258772e65ac05fd172a9f961/lib/counter_web/live/counter.ex)\n\n\u003cbr /\u003e\n\n## Step 3: Create the `live` Route in `router.ex`\n\nNow that we have created our `LiveView` handler functions in Step 2,\nit's time to tell `Phoenix` how to _find_ it.\n\nOpen the\n`lib/counter_web/router.ex`\nfile and locate the block of code\nthat starts with `scope \"/\", CounterWeb do`:\n\n```elixir\nscope \"/\", CounterWeb do\n  pipe_through :browser\n\n  get \"/\", PageController, :index\nend\n```\n\nReplace the line `get \"/\", PageController, :index`\nwith `live(\"/\", Counter)`.\nSo you end up with:\n\n```elixir\nscope \"/\", CounterWeb do\n  pipe_through :browser\n\n  live(\"/\", Counter)\nend\n```\n\n\u003cbr /\u003e\n\n### 3.1 Update the Failing Test Assertion\n\nSince we have replaced the\n`get \"/\", PageController, :index` route in `router.ex`\nin the previous step, the test in\n`test/counter_web/controllers/page_controller_test.exs`\nwill now _fail_:\n\n```sh\nCompiling 6 files (.ex)\nGenerated counter app\n....\n\n  1) test GET / (CounterWeb.PageControllerTest)\n     test/counter_web/controllers/page_controller_test.exs:4\n     Assertion with =~ failed\n     code:  assert html_response(conn, 200) =~ \"Peace of mind from prototype to production\"\n     left:  \"\u003c!DOCTYPE html\u003e\\n\u003chtml lang=\\\"en\\\"\u003e\\n  \u003chead\u003e\\n    \u003cmeta charset=\\\"utf-8\\\"\u003e\\n    \u003cmeta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1\\\"\u003e\\n    \u003cmeta name=\\\"csrf-token\\\" content=\\\"EFt5PABkPz1nPg5FMAoaDSA6BCFtBCMO_4JmNTx_2vO6i3qxXjETTpal\\\"\u003e\\n    \u003ctitle data-suffix=\\\" · Phoenix Framework\\\"\u003e\\nLiveViewCounter\\n     · Phoenix Framework\u003c/title\u003e\\n    \u003clink phx-track-static rel=\\\"stylesheet\\\" href=\\\"/assets/app.css\\\"\u003e\\n    \u003cscript defer phx-track-static type=\\\"text/javascript\\\" src=\\\"/assets/app.js\\\"\u003e\\n    \u003c/script\u003e\\n  \u003c/head\u003e\\n  \u003cbody class=\\\"bg-white antialiased\\\"\u003e\\n\u003cdiv data-phx-main=\\\"true\\\" data-phx-session=\\\"SFMyNTY.g2gDaA\\\" id=\\\"phx-Fyi3ICCa7vPqDADE\\\"\u003e\u003cheader class=\\\"px-4 sm:px-6 lg:px-8\\\"\u003e\\n  \u003cdiv class=\\\"flex items-center justify-between border-b border-zinc-100 py-3\\\"\u003e\\n    \u003cdiv class=\\\"flex items-center gap-4\\\"\u003e\\n      \u003ca href=\\\"/\\\"\u003e\\n        \u003csvg viewBox=\\\"0 0 71 48\\\" class=\\\"h-6\\\" aria-hidden=\\\"true\\\"\u003e\\n          \u003cpath d=\\\"etc.\" fill=\\\"#FD4F00\\\"\u003e\u003c/path\u003e\\n        \u003c/svg\u003e\\n      \u003c/a\u003e\\n      \u003cp class=\\\"rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6 text-brand\\\"\u003e\\n        v1.7\\n      \u003c/p\u003e\\n    \u003c/div\u003e\\n    \u003cdiv class=\\\"flex items-center gap-4\\\"\u003e\\n      \u003ca href=\\\"https://twitter.com/elixirphoenix\\\" class=\\\"text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700\\\"\u003e\\n        @elixirphoenix\\n      \u003c/a\u003e\\n      \u003ca href=\\\"https://github.com/phoenixframework/phoenix\\\" class=\\\"text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700\\\"\u003e\\n        GitHub\\n      \u003c/a\u003e\\n      \u003ca href=\\\"https://hexdocs.pm/phoenix/overview.html\\\" class=\\\"rounded-lg bg-zinc-100 px-2 py-1 text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:bg-zinc-200/80 active:text-zinc-900/70\\\"\u003e\\n        Get Started \u003cspan aria-hidden=\\\"true\\\"\u003e\u0026rarr;\u003c/span\u003e\\n      \u003c/a\u003e\\n    \u003c/div\u003e\\n  \u003c/div\u003e\\n\u003c/header\u003e\\n\u003cmain class=\\\"px-4 py-20 sm:px-6 lg:px-8\\\"\u003e\\n \u003c/p\u003e\\n  \\n\u003c/div\u003e\\n\u003cdiv\u003e\\n\u003ch1 class=\\\"text-4xl font-bold text-center\\\"\u003e The count is: 0 \u003c/h1\u003e\\n\\n\u003cp class=\\\"text-center\\\"\u003e\\n \u003cbutton class=\\\"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3 text-sm font-semibold leading-6 text-white active:text-white/80 \\\" phx-click=\\\"dec\\\"\u003e\\n  -\\n\u003c/button\u003e\\n \u003cbutton class=\\\"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3 text-sm font-semibold leading-6 text-white active:text-white/80 \\\" phx-click=\\\"inc\\\"\u003e\\n  +\\n\u003c/button\u003e\\n \u003c/p\u003e\\n \u003c/div\u003e\\n  \u003c/div\u003e\\n\u003c/main\u003e\u003c/div\u003e\\n  \u003c/body\u003e\\n\u003c/html\u003e\"\n     right: \"Peace of mind from prototype to production\"\n     stacktrace:\n       test/counter_web/controllers/page_controller_test.exs:6: (test)\n\n\nFinished in 0.1 seconds (0.06s async, 0.09s sync)\n5 tests, 1 failure\n```\n\nThis just tells us that the test is looking for the string\n`\"Peace of mind from prototype to production\"` \nin the page and did not find it.\n\nTo fix the broken test, open the\n`test/counter_web/controllers/page_controller_test.exs`\nfile and locate the line:\n\n```elixir\nassert html_response(conn, 200) =~ \"Peace of mind from prototype to production\"\n```\n\nUpdate the string from `\"Peace of mind from prototype to production\"`\nto something we _know_ is present on the page,\ne.g:\n`\"The count is\"`\n\n\u003e 🏁 The `page_controller_test.exs.exs` file should now look like this:\n\u003e [`test/counter_web/controllers/page_controller_test.exs#L6`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/73e6f79a5fb75c36d3e7bb5a894b7f515ab83d40/test/counter_web/controllers/page_controller_test.exs)\n\nConfirm the tests pass again by running:\n\n```sh\nmix test\n```\n\nYou should see output similar to:\n\n```\n.....\nFinished in 0.08 seconds (0.03s async, 0.05s sync)\n5 tests, 0 failures\n\nRandomized with seed 268653\n```\n\n\u003cbr /\u003e\n\n### Checkpoint: Run Counter App!\n\nNow that all the code for the `counter.ex` is written,\nrun the Phoenix app with the following command:\n\n```\nmix phx.server\n```\n\nVist\n[`localhost:4000`](http://localhost:4000)\nin your web browser.\n\nYou should expect to see a fully functioning `LiveView` counter:\n\n![liveview-counter-1.7.7](https://github.com/dwyl/phoenix-liveview-counter-tutorial/assets/194400/abb5bb09-de59-4631-b5a9-48f9de28ef75)\n\n\u003cbr /\u003e\n\n### Recap: Working Counter _Without_ Writing `JavaScript`!\n\nOnce the initial installation\nand configuration of `LiveView` was complete,\nthe creation of the actual counter was _remarkably_ simple.\nWe created a _single_ new file\n[`lib/counter_web/live/counter.ex`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/6aeb1b53b4c22b14258772e65ac05fd172a9f961/lib/counter_web/live/counter.ex)\nthat contains all the code required to\ninitialise, render and update the counter.\nThen we set the `live \"/\", Counter` route\nto invoke the `Counter` module in `router.ex`.\n\nIn total our `counter` App is **25 lines** of (relevant) code. 🤯\n\n\u003cbr /\u003e\n\nOne important thing to note is that\nthe counter only maintains state \nfor a _single_ web browser.\nTry opening a _second_ browser window (_e.g: in \"incognito mode\"_)\nand notice how the counter only updates in one window at a time:\n\n![phoenix-liveview-counter-two-windows-independent-count](https://github.com/dwyl/phoenix-liveview-counter-tutorial/assets/194400/7f8d0742-a295-4507-b7a2-fa2a281a32cb)\n\nIf we want to _share_ the `counter` state between multiple clients,\nwe need to add a bit more code.\n\n\u003cbr /\u003e\n\n## Step 4: _Share_ State Between Clients!\n\nOne of the biggest selling points\nof using `Phoenix` to build web apps\nis the built-in support for \n[`WebSockets`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)\nin the form of \n[`Phoenix Channels`](https://hexdocs.pm/phoenix/channels.html).\n`Channels` allow us to\n_effortlessly_ sync data between\nclients and servers with _minimal_ overhead;\nthey are one of `Elixir` (`Erlang/OTP`) superpowers! \n\nWe can share the `counter` state\nbetween multiple clients by updating the\n`counter.ex`\nfile with the following code:\n\n```elixir\ndefmodule CounterWeb.Counter do\n  use CounterWeb, :live_view\n\n  @topic \"live\"\n\n  def mount(_session, _params, socket) do\n    if connected?(socket) do\n      CounterWeb.Endpoint.subscribe(@topic) # subscribe to the channel\n    end\n    {:ok, assign(socket, :val, 0)}\n  end\n\n  def handle_event(\"inc\", _value, socket) do\n    new_state = update(socket, :val, \u0026(\u00261 + 1))\n    CounterWeb.Endpoint.broadcast_from(self(), @topic, \"inc\", new_state.assigns)\n    {:noreply, new_state}\n  end\n\n  def handle_event(\"dec\", _, socket) do\n    new_state = update(socket, :val, \u0026(\u00261 - 1))\n    CounterWeb.Endpoint.broadcast_from(self(), @topic, \"dec\", new_state.assigns)\n    {:noreply, new_state}\n  end\n\n  def handle_info(msg, socket) do\n    {:noreply, assign(socket, val: msg.payload.val)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003cdiv class=\"text-center\"\u003e\n      \u003ch1 class=\"text-4xl font-bold text-center\"\u003e Counter: \u003c%= @val %\u003e \u003c/h1\u003e\n      \u003c.button phx-click=\"dec\" class=\"w-20 bg-red-500 hover:bg-red-600\"\u003e-\u003c/.button\u003e\n      \u003c.button phx-click=\"inc\" class=\"w-20 bg-green-500 hover:bg-green-600\"\u003e+\u003c/.button\u003e\n    \u003c/div\u003e\n    \"\"\"\n  end\nend\n```\n\n### Code Explanation\n\nThe first change is on\n[Line 4](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/664228ac564a79a0dd92d06857622c1ba22cda71/lib/counter_web/live/counter.ex#L4)\n`@topic \"live\"` defines a module attribute\n(_think of it as a global constant_),\nthat lets us to reference `@topic` \nanywhere in the file.\n\nThe second change is on\n[Line 7](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/664228ac564a79a0dd92d06857622c1ba22cda71/lib/counter_web/live/counter.exL7)\nwhere the\n[`mount/3`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/d3cddb14dff911a377d0e41b916cfe57b0557606/lib/counter_web/live/counter.ex#L6)\nfunction now creates a subscription to the `@topic` when the socket is connected:\n\n```elixir\nCounterWeb.Endpoint.subscribe(@topic) # subscribe to the channel topic\n```\n\nWhen the client (the browser) connects to the Phoenix server, \na websocket connection is established. \nThe interface is the `socket` and \nwe know that the socket is connected \nwhen the [connected?/1](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#connected?/1) function returns `true`. \nThis is why we only subscribe to the channel \nwhen the socket is connected.\nWhy do we do this? \nBecause a websocket connection starts with an HTTP request \nand HTTP is a stateless protocol. \nSo when the client connects to the server, \nthe server does not know if the client is already connected to the server. \nOnce the websocket connection is established, \nthe server knows that the client is connected, \nthus `connected?(socket) == true`.\n\nEach client connected to the App\nsubscribes to the `@topic`\nso when the count is updated on any of the clients,\nall the other clients see the same value.\n\nNext we update the first\n[`handle_event/3`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/d3cddb14dff911a377d0e41b916cfe57b0557606/lib/counter_web/live/counter.ex#L11)\nfunction which handles the `\"inc\"` event:\n\n```elixir\ndef handle_event(\"inc\", _msg, socket) do\n  new_state = update(socket, :val, \u0026(\u00261 + 1))\n  CounterWeb.Endpoint.broadcast_from(self(), @topic, \"inc\", new_state.assigns)\n  {:noreply, new_state}\nend\n```\n\nAssign the result of the `update` invocation to `new_state`\nso that we can use it on the next two lines.\nInvoking\n`CounterWeb.Endpoint.broadcast_from`\nsends a message from the current process `self()`\non the `@topic`, the key is \"inc\"\nand the _value_ is the `new_state.assigns` Map.\n\nIn case you are curious (_like we are_),\n`new_state` is an instance of the\n[`Phoenix.LiveView.Socket`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Socket.html)\nsocket:\n\n```elixir\n#Phoenix.LiveView.Socket\u003c\n  assigns: %{\n    flash: %{},\n    live_view_action: nil,\n    live_view_module: CounterWeb.Counter,\n    val: 1\n  },\n  changed: %{val: true},\n  endpoint: CounterWeb.Endpoint,\n  id: \"phx-Ffq41_T8jTC_3gED\",\n  parent_pid: nil,\n  view: CounterWeb.Counter,\n  ...\n}\n```\n\nThe `new_state.assigns` is a Map\nthat includes the key `val`\nwhere the value is `1`\n(_after we clicked on the increment button_).\n\nThe _fourth_ update is to the\n`\"dec\"` version of\n[`handle_event/3`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/d3cddb14dff911a377d0e41b916cfe57b0557606/lib/counter_web/live/counter.ex#L17)\n\n```elixir\ndef handle_event(\"dec\", _msg, socket) do\n  new_state = update(socket, :val, \u0026(\u00261 - 1))\n  CounterWeb.Endpoint.broadcast_from(self(), @topic, \"dec\", new_state.assigns)\n  {:noreply, new_state}\nend\n```\n\nThe only difference from the `\"inc\"`\nversion is the `\u0026(\u00261 - 1)`\nand \"dec\" in the `broadcast_from`.\n\nThe _final_ change is the implementation of the `handle_info/2` function:\n\n```elixir\ndef handle_info(msg, socket) do\n  {:noreply, assign(socket, val: msg.payload.val)}\nend\n```\n\n[`handle_info/2`](https://hexdocs.pm/phoenix/Phoenix.Channel.html#c:handle_info/2)\nhandles `Elixir` process messages\nwhere `msg` is the received message\nand `socket` is the `Phoenix.Socket`. \u003cbr /\u003e\nThe line `{:noreply, assign(socket, val: msg.payload.val)}`\njust means \"don't send this message to the socket again\"\n(_which would cause a recursive loop of updates_).\n\n_Finally_ we modified the `HTML` inside the `render/1` function\nto be a bit more visually appealing.\n\n\u003e 🏁 At the end of Step 6 the file looks like:\n\u003e [`lib/counter_web/live/counter.ex`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/664228ac564a79a0dd92d06857622c1ba22cda71/lib/counter_web/live/counter.ex)\n\n\u003cbr /\u003e\n\n### Checkpoint: Run It!\n\nNow that\n[`counter.ex`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/664228ac564a79a0dd92d06857622c1ba22cda71/lib/counter_web/live/counter.ex#L4)\nhas been updated to broadcast the count to all connected clients,\nlet's _run_ the app in a few web browsers to show it in _action_!\n\nIn your terminal, run:\n\n```\nmix phx.server\n```\n\nOpen\n[`localhost:4000`](http://localhost:4000)\nin as many web browsers as you have\nand test the increment/decrement buttons!\n\nYou should see the count increasing/decreasing in all browsers simultaneously!\n\n![phoenix-liveview-counter-four-windows](https://github.com/dwyl/phoenix-liveview-counter-tutorial/assets/194400/f25db87c-d8b3-40db-aee6-631c6fadd8da)\n\n\n\u003cbr /\u003e\n\n## Congratulations! 🎉\n\nYou just built a real-time counter\nthat seamlessly updates all connected clients\nusing `Phoenix LiveView`\nin less than **40 lines** of code!\n\n\u003cbr /\u003e\n\n## Tests! 🧪\n\n`before` we get carried away celebrating that we've _finished_ the counter,\nLet's make sure that all the functionality, however basic, is fully tested.\n\n### Add `excoveralls` to Check/Track Coverage\n\nOpen your `mix.exs` file and locate the `deps` list.\nAdd the following line to the list:\n\n```elixir\n# Track test coverage: github.com/parroty/excoveralls\n{:excoveralls, \"~\u003e 0.16.0\", only: [:test, :dev]},\n```\n\ne.g: \n[`mix.exs#L58`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/664228ac564a79a0dd92d06857622c1ba22cda71/mix.exs#L58)\n\nThen, still in the `mix.exs` file, locate the `project` definition,\nand replace:\n\n```elixir\ndeps: deps()\n```\n\nWith the following lines:\n\n```elixir\ndeps: deps(),\ntest_coverage: [tool: ExCoveralls],\npreferred_cli_env: [\n  c: :test,\n  coveralls: :test,\n  \"coveralls.detail\": :test,\n  \"coveralls.json\": :test,\n  \"coveralls.post\": :test,\n  \"coveralls.html\": :test,\n  t: :test\n]\n```\n\ne.g:\n[`mix.exs#L13-L22`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/664228ac564a79a0dd92d06857622c1ba22cda71/mix.exs#L13-L22)\n\nFinally in the `aliases` section of `mix.exs`,\nadd the following lines:\n\n```elixir\nc: [\"coveralls.html\"],\ns: [\"phx.server\"],\nt: [\"test\"]\n```\n\nThe `mix c` alias is the one we care about, we're going to use it immediately.\nThe other two `mix s` and `mix t` are convenient shortcuts too. \nHopefully you can infer what they do. 👌\n\n\nWith the the `mix.exs` file updated, \nrun the following commands in your terminal:\n\n```sh\nmix deps.get\nmix c\n```\n\nThat will download the `excoveralls` dependency\nand execute the tests with coverage tracking.\n\nYou should see output similar to the following:\n\n```sh\nRandomized with seed 468341\n----------------\nCOV    FILE                                        LINES RELEVANT   MISSED\n100.0% lib/counter.ex                                  9        0        0\n 75.0% lib/counter/application.ex                     34        4        1\n100.0% lib/counter_web.ex                            111        2        0\n 15.9% lib/counter_web/components/core_componen      661      151      127\n100.0% lib/counter_web/components/layouts.ex           5        0        0\n100.0% lib/counter_web/controllers/error_html.e       19        1        0\n100.0% lib/counter_web/controllers/error_json.e       15        1        0\n  0.0% lib/counter_web/controllers/page_control        9        1        1\n100.0% lib/counter_web/controllers/page_html.ex        5        0        0\n100.0% lib/counter_web/endpoint.ex                    46        0        0\n 33.3% lib/counter_web/live/counter.ex                32       12        8\n100.0% lib/counter_web/live/counter_component.e       17        2        0\n100.0% lib/counter_web/router.ex                      18        2        0\n 80.0% lib/counter_web/telemetry.ex                   69        5        1\n[TOTAL]  23.8%\n----------------\nGenerating report...\nSaved to: cover/\nFAILED: Expected minimum coverage of 100%, got 23.8%.\n```\n\nThis tells us that only `23.8%` of the code in the project is covered by tests. 😕\nLet's fix that!\n\n### Create a `coveralls.json` File\n\nIn the root of the project, \ncreate a file called `coveralls.json`\nand add the following code to it:\n\n\n```json\n{\n  \"coverage_options\": {\n    \"minimum_coverage\": 100\n  },\n  \"skip_files\": [\n    \"lib/counter/application.ex\",\n    \"lib/counter_web.ex\",\n    \"lib/counter_web/channels/user_socket.ex\",\n    \"lib/counter_web/telemetry.ex\",\n    \"lib/counter_web/views/error_helpers.ex\",\n    \"lib/counter_web/router.ex\",\n    \"lib/counter_web/live/page_live.ex\",\n    \"lib/counter_web/components/core_components.ex\",\n    \"lib/counter_web/controllers/error_json.ex\",\n    \"lib/counter_web/controllers/error_html.ex\",\n    \"test/\"\n  ]\n}\n```\n\nThis file and the `skip_files` list specifically, \ntells `excoveralls` to ignore boilerplate `Phoenix` files\nwe cannot test. \n\nIf you re-run `mix c` now you should see something similar to the following:\n\n```sh\nRandomized with seed 572431\n----------------\nCOV    FILE                                        LINES RELEVANT   MISSED\n100.0% lib/counter.ex                                  9        0        0\n100.0% lib/counter_web/components/layouts.ex           5        0        0\n  0.0% lib/counter_web/controllers/page_control        9        1        1\n100.0% lib/counter_web/controllers/page_html.ex        5        0        0\n100.0% lib/counter_web/endpoint.ex                    46        0        0\n 33.3% lib/counter_web/live/counter.ex                32       12        8\n[TOTAL]  40.0%\n----------------\nGenerating report...\nSaved to: cover/\nFAILED: Expected minimum coverage of 100%, got 40%.\n```\n\nThis is already much better. \nThere are only 2 files we need to focus on.\nLet's start by tidying up the unused files.\n\n### `DELETE` Unused Files\n\nGiven that this `counter` App doesn't use any \"controllers\",\nwe can simply `DELETE` the \n`lib/counter_web/controllers/page_controller.ex` file.\n\n```sh\ngit rm lib/counter_web/controllers/page_controller.ex\n```\n\n\nRename the `test/counter_web/controllers/page_controller_test.exs`\nto: `test/counter_web/live/counter_test.exs`\n\nUpdate the code in the `test/counter_web/live/counter_test.exs` to:\n\n```elixir\ndefmodule CounterWeb.CounterTest do\n  use CounterWeb.ConnCase\n  import Phoenix.LiveViewTest\n\n  test \"connected mount\", %{conn: conn} do\n    {:ok, _view, html} = live(conn, \"/\")\n    assert html =~ \"Counter: 0\"\n  end\nend\n```\n\nRe-run the tests:\n\n```sh\nmix c\n```\n\nYou should see:\n\n```sh\nFinished in 0.1 seconds (0.04s async, 0.07s sync)\n6 tests, 0 failures\n\nRandomized with seed 603239\n----------------\nCOV    FILE                                        LINES RELEVANT   MISSED\n100.0% lib/counter.ex                                  9        0        0\n100.0% lib/counter_web/components/layouts.ex           5        0        0\n100.0% lib/counter_web/controllers/page_html.ex        5        0        0\n100.0% lib/counter_web/endpoint.ex                    46        0        0\n 33.3% lib/counter_web/live/counter.ex                32       12        8\n[TOTAL]  42.9%\n----------------\nGenerating report...\nSaved to: cover/\nFAILED: Expected minimum coverage of 100%, got 42.9%.\n```\n\nOpen the coverage `HTML` file:\n```sh\nopen cover/excoveralls.html\n```\n\nYou should see:\n\n\u003cimg width=\"1180\" alt=\"image\" src=\"https://github.com/dwyl/phoenix-liveview-counter-tutorial/assets/194400/3efbe3ba-d3d4-47a7-88d5-82f0e8cb2712\"\u003e\n\nThis shows us which functions/lines are _not_ being covered by our _existing_ tests.\n\n\u003cbr /\u003e\n\n### Add More Tests!\n\nOpen the `test/counter_web/live/counter_test.exs`\nand add the following tests:\n\n```elixir\ntest \"Increment\", %{conn: conn} do\n  {:ok, view, _html} = live(conn, \"/\")\n  assert render_click(view, :inc) =~ \"Counter: 1\"\nend\n\ntest \"Decrement\", %{conn: conn} do\n  {:ok, view, _html} = live(conn, \"/\")\n  assert render_click(view, :dec) =~ \"Counter: -1\"\nend\n\ntest \"handle_info/2 broadcast message\", %{conn: conn} do\n  {:ok, view, _html} = live(conn, \"/\")\n  {:ok, view2, _html} = live(conn, \"/\")\n\n  assert render_click(view, :inc) =~ \"Counter: 1\"\n  assert render_click(view2, :inc) =~ \"Counter: 2\"\nend\n```\n\nOnce you've saved the file, \nre-run the tests: `mix c`\nYou should see:\n\n```sh\n........\nFinished in 0.1 seconds (0.03s async, 0.09s sync)\n8 tests, 0 failures\n\nRandomized with seed 552859\n----------------\nCOV    FILE                                        LINES RELEVANT   MISSED\n100.0% lib/counter.ex                                  9        0        0\n100.0% lib/counter_web/components/layouts.ex           5        0        0\n100.0% lib/counter_web/controllers/page_html.ex        5        0        0\n100.0% lib/counter_web/endpoint.ex                    46        0        0\n100.0% lib/counter_web/live/counter.ex                32       12        0\n[TOTAL] 100.0%\n----------------\n```\n\n**Done**. ✅\n\n## Bonus Level: Use a `LiveView Component` (Optional)\n\nAt present the\n[`render/1`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/664228ac564a79a0dd92d06857622c1ba22cda71/lib/counter_web/live/counter.ex#L27-L35)\nfunction in\n[`counter.ex`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/664228ac564a79a0dd92d06857622c1ba22cda71/lib/counter_web/live/counter.ex)\nhas an inline template:\n\n```elixir\ndef render(assigns) do\n  ~H\"\"\"\n  \u003cdiv class=\"text-center\"\u003e\n    \u003ch1 class=\"text-4xl font-bold text-center\"\u003e Counter: \u003c%= @val %\u003e \u003c/h1\u003e\n    \u003c.button phx-click=\"dec\" class=\"text-6xl pb-2 w-20 bg-red-500 hover:bg-red-600\"\u003e-\u003c/.button\u003e\n    \u003c.button phx-click=\"inc\" class=\"text-6xl pb-2 w-20 bg-green-500 hover:bg-green-600\"\u003e+\u003c/.button\u003e\n  \u003c/div\u003e\n  \"\"\"\n```\n\nThis is _fine_ when the template is _small_ like in this `counter`,\nbut in a bigger App \nlike our \n[`MPV`](https://github.com/dwyl/mvp/)\nit's a good idea to _split_ the template into a _separate_ file\nto make it easier to read and maintain.\n\nThis is where \n[`Phoenix.LiveComponent`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html)\ncomes to the rescue!\n`LiveComponents` are a mechanism \nto compartmentalize state, markup, \nand events in `LiveView`.\n\n### Create a `LiveView Component`\n\nCreate a new file with the path:\n`lib/counter_web/live/counter_component.ex`\n\nAnd type (or paste) the following code in it: \n\n```elixir\ndefmodule CounterComponent do\n  use Phoenix.LiveComponent\n\n  # Avoid duplicating Tailwind classes and show hot to inline a function call:\n  defp btn(color) do\n    \"text-6xl pb-2 w-20 rounded-lg bg-#{color}-500 hover:bg-#{color}-600\"\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003cdiv class=\"text-center\"\u003e\n      \u003ch1 class=\"text-4xl font-bold text-center\"\u003e Counter: \u003c%= @val %\u003e \u003c/h1\u003e\n      \u003cbutton phx-click=\"dec\" class={btn(\"red\")}\u003e\n        -\n      \u003c/button\u003e\n      \u003cbutton phx-click=\"inc\" class={btn(\"green\")}\u003e\n        +\n      \u003c/button\u003e\n    \u003c/div\u003e\n    \"\"\"\n  end\nend\n```\n\nThen back in the `counter.ex` file, \nupdate the `render/1` function to:\n\n```elixir\n  def render(assigns) do\n    ~H\"\"\"\n    \u003c.live_component module={CounterComponent} id=\"counter\" val={@val} /\u003e\n    \"\"\"\n  end\n```\n\n\n\u003e 🏁 Your `counter_component.ex` should look like this:\n[`lib/counter_web/live/counter_component.ex`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/92a25e41d52be7bbfd430b92181540806af64baa/lib/counter_web/live/counter_component.ex)\n\nThe component has a similar\n[`render/1`](https://github.com/dwyl/phoenix-liveview-counter-tutorial/blob/33e0e47fd379e1314dcba6509d214c9468632c77/lib/counter_web/live/counter.ex#L27-L34)\nfunction to what was in `counter.ex`.\nThat's the point; we just want it in a separate file\nfor maintainability.\n\n\u003cbr /\u003e\n\nRe-run the `counter` App \nusing `mix phx.server` \nand confirm everything still works:\n\n![phoenix-liveview-counter-component](https://github.com/dwyl/phoenix-liveview-counter-tutorial/assets/194400/7026e461-003d-4033-a0ea-c80b55494fa5)\n\n\nThe tests all still pass and we have 100% coverage:\n\n```sh\n........\nFinished in 0.1 seconds (0.03s async, 0.09s sync)\n8 tests, 0 failures\n\nRandomized with seed 470293\n----------------\nCOV    FILE                                        LINES RELEVANT   MISSED\n100.0% lib/counter.ex                                  9        0        0\n100.0% lib/counter_web/components/layouts.ex           5        0        0\n100.0% lib/counter_web/controllers/page_html.ex        5        0        0\n100.0% lib/counter_web/endpoint.ex                    46        0        0\n100.0% lib/counter_web/live/counter.ex                32       12        0\n100.0% lib/counter_web/live/counter_component.e       17        2        0\n[TOTAL] 100.0%\n----------------\n```\n\n\u003cbr /\u003e\n\n\n## Moving state out of the `LiveView`\n\nWith this implementation you may have noticed that when we open a new browser\nwindow the count is always zero. As soon as we click plus or minus it adjusts\nand all the views get back in line. This is because the state is replicated\nacross LiveView instances and coordinated via pub-sub. If the state was big\nand complicated this would get wasteful in resources and hard to manage.\n\nGenerally it is good practice \nto identify _shared state_ \nand to manage that in\na single location.\n\nThe `Elixir` way of managing state is the\n[`GenServer`](https://hexdocs.pm/elixir/GenServer.html), \nusing `PubSub` to update\nthe `LiveView` about changes. \nThis allows the `LiveViews` \nto focus on specific state, \nseparating concerns; \nmaking the application both more efficient\n(hopefully) and easier to reason about and debug.\n\n\nCreate a file with the path:\n`lib/counter_web/live/counter_state.ex` \nand add the following:\n\n```elixir\ndefmodule Counter.Count do\n  use GenServer\n  alias Phoenix.PubSub\n  @name :count_server\n\n  @start_value 0\n\n  # External API (runs in client process)\n\n  def topic do\n    \"count\"\n  end\n\n  def start_link(_opts) do\n    GenServer.start_link(__MODULE__, @start_value, name: @name)\n  end\n\n  def incr() do\n    GenServer.call @name, :incr\n  end\n\n  def decr() do\n    GenServer.call @name, :decr\n  end\n\n  def current() do\n    GenServer.call @name, :current\n  end\n\n  def init(start_count) do\n    {:ok, start_count}\n  end\n\n  # Implementation (Runs in GenServer process)\n\n  def handle_call(:current, _from, count) do\n     {:reply, count, count}\n  end\n\n  def handle_call(:incr, _from, count) do\n    make_change(count, +1)\n  end\n\n  def handle_call(:decr, _from, count) do\n    make_change(count, -1)\n  end\n\n  defp make_change(count, change) do\n    new_count = count + change\n    PubSub.broadcast(Counter.PubSub, topic(), {:count, new_count})\n    {:reply, new_count, new_count}\n  end\nend\n```\n\nThe `GenServer` runs in its own process. \nOther parts of the application invoke\nthe API in their own process, these calls are forwarded to the `handle_call`\nfunctions in the `GenServer` process where they are processed serially.\n\nWe have also moved the `PubSub` publication here as well.\n\nWe are also going to need to tell the Application that it now has some business\nlogic; we do this in the `start/2` function in the\n`lib/counter/application.ex` file.\n\n```diff\n  def start(_type, _args) do\n    children = [\n      # Start the App State\n+     Counter.Count,\n      # Start the Telemetry supervisor\n      CounterWeb.Telemetry,\n      # Start the PubSub system\n      {Phoenix.PubSub, name: Counter.PubSub},\n      # Start the Endpoint (http/https)\n      CounterWeb.Endpoint\n      # Start a worker by calling: Counter.Worker.start_link(arg)\n      # {Counter.Worker, arg}\n    ]\n\n    # See https://hexdocs.pm/elixir/Supervisor.html\n    # for other strategies and supported options\n    opts = [strategy: :one_for_one, name: Counter.Supervisor]\n    Supervisor.start_link(children, opts)\n  end\n...\n```\n\nFinally, we need make some changes to the `LiveView` itself,\nit now has less to do!\n\n```elixir\ndefmodule CounterWeb.Counter do\n  use CounterWeb, :live_view\n  alias Counter.Count\n  alias Phoenix.PubSub\n\n  @topic Count.topic\n\n  def mount(_params, _session, socket) do\n    if connected?(socket) do\n      PubSub.subscribe(Counter.PubSub, @topic)\n    end\n    {:ok, assign(socket, val: Count.current()) }\n  end\n\n  def handle_event(\"inc\", _, socket) do\n    {:noreply, assign(socket, :val, Count.incr())}\n  end\n\n  def handle_event(\"dec\", _, socket) do\n    {:noreply, assign(socket, :val, Count.decr())}\n  end\n\n  def handle_info({:count, count}, socket) do\n    {:noreply, assign(socket, val: count)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003cdiv\u003e\n      \u003ch1\u003eThe count is: \u003c%= @val %\u003e\u003c/h1\u003e\n      \u003c.button phx-click=\"dec\"\u003e-\u003c/.button\u003e\n      \u003c.button phx-click=\"inc\"\u003e+\u003c/.button\u003e\n    \u003c/div\u003e\n    \"\"\"\n  end\nend\n```\n\nThe initial state is retrieved from the\nshared Application `GenServer` process \nand the updates are being forwarded there\nvia its API. \nFinally, the `GenServer`\nto all the active `LiveView` clients.\n\n### Update the Tests for `GenServer` State\n\nGiven that the `counter.ex` is now using the `GenServer` State,\ntwo of the tests now fail because the count is not correct.\n\n```sh\nmix t\n\nGenerated counter app\n.....\n\n  1) test connected mount (CounterWeb.CounterTest)\n     test/counter_web/live/counter_test.exs:6\n     Assertion with =~ failed\n     code:  assert html =~ \"Counter: 0\"\n     left:  \"\u003chtml lang=\\\"en\\\" class=\\\"[scrollbar-gutter:stable]\\\"\u003e\u003chead\u003e\u003cmeta charset=\\\"utf-8\\\"/\u003e\u003cmeta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1\\\"/\u003e\u003cmeta name=\\\"csrf-token\\\" content=\\\"A2QQMHc0OgsbDl0mUCZdGDoHWhUtMC4CDUIv9XHhtx2p6_iLerkvIbbk\\\"/\u003e\u003ctitle data-suffix=\\\" · Phoenix Framework\\\"\u003e\\nCounter\\n     · Phoenix Framework\u003c/title\u003e\u003clink phx-track-static=\\\"phx-track-static\\\" rel=\\\"stylesheet\\\" href=\\\"/assets/app.css\\\"/\u003e\u003cscript defer=\\\"defer\\\" phx-track-static=\\\"phx-track-static\\\" type=\\\"text/javascript\\\" src=\\\"/assets/app.js\\\"\u003e\\n    \u003c/script\u003e\u003c/head\u003e\u003cbody class=\\\"bg-white antialiased\\\"\u003e\u003cdiv data-phx-main=\\\"data-phx-main\\\" data-phx-session=\\\"SFMyNTY.g2gDaAJhBXQAAAAIdwJpZG0AAAAUcGh4LUYzWm01LWgycVNXZW5RREJ3B3Nlc3Npb250AAAAAHcKcGFyZW50X3BpZHcDbmlsdwZyb3V0ZXJ3GEVsaXhpci5Db3VudGVyV2ViLlJvdXRlcncEdmlld3cZRWxpeGlyLkNvdW50ZXJXZWIuQ291bnRlcncIcm9vdF9waWR3A25pbHcJcm9vdF92aWV3dxlFbGl4aXIuQ291bnRlcldlYi5Db3VudGVydwxsaXZlX3Nlc3Npb25oAncHZGVmYXVsdG4IAPP26BwRWHYXbgYA2w20ookBYgABUYA.Zae9BLTboLn6SPPe0qmktsfuru8HX2W4CALIBZNpcqE\\\" data-phx-static=\\\"SFMyNTY.g2gDaAJhBXQAAAADdwJpZG0AAAAUcGh4LUYzWm01LWgycVNXZW5RREJ3BWZsYXNodAAAAAB3CmFzc2lnbl9uZXdqbgYA2w20ookBYgABUYA.uooN8p97RRE1JN4tmkVNqC9islv-Np5B8wrewhwLnKc\\\" id=\\\"phx-F3Zm5-h2qSWenQDB\\\"\u003e\u003cheader class=\\\"px-4 sm:px-6 lg:px-8\\\"\u003e\u003cdiv class=\\\"flex items-center justify-between border-b border-zinc-100 py-3 text-sm\\\"\u003e\u003cdiv class=\\\"flex items-center gap-4\\\"\u003e\u003ca href=\\\"/\\\"\u003e\u003cimg src=\\\"/images/logo.svg\\\" width=\\\"36\\\"/\u003e\u003c/a\u003e\u003cp class=\\\"bg-brand/5 text-brand rounded-full px-2 font-medium leading-6\\\"\u003e\\n        v1.7.7\\n      \u003c/p\u003e\u003c/div\u003e\u003cdiv class=\\\"flex items-center gap-4 font-semibold leading-6 text-zinc-900\\\"\u003e\u003ca href=\\\"https://github.com/dwyl/phoenix-liveview-counter-tutorial\\\" class=\\\"hover:text-zinc-700\\\"\u003e\\n        GitHub\\n      \u003c/a\u003e\u003ca href=\\\"https://github.com/dwyl/phoenix-liveview-counter-tutorial#how-\\\" class=\\\"rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80\\\"\u003e\\n        Get Started ...\"\n     right: \"Counter: 0\"\n     stacktrace:\n       test/counter_web/live/counter_test.exs:8: (test)\n\n\n\n  2) test Increment (CounterWeb.CounterTest)\n     test/counter_web/live/counter_test.exs:11\n     Assertion with =~ failed\n     code:  assert render_click(view, :inc) =~ \"Counter: 1\"\n     left:  \"\u003cheader class=\\\"px-4 sm:px-6 lg:px-8\\\"\u003e\u003cdiv class=\\\"flex items-center justify-between border-b border-zinc-100 py-3 text-sm\\\"\u003e\u003cdiv class=\\\"flex items-center gap-4\\\"\u003e\u003ca href=\\\"/\\\"\u003e\u003cimg src=\\\"/images/logo.svg\\\" width=\\\"36\\\"/\u003e\u003c/a\u003e\u003cp class=\\\"bg-brand/5 text-brand rounded-full px-2 font-medium leading-6\\\"\u003e\\n        v1.7.7\\n      \u003c/p\u003e\u003c/div\u003e\u003cdiv class=\\\"flex items-center gap-4 font-semibold leading-6 text-zinc-900\\\"\u003e\u003ca href=\\\"https://github.com/dwyl/phoenix-liveview-counter-tutorial\\\" class=\\\"hover:text-zinc-700\\\"\u003e\\n        GitHub\\n      \u003c/a\u003e\u003ca href=\\\"https://github.com/dwyl/phoenix-liveview-counter-tutorial#how-\\\" class=\\\"rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80\\\"\u003e\\n      etc...\"\n     right: \"Counter: 1\"\n     stacktrace:\n       test/counter_web/live/counter_test.exs:13: (test)\n\n.\nFinished in 0.1 seconds (0.02s async, 0.09s sync)\n8 tests, 2 failures\n```\n\nThe test is expecting the _initial_ state to be `0` (zero) each time.\nBut if we are storing the `count` in the `GenServer`,\nit will not be `0`.\n\nWe can easily update the tests to check the state \n_before_ incrementing/decrementing it.\nOpen the \n`test/counter_web/live/counter_test.exs`\nfile and replace the contents with the following:\n\n```elixir\ndefmodule CounterWeb.CounterTest do\n  use CounterWeb.ConnCase\n  import Phoenix.LiveViewTest\n\n  test \"Increment\", %{conn: conn} do\n    {:ok, view, html} = live(conn, \"/\")\n    current = Counter.Count.current()\n    assert html =~ \"Counter: #{current}\"\n    assert render_click(view, :inc) =~ \"Counter: #{current + 1}\"\n  end\n\n  test \"Decrement\", %{conn: conn} do\n    {:ok, view, html} = live(conn, \"/\")\n    current = Counter.Count.current()\n    assert html =~ \"Counter: #{current}\"\n    assert render_click(view, :dec) =~ \"Counter: #{current - 1}\"\n  end\n\n  test \"handle_info/2 Count Update\", %{conn: conn} do\n    {:ok, view, disconnected_html} = live(conn, \"/\")\n    current = Counter.Count.current()\n    assert disconnected_html =~ \"Counter: #{current}\"\n    assert render(view) =~ \"Counter: #{current}\"\n    send(view.pid, {:count, 2})\n    assert render(view) =~ \"Counter: 2\"\n  end\nend\n```\n\nRe-run the tests `mix c` and watch them pass with 100% coverage:\n\n```sh\n.......\nFinished in 0.1 seconds (0.04s async, 0.09s sync)\n7 tests, 0 failures\n\nRandomized with seed 614997\n----------------\nCOV    FILE                                        LINES RELEVANT   MISSED\n100.0% lib/counter.ex                                  9        0        0\n100.0% lib/counter_web/components/layouts.ex           5        0        0\n100.0% lib/counter_web/controllers/page_html.ex        5        0        0\n100.0% lib/counter_web/endpoint.ex                    46        0        0\n100.0% lib/counter_web/live/counter.ex                31        7        0\n100.0% lib/counter_web/live/counter_component.e       17        2        0\n100.0% lib/counter_web/live/counter_state.ex          53       12        0\n[TOTAL] 100.0%\n----------------\n```\n\n\n\n\n## How many `people` are viewing the Counter?\n\n`Phoenix` has a very cool feature called\n[`Presence`](https://hexdocs.pm/phoenix/presence.html#content) \nto track how many\n`people` (connected clients) are using our system. \nIt does a lot more than count clients, \nbut this is a counting app so ...\n\nFirst of all we need to tell the `Application` \nwe are going to use `Presence`.\nFor this we need to create a \n`lib/counter/presence.ex` \nfile and add the following lines of code:\n\n```elixir\ndefmodule Counter.Presence do\n  use Phoenix.Presence,\n    otp_app: :counter,\n    pubsub_server: Counter.PubSub\nend\n```\n\nand tell the application about it in the \n`lib/counter/application.ex`\nfile (add it just below the PubSub config):\n\n```diff\n  def start(_type, _args) do\n    children = [\n      # Start the App State\n      Counter.Count,\n      # Start the Telemetry supervisor\n      CounterWeb.Telemetry,\n      # Start the PubSub system\n      {Phoenix.PubSub, name: Counter.PubSub},\n+     Counter.Presence,\n      # Start the Endpoint (http/https)\n      CounterWeb.Endpoint\n      # Start a worker by calling: Counter.Worker.start_link(arg)\n      # {Counter.Worker, arg}\n    ]\n\n    # See https://hexdocs.pm/elixir/Supervisor.html\n    # for other strategies and supported options\n    opts = [strategy: :one_for_one, name: Counter.Supervisor]\n    Supervisor.start_link(children, opts)\n  end\n\n```\n\nThe application doesn't need to know any more about the user count (it might,\nbut not here) so the rest of the code goes into\n`lib/counter_web/live/counter.ex`.\n\n1. We subscribe to and participate in the Presence system (we do that in\n   `mount`)\n2. We handle Presence updates and use the current count, adding joiners and\n   subtracting leavers to calculate the current numbers 'present'. We do that\n   in a pattern matched `handle_info`.\n   Notice that since we populate the socket's state in the `mount/3` callback,\n   and compute the Presence there, we need to remove the connected client\n   from the joins in the `handle_info` callback.\n   We use `Map.pop/3` to remove the client from the joins (note: `Math.pop/3` returns\n   a default map we parse in if `key` is not present in the map).\n   This works because the client is identified by the socket's `id` and Presence\n   process returns a map whose key value is the `socket.id`.\n4. We publish the additional data to the client in `render`\n\n```diff\ndefmodule CounterWeb.Counter do\n  use CounterWeb, :live_view\n  alias Counter.Count\n  alias Phoenix.PubSub\n+ alias Counter.Presence\n\n  @topic Count.topic\n+ @presence_topic \"presence\"\n\n  def mount(_params, _session, socket) do\n\n+   initial_present =\n      if connected?(socket) do\n        PubSub.subscribe(Counter.PubSub, @topic)\n\n+       Presence.track(self(), @presence_topic, socket.id, %{})\n+       CounterWeb.Endpoint.subscribe(@presence_topic)\n+\n+       Presence.list(@presence_topic)\n+       |\u003e map_size\n+     else\n+       0\n+     end\n\n+   {:ok, assign(socket, val: Count.current(), present: initial_present) }\n-   {:ok, assign(socket, val: Count.current()) }\n  end\n\n  def handle_event(\"inc\", _, socket) do\n    {:noreply, assign(socket, :val, Count.incr())}\n  end\n\n  def handle_event(\"dec\", _, socket) do\n    {:noreply, assign(socket, :val, Count.decr())}\n  end\n\n  def handle_info({:count, count}, socket) do\n    {:noreply, assign(socket, val: count)}\n  end\n\n+ def handle_info(\n+       %{event: \"presence_diff\", payload: %{joins: joins, leaves: leaves}},\n+       %{assigns: %{present: present}} = socket\n+    ) do\n+   {_, joins} = Map.pop(joins, socket.id, %{})\n+   new_present = present + map_size(joins) - map_size(leaves)\n+\n+   {:noreply, assign(socket, :present, new_present)}\n+ end\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003c.live_component module={CounterComponent} id=\"counter\" val={@val} /\u003e\n+   \u003c.live_component module={PresenceComponent} id=\"presence\" present={@present} /\u003e\n    \"\"\"\n  end\nend\n```\n\nYou will have noticed that last addition in the `render/1` function invokes a `PresenceComponent`.\nIt doesn't exist yet, let's create it now!\n\nCreate a file with the path:\n`lib/counter_web/live/presence_component.ex`\nand add the following code to it:\n\n```elixir\ndefmodule PresenceComponent do\n  use Phoenix.LiveComponent\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003ch1 class=\"text-center pt-2 text-xl\"\u003eConnected Clients: \u003c%= @present %\u003e\u003c/h1\u003e\n    \"\"\"\n  end\nend\n```\n\nNow, as you open and close your incognito windows to `localhost:4000`, \nyou will get a count of how many are running.\n\n\n![dwyl-liveview-counter-presence-genserver-state](https://github.com/dwyl/phoenix-liveview-counter-tutorial/assets/194400/33220b3e-3d22-42a0-be37-414a1cb0b693)\n\n\u003cbr /\u003e\n\n## More Tests!\n\nOnce you have implemented the solution,\nyou need to make sure that the new code is tested. \nOpen the `test/counter_web/live/counter_test.exs` file\nand add the following tests:\n\n```elixir\ntest \"handle_info/2 Presence Update - Joiner\", %{conn: conn} do\n  {:ok, view, html} = live(conn, \"/\")\n  assert html =~ \"Connected Clients: 1\"\n  send(view.pid, %{\n    event: \"presence_diff\",\n    payload: %{joins: %{\"phx-Fhb_dqdqsOCzKQAl\" =\u003e %{metas: [%{phx_ref: \"Fhb_dqdrwlCmfABl\"}]}},\n                leaves: %{}}})\n  assert render(view) =~ \"Connected Clients: 2\"\nend\n\ntest \"handle_info/2 Presence Update - Leaver\", %{conn: conn} do\n  {:ok, view, html} = live(conn, \"/\")\n  assert html =~ \"Connected Clients: 1\"\n  send(view.pid, %{\n    event: \"presence_diff\",\n    payload: %{joins: %{},\n                leaves: %{\"phx-Fhb_dqdqsOCzKQAl\" =\u003e %{metas: [%{phx_ref: \"Fhb_dqdrwlCmfABl\"}]}}}})\n  assert render(view) =~ \"Connected Clients: 0\"\nend\n```\n\nWith those tests in place, re-running the tests with coverage `mix c`,\nyou should see the following:\n\n```sh\n.........\nFinished in 0.1 seconds (0.04s async, 0.1s sync)\n9 tests, 0 failures\n\nRandomized with seed 958121\n----------------\nCOV    FILE                                        LINES RELEVANT   MISSED\n100.0% lib/counter.ex                                  9        0        0\n100.0% lib/counter/presence.ex                         5        0        0\n100.0% lib/counter_web/components/layouts.ex           5        0        0\n100.0% lib/counter_web/controllers/page_html.ex        5        0        0\n100.0% lib/counter_web/endpoint.ex                    46        0        0\n100.0% lib/counter_web/live/counter.ex                51       13        0\n100.0% lib/counter_web/live/counter_component.e       17        2        0\n100.0% lib/counter_web/live/counter_state.ex          53       12        0\n100.0% lib/counter_web/live/presence_component.        9        2        0\n[TOTAL] 100.0%\n----------------\n```\n\n# _Done_! 🏁\n\nThat's it for this tutorial. \u003cbr /\u003e\nWe hope you enjoyed learning with us! \u003cbr /\u003e\nIf you found this useful, \nplease ⭐️ and _share_ the `GitHub` repo\nso we know you like it!\n\n\n\u003cbr /\u003e\n\n# What's _Next_?\n\nIf you've enjoyed this basic `counter` tutorial\nand want something a bit more advanced,\ncheckout our **`LiveView` _Chat_ Tutorial**:\n[github.com/dwyl/**phoenix-liveview-chat-example**](https://github.com/dwyl/phoenix-liveview-chat-example) 💬 \u003cbr /\u003e\nThen if you want a more advanced \"real world\" App\nthat uses `LiveView` _extensively_\nincluding `Authentication` and some client-side `JS`,\ncheckout our \n**`MVP` App** \n[/dwyl/**mvp**](https://github.com/dwyl/mvp/)\n📱⏳✅ ❤️ \n\n\n\u003cbr /\u003e\u003cbr /\u003e\n\n# _Feedback_ 💬 🙏\n\nSeveral people in the `Elixir` / `Phoenix` community\nhave found this tutorial helpful when starting to use `LiveView`,\ne.g: Kurt Mackey [**`@mrkurt`**](https://github.com/mrkurt)\n[twitter.com/mrkurt/status/1362940036795219973](https://twitter.com/mrkurt/status/1362940036795219973)\n\n![mrkurt-liveview-tweet](https://user-images.githubusercontent.com/194400/109387184-c8707300-78f7-11eb-9f2f-3a13f5433b77.png)\n\nHe deployed the counter app to a 17 region cluster using fly.io: https://liveview-counter.fly.dev\n\n![liveview-counter-cluster](https://user-images.githubusercontent.com/194400/170820493-117751b7-078a-4d4c-9539-33bb5ff8e14d.png)\n\nCode: https://github.com/fly-apps/phoenix-liveview-cluster/blob/master/lib/counter_web/live/counter.ex\n\n\u003e **_Your_ feedback** is always very much **welcome**! 🙏\n\n\u003cbr /\u003e\u003cbr /\u003e\n\n# Credits + Thanks! 🙌\n\nCredit for inspiring this tutorial goes to Dennis Beatty\n[@dnsbty](https://github.com/dnsbty)\nfor his superb post:\nhttps://dennisbeatty.com/2019/03/19/how-to-create-a-counter-with-phoenix-live-view.html\nand corresponding video: [youtu.be/2bipVjOcvdI](https://youtu.be/2bipVjOcvdI)\n\n[![dennisbeatty-counter-video](https://user-images.githubusercontent.com/194400/76142652-953e2f80-6067-11ea-95f7-1efdad619b2f.png)](https://youtu.be/2bipVjOcvdI)\n\nWe recommend _everyone_ learning `Elixir`\n_subscribe_ to his YouTube channel and watch _all_ his videos\nas they are a _superb_ resource!\n\nThe 3 key differences\nbetween this tutorial and Dennis' original post are:\n\n1. **_Complete_ code** commit (snapshot) at the end of each section\n   (_not just inline snippets of code_). \u003cbr /\u003e\n   We feel that having the _complete_ code\n   speeds up learning significantly, especially if (when) we get _stuck_.\n2. **_Latest_ `Phoenix`, `Elixir` and `LiveView`** versions.\n   _Many_ updates have been made to `LiveView` setup since Dennis published his video,\n   these are reflected in our tutorial which uses the _latest_ release.\n3. **_Broadcast updates_** to all connected clients.\n   So when the counter is incremented/decremented in one client,\n   all others see the update.\n   This is the true power and \"WOW Moment\" of `LiveView`!\n\n\u003cbr /\u003e\n\n\n## `Phoenix LiveView` for Web Developers Who Don't know `Elixir`\n\nIf you are new to LiveView (_and have the bandwidth_),\nwe recommend watching\nJames [@knowthen](https://github.com/knowthen) Moore's\nintro to LiveView where he explains the concepts:\n[youtu.be/U_Pe8Ru06fM](https://youtu.be/U_Pe8Ru06fM)\n\n[![phoenix-liveview-intro-](https://user-images.githubusercontent.com/194400/76150088-6d1df300-609e-11ea-8b73-67a263fc762b.png)](https://youtu.be/U_Pe8Ru06fM)\n\nWatching the video is _not required_;\nyou will be able to follow the tutorial without it.\n\n\u003cbr /\u003e\n\nChris McCord (creator of Phoenix and LiveView) has\n[github.com/chrismccord/phoenix_live_view_example](https://github.com/chrismccord/phoenix_live_view_example/tree/e3966aca18f7d8f4da84d197e3ee22af4050fd5e) \u003cbr /\u003e\n![chris-phoenix-live-view-example-rainbow](https://user-images.githubusercontent.com/194400/76169589-9ce9fb00-6171-11ea-9238-2c72287b0eed.png)\nIt's a great collection of examples for people who already understand LiveView.\nHowever we feel that it is not very beginner-friendly\n(_at the time of writing_).\nOnly the default \"_start your Phoenix server_\" instructions are included,\nand the\n[dependencies have diverged](https://github.com/chrismccord/phoenix_live_view_example/issues/56)\nso the app does not _compile/run_ for some people.\nWe understand/love that Chris is focussed _building_\nPhoenix and LiveView so we decided to fill in the gaps\nand write this _beginner-focussed_ tutorial.\n\n\u003cbr /\u003e\n\nIf you haven't watched Chris' Keynote from ElixirConf EU 2019,\nwe _highly_ recommend watching it:\n[youtu.be/8xJzHq8ru0M](https://youtu.be/8xJzHq8ru0M)\n\n[![chris-keynote-elixirconf-eu-2019](https://user-images.githubusercontent.com/194400/59027797-dd6ac000-8851-11e9-82b9-b53c48f7e1b9.png)](https://youtu.be/8xJzHq8ru0M)\n\nAlso read the original announcement for LiveView to understand the hype! \u003cbr /\u003e:\nhttps://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript\n\n\u003cbr /\u003e\n\nSophie DeBenedetto's ElixirConf 2019 talk \"Beyond LiveView:\nBuilding Real-Time features with Phoenix LiveView, PubSub,\nPresence and Channels (Hooks) is worth watching:\n[youtu.be/AbNAuOQ8wBE](https://youtu.be/AbNAuOQ8wBE)\n\n[![Sophie-DeBenedetto-elixir-conf-2019-talk](https://user-images.githubusercontent.com/194400/76205486-3a850f00-61f2-11ea-9503-aec19ee666b5.png)](https://youtu.be/AbNAuOQ8wBE)\n\nRelated blog post: https://elixirschool.com/blog/live-view-live-component/\n\n## Relevant + Recommended Reading\n\n+ Real-Time Form Validation with Phoenix LiveView:\n  https://blog.appsignal.com/2021/09/28/real-time-form-validations-with-phoenix-liveview.html\n+ Optimizing User Experience with LiveView:\n  https://dockyard.com/blog/2020/12/21/optimizing-user-experience-with-liveview\n+ `TDD` with `LiveView`: \n  https://youtu.be/KfW3l3qJPH8\n","projects_url":"https://awesome.ecosyste.ms/api/v1/lists/dwyl%2Fphoenix-liveview-counter-tutorial/projects"}