SpaceRocket |> Blog

Think, Learn, Share

Michael Chavez
Michael Chavez

Add Tags to Post

Posted by Michael Chavez on November 2, 2019

Add Tags to Posts

Game Plan

  1. Create example application
  2. Add dependencies
  3. Customize iex
  4. Create posts context and post schema
  5. Create tag schema
  6. Add belongs_to
  7. Add relations to tag to posts
  8. Add Parent Child Relationship to Tags

Create Example Application

  1. mix phx.new tags_example
  2. cd tags_example && mix ecto.drop -r TagsExample.Repo && mix ecto.create
  3. git init && git add --all && git commit -m "initial commit"

Add dependencies

Arbor: Ecto adjacency list and tree traversal using CTEs. Arbor uses a parent_id field and CTEs to create simple deep tree-like SQL hierarchies. Arbor Github

mix.exs

...
      {:arbor, "~> 1.1"}
...
mix deps.get

Customize iex

.iex.exs

import_if_available Ecto.Query

alias TagsExample.{
    Repo,
    PostTypes,
    PostTypes.Post,
    PostTypes.Tag
}

Create Post Schema

  1. mix phx.gen.html PostTypes Post posts title:unique

Add has_many tags to Post Schema

lib/tags_example/post_types/post.ex

defmodule TagsExample.PostTypes.Post do
  use Ecto.Schema
  import Ecto.Changeset

  alias TagsExample.PostTypes.Tag

  schema "posts" do
    field :title, :string
    many_to_many :tags, Tag, join_through: "posts_tags"

    timestamps()
  end

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title])
    |> validate_required([:title])
    |> unique_constraint(:title)
  end
end

Create Posts Migration

priv/repo/migrations/20191015062600_create_posts.exs

defmodule TagsExample.Repo.Migrations.CreatePosts do
  use Ecto.Migration

  def change do
    create table(:posts) do
      add :title, :string

      timestamps()
    end

    create unique_index(:posts, [:title])
  end
end

Create Tag Schema

  1. mix phx.gen.html PostTypes Tag tags title:unique

Add has_many Post to Tag Schema

lib/tags_example/post_types/tag.ex

defmodule TagsExample.PostTypes.Tag do
  use Ecto.Schema
  import Ecto.Changeset

  alias TagsExample.PostTypes.Post
  alias TagsExample.PostTypes.Tag

  schema "tags" do
    field :title, :string
    
    many_to_many :posts, Post, join_through: "posts_tags"

    timestamps()
  end

  @doc false
  def changeset(tag, attrs) do
    tag
    |> cast(attrs, [:pid, :title, :order, :slug])
    |> validate_required([:title, :order, :slug])
    |> unique_constraint(:title)
  end
end

Create Tags Migration

priv/repo/migrations/20191015062608_create_tags.exs

defmodule TagsExample.Repo.Migrations.CreateTags do
  use Ecto.Migration

  def change do
    create table(:tags) do
      add :title, :string

      timestamps()
    end

    create unique_index(:tags, [:title])
  end
end

Create posts_tags Schema

  1. mix phx.gen.schema PostTypes.PostTag posts_tags post_id:references:posts tag_id:references:tags

lib/tags_example/post_types/post_tag.ex

defmodule TagsExample.PostTypes.PostTag do
  use Ecto.Schema
  alias TagsExample.PostTypes.Post
  alias TagsExample.PostTypes.Tag

  schema "posts_tags" do
    belongs_to :posts, Post
    belongs_to :tags, Tag 

    timestamps()
  end

end

Create Post Tags Migration

priv/repo/migrations/20191015062634_create_posts_tags.exs

defmodule TagsExample.Repo.Migrations.CreatePostTags do
  use Ecto.Migration

  def change do
    create table(:posts_tags) do
      add :post_id, references(:posts)
      add :tag_id, references(:tags)

      timestamps()
    end

    create index(:posts_tags, [:post_id])
    create index(:posts_tags, [:tag_id])
  end
end

Mix Ecto Migrate

mix ecto.migrate

Seed Data

  • Education

    • Universities
    • Colleges
    • High Schools
  • Stores

    • Jewelry
    • Clothes

priv/repo/seeds.exs

alias TagsExample.Repo
alias TagsExample.PostTypes.{Tag, Post}

phx_tag = Repo.insert!(%Tag{ title: "Phoenix" })


Repo.insert(
  %Post{
    title: "Hello World",
    tags: [phx_tag]
  }
)
mix run priv/repo/seeds.exs

Check it out

iex -S mix
posts = Repo.all(from p in Post, preload: :tags)
tags = Repo.all(from c in Tag, preload: :posts)
posts = Repo.all(p in Post, preload: :tags)

Adding to template

Index Page

lib/tags_example/post_types.ex

  def list_posts do
    Repo.all(from p in Post, preload: :tags)
  end

lib/tags_example_web/templates/post/index.html.eex

        <%= for tag <- post.tags do %>
            <%= tag.title %>
        <%= end %> 

Show Page

lib/tags_example/post_types.ex

  def get_post!(id), do: Repo.get!(Post, id)
  |> Repo.preload([:tags])

lib/tags_example_web/templates/post/show.html.eex

<ul class="tags">
  <strong>Tags:</strong>
    <%= for tag <- @post.tags do %>
    <li><%= tag.title %></li>
    <%= end %>
</ul>

assets/css/app.css

ul.tags {
  display: inline;
  margin: 0;
  padding: 0;
}

ul.tags li {
  display: inline;
  list-style: none;
  margin: 0;
  padding: 0;
}

ul.tags li:after {
  content: ", ";
  color: #aaa;
}

ul.tags li:last-child:after {
  content: "";
}

To be continued…