Creating a real-time Trello board with Phoenix LiveView-Part-1

Photo by Alphabag on Unsplash

Creating a real-time Trello board with Phoenix LiveView-Part-1

ยท

9 min read

Trello cards help you to collaborate, manage projects, and reach new productivity peaks(As per their official Website ๐Ÿ˜Š). You may have used it earlier or some similar board like Jira to manage projects and tasks. When I joined one of my previous organizations, my training was tracked using the Trello board which helped me and other new joiners in learning and track our progress. In this Blog, we are about to create a similar board with a real-time update feature.

What are going to build?

We are going to build a Trello look-alike board. It will have different cards belonging to different sections. We can drag and drop cards to different sections with a smooth transition. Along with the drag-and-drop transition of cards to different sections, the whole experience will be real-time. If any user makes any change in the card section then any other user watching the application screen doesn't have to explicitly refresh the browser to see the effect. Refer to the gif below to get an idea.

tasker.gif

Blog series!!!

This is a 3 part blog series.

  • Part 1: We will be generating the application, adding the different tables, and the API layer, and we will test the API layer in the iex shell (This is Part 1)
  • Part 2: We will build the board, add tasks UI, and integrate the board with the functions (API) we created in Part 1
  • Part 3: We will be adding real-time behavior to our board (under development)

Action Plan

  • Creating a LiveView project
  • Adding User and Task Tables.
  • Adding Task and User struct
  • Designing the API Layer

Let's start building ๐Ÿ‘ท

Creating LiveView project

  • First of all, our system should have Elixir installed. We can confirm this by running Elixir -v.
  • Once confirmed we need to create/generate the Phoenix LiveView application.
  • This can be done by running mix phx.new trello_app --live
  • if you get this error
    ** (Mix) The task "phx.new" could not be found
    Note no mix.exs was found in the current directory
    
    Then run mix archive.install hex phx_new
  • Then navigate to project directory cd trello_app and run mix phx.server. Once, the project starts running navigate to localhost:4000 to see the welcome phoenix screen.

Adding User and Task Tables.

  • As per our requirement, we have two entities that need to be created, the card and the user to which this card belongs. We will call these cards Task.
  • So, we will add two tables the User table and the Task table.
  • Let's add migration files for these two Tables.
  • For the sake of clarity we will add two separate migration files for these two tables.
  • For User table run mix ecto.gen.migration create_user and for Task table run mix ecto.gen.migration create_task
  • Now open trello_app/priv/repo/migrations/{{some_time_stamp}}_create_user.exs migration file at and the add following fields in the migration file.

    defmodule TrelloApp.Repo.Migrations.CreateUser do
      use Ecto.Migration
    
      def change do
         create table(:users) do
            add :first_name, :string, null: false
            add :last_name, :string, null: false
    
            timestamps()
         end
      end
    end
    
  • Similarly for task table open trello_app/priv/repo/migrations/{{some_time_stamp}}_create_task.exs migration file and the following fields

    defmodule TrelloApp.Repo.Migrations.CreateTask do
       use Ecto.Migration
    
       def change do
          create table(:tasks) do
             add :title, :string, null: false
             add :description, :text
             add :user_id, references(:users)
             add :state, :string, null: false
    
             timestamps()
         end
     end
    end
    
  • Now we will now run the command mix ecto.migrate. This will create tables User and Task in the database.

Adding Task and User struct

  • We have created User and Task tables now we are going to add User and Task structs so that we can use Ecto queries effectively.
  • First, we will add the Task struct. For that we will create a file trello_app/lib/trello_app/organization/task.ex.
  • Add the following code to the file.

    defmodule TrelloApp.Organization.Task do
      use Ecto.Schema
      import Ecto.Changeset
    
      alias TrelloApp.Organization.{ Task, User }
    
      schema "tasks" do
         field :title, :string
         field :description, :string
         field :state, :string
    
         belongs_to :user, User
    
         timestamps()
      end
    
      def changeset(%Task{} = task, attrs) do
        task
        |> cast(attrs, [:title, :description, :state, :user_id])
      end
    end
    
  • Similarly, we will add the User struct in the same organization directory with a file name user.ex.
  • We will add a similar code here as well.

    defmodule TrelloApp.Organization.User do
      use Ecto.Schema
      import Ecto.Changeset
    
      alias TrelloApp.Organization.{ Task, User }
      schema "users" do
        field :first_name
        field :last_name
    
        has_many :task, Task
        timestamps()
      end
    
      def changeset(%User{} = user, attrs) do
         user
         |> cast(attrs, [:first_name, :last_name])
      end
    end
    
  • In the above code, both the structs i.e User and Task has fields defined in the schema macro similar to the migration files.
  • We've also defined the relationship between the structs(Tables). It is a one-to-many relationship between User and Task which means User can have many Task. We've also defined the changeset functions for manipulating the data.
  • Now, to test if everything is working fine we will add some dummy data in both the tables using the iex shell.
  • Open the terminal and navigate to the root directory level of the project. Now run the iex shell by typing iex -S mix.
  • Now alias both the structs and Repo to avoid typing long struct names.
    alias TrelloApp.Organization.{ Task, User }
    
  • First, we will insert the User record for that we will run the following code.

    User.changeset(%User{}, %{first_name: "Michael", last_name: "Jordan"}) 
    |> Repo.insert
    
    User.changeset(%User{}, %{first_name: "Andrew", last_name: "Flintoff"}) 
    |> Repo.insert
    
  • This will insert two User's records with id of 1 and 2.
  • Similarly, we will add a few Task records for both users but with different states. For testing purposes, we will add four tasks with the states "planning", "progress", and "completed".

    %Task{} 
    |> Task.changeset(%{title: "Designing API", description: "designing api", state: "planning", user_id: 1}) 
    |> Repo.insert
    
    %Task{} 
    |> Task.changeset(%{title: "Take Backup", description: "take backup", state: "progress", user_id: 2}) 
    |> Repo.insert
    
    %Task{} 
    |> Task.changeset(%{title: "Add Migrations", description: "write migrations", state: "progress", user_id: 1}) 
    |> Repo.insert
    
    %Task{} 
    |> Task.changeset(%{title: "Write Script", description: "write script", state: "completed", user_id: 2}) 
    |> Repo.insert
    
  • After running the above queries we will have one User record and three Task records.

Designing the API Layer

  • As per our requirement we only want two functions in our API Layer, which are get_grouped_tasks and change_task_state.
  • What get_grouped_tasks will do?

  • If you remember in the Task struct we have a state field. We've added three tasks records with state planning, progress, and completed.
  • We want our tasks should be grouped (as per the state mentioned in the task) and placed in the respective states column. grouped_tasks (1).png
  • In this function, we will have three stages of data transformation
    • Fetch all tasks
    • Preload the owner of the tasks (Just for beautification of the task to mention the owner of the task)
    • Grouping the tasks
  • Below is the code of the above pseudo-code.
    def get_grouped_tasks() do
      Task
      |> Repo.all()
      |> Repo.preload(:user)
      |> Enum.group_by(fn %{state: state} -> state end)
    end
    
  • Our second method will be change_task_state.
  • What change_task_state will do?

    add_task (1).png
  • When we move our task (say) from Progress to Completed, we want to change the state field of the task from progress to completed.
  • This function will take two parameters task_id and transition_state i.e the column we want our task should be moved.
  • In this function, we will have three stages of data transformation
    • Get task by task_id parameter
    • Change the state of the task as per provided transition_state using changeset function in Task struct
    • Update the Task
  • Below is the code of the above pseudo-code.
    def change_task_state(task_id, transition_state) do
      Task
      |> Repo.get(task_id)
      |> Task.changeset(%{state: transition_state})
      |> Repo.update
    end
    
  • We are going to place both functions in a separate module organization.ex under trello_app/lib/trello_app folder.
  • This module will be the provider of the two functions, which we will use in the LiveView module later.

    defmodule TrelloApp.Organization do
      alias TrelloApp.Repo
      alias TrelloApp.Organization.Task
    
      def get_grouped_tasks() do
        Task
        |> Repo.all()
        |> Repo.preload(:user)
        |> Enum.group_by(fn %{state: state} -> state end)
      end
    
      def change_task_state(task_id, transition_state) do
        Task
        |> Repo.get(task_id)
        |> Task.changeset(%{state: transition_state})
        |> Repo.update
      end
    end
    

Testing the API Layer in the iex shell

  • We've created our Task and User structs as well as designed our API Layer. Let's test the API Layer in the iex shell.
  • If you remember, we've added a few records of task and user.
  • So, as per our API layer, we have to functions get_grouped_tasks/0 and change_task_state/2. First, we will test get_grouped_tasks/0,
  • For testing, we will alias the Organization module
      alias TrelloApp.Organization
    
  • Now run Organization.get_grouped_tasks(), this will return grouped tasks as per their states
    %{
    "planning" => [
        %TrelloApp.Organization.Task{
          __meta__: #Ecto.Schema.Metadata<:loaded, "tasks">,
          description: "designing api",
          id: 2,
          inserted_at: ~N[2022-08-25 09:30:08],
          state: "planning",
          title: "Designing API",
          updated_at: ~N[2022-08-25 09:30:08],
          user: %TrelloApp.Organization.User{
            __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
            first_name: "Michael",
            id: 1,
            inserted_at: ~N[2022-08-25 08:45:25],
            last_name: "holding",
            tasks: #Ecto.Association.NotLoaded<association :tasks is not loaded>,
            updated_at: ~N[2022-08-25 08:45:25]
          },
          user_id: 1
        }
    ],
    "progress" => [
        // two tasks under progress
        %TrelloApp.Organization.Task{ id: 2 },  
        %TrelloApp.Organization.Task{ id: 3 }
     ],
     "completed" => [
        // one task under completed
        %TrelloApp.Organization.Task{id: 4}
     ]
    }
    
  • We got one task for planning, one task for completed, and two for progress. This is exactly as per the tasks we added with different states.
  • So far so good, now we will test change_task_state for first task i.e id=1.
  • change_task_state/2 expects two parameters task_id and transtion_state.
  • We will move our task with id=1 to the completed state. So, we will run Organization.change_task_state(1, 'completed') in our shell.
  • This will transition our task of id=1 to the completed column. We can test this by again running the get_grouped_task. It will not return the planning group since there is no task left with the planning state.

This completes the first blog of this series. In the next part as per our planning, we will build the board, add tasks UI along with drag and drop functionality and will integrate the board with our API layer. I hope you like this blog. If you have any questions then please comment below. Thanks for reading ๐Ÿ˜Š.

References

Did you find this article valuable?

Support AbulAsar S. by becoming a sponsor. Any amount is appreciated!