Build a Message Board App in Vue

GraphQL Queries in Apollo Vue

With Apollo Client set up and connected to your Dgraph Cloud backend, and Vue routing set up, you can move on to the GraphQL queries that get the data to render the main pages.

You’ll use the Apollo Client to automatically query your endpoint.

GraphQL queries

You’ll use the query below to query posts:

query allPosts{
  queryPost(order: { desc: datePublished }) {
    id
    title
    tags
    datePublished
    category {
      id
      name
    }
    author {
      username
      displayName
      avatarImg
    }
    commentsAggregate {
      count
    }
  }
}

Let’s save this post as a gql file as src/graphql/queries/posts.gql.

Next, let’s create a PostFeed componenent that uses the query above and renders data into a Semantic UI Table.

//components/PostFeed.vue
<template>
  <ApolloQuery :query="require('@/graphql/queries/posts.gql')">
    <template v-slot="{ result: { loading, error, data}}">
      <sui-loader v-if="loading" active />

      <sui-container v-else-if="error" text class="mt-24">
        <sui-header as="h1">Ouch! That page didn't load</sui-header>
        <p>Here's why : {{error.message}}</p>
      </sui-container>

      <sui-table v-else basic="very">
        <sui-table-header>
          <sui-table-row>
            <sui-table-header-cell>Posts</sui-table-header-cell>
            <sui-table-header-cell>Category</sui-table-header-cell>
            <sui-table-header-cell>Tags</sui-table-header-cell>
            <sui-table-header-cell>Responses</sui-table-header-cell>
          </sui-table-row>
        </sui-table-header>

        <sui-table-body v-if="data">
          <sui-table-row 
            v-for="post in data.queryPost"
            :key="post.id"
          >
            <sui-table-cell>
              <router-link
                :to="{ pathname: '/post/' + post.id }"
              >
                <sui-header as="h4" image>
                  <sui-image :src="getAvatarUrl(post.author.avatarImg)" rounded size="mini" />
                  <sui-header-content>
                    {{post.title}}
                    <sui-header-subheader>{{post.author.displayName}}</sui-header-subheader>
                  </sui-header-content>
                </sui-header>
              </router-link>
            </sui-table-cell>
            <sui-table-cell>
              <span class="ui red empty mini circular label"></span>{{" "}}
              {{" " + post.category.name}}
            </sui-table-cell>
            <sui-table-cell>
              <template v-if="post.tags">
                <sui-label 
                  v-for="tag in post.tags.trim().split(/\s+/).filter(t => t)"
                  :key="tag"
                >
                  {{tag}}
                </sui-label>
              </template>
            </sui-table-cell>
            <sui-table-cell>
              <p 
                v-for="likes in [Math.floor(Math.random() * 10)]" 
                :key="likes"
              >
                <i class="heart outline icon"></i> 
                {{ likes }} Like{{likes === 1 ? "" : "s"}}
              </p>
              <p
                v-for="replies in [post.commentsAggregate.count]"
                :key="replies"
              >
                {{" "}}
                <i class="comment outline icon"></i> {{replies}}{{" "}}
                {{replies === 1 ? "Reply" : "Replies"}}
              </p>
            </sui-table-cell>
          </sui-table-row>
        </sui-table-body>

      </sui-table>
    </template>
  </ApolloQuery>
</template>

<script>
export default {
}
</script>

There’s some layout and CSS styling in there, but the actual data layout is just indexing into the queried data with post.title, post.author.displayName, etc. Note that the title of the post is made into a link with the following:

<router-link to={{pathname: "/post/" + post?.id}}> ... </router-link>

When clicked, this link will go through the Vue router to render the post component.

You can add whatever avatar links you like into the data, and you’ll do that later in the tutorial after you add authorization and logins; but for now, add a method into the methods portion of the vue object, and fill it with this function that uses random avatars we’ve supplied with the app boilerplate, as follows:

export default {
  methods: {
    getAvatarUrl: (img) => img ?? "/" + Math.floor(Math.random() * (9 - 1) + 1) + ".svg"
  }
}

Then, update the src/views/Home.vue component to render the post list, as follows:

<template>
  <div class="layout-margin">
    <PostFeed />
  </div>
</template>

<script>
import PostFeed from '@/components/PostFeed'

export default {
  name: 'Home',
  components: { PostFeed }
}
</script>

With this much in place, you will see a home screen (start the app with npm run serve if you haven’t already) with a post list of the sample data you have added to your Dgraph Cloud database.

post list component

Each post title in the post list is a link to /post/0x... for the id of the post. At the moment, those like won’t work because there’s not component to render the post. Let’s add that component now.

Layout of a post with GraphQL

Add a GraphQL query that gets a particular post by it’s id to src/graphql/queries/post.gql with this GraphQL query.

query getPost($id: ID!) {
  getPost(id: $id) {
    id
    title
    text
    tags
    datePublished
    category {
      id
      name
    }
    author {
      username
      displayName
      avatarImg
    }
    comments {
      id
      text
      commentsOn {
        comments {
          id
          text
          author {
            username
            displayName
            avatarImg
          }
        }
      }
      author {
        username
        displayName
        avatarImg
      }
    }
  }
}

Now we can start to create the post component.

const { id } = useParams<PostParams>()

const { data, loading, error } = useGetPostQuery({
  variables: { id: id },
})

Laying out the post component is then a matter of using the data from the ApolloQuery to layout an interesting UI. Edit the src/views/Post.vue component, so it lays out the post’s data like this:

<template>
  <ApolloQuery 
    :query="require('@/graphql/queries/post.gql')"
    :variables="{ id: $route.params.id }"
  >
    <template v-slot="{ result: { loading, error, data}}">
      <sui-loader v-if="loading" active />

      <sui-container v-else-if="error" text class="mt-24">
        <h1 is="sui-header">Ouch! That page didn't load</h1>
        <p>Here's why : {{error.message}}</p>
      </sui-container>

      <sui-container v-else-if="!data || !data.getPost" text class="mt-24">
        <h1 is="sui-header">This is not a post</h1>
        <p>You've navigated to a post that doesn't exist.</p>
        <p>That most likely means that the id {{$route.params.id}} isn't the id of post.</p>
      </sui-container>

      <div v-else-if="data" class="layout-margin">
        <div>
          <h1 is="sui-header">{{data.getPost.title}} </h1>
          <span class="ui red empty mini circular label"></span>
          {{" " + data.getPost.category.name + "  "}}

          <template v-if="data.getPost.tags">
            <sui-label
              v-for="tag in data.getPost.tags.trim().split(/\s+/)"
              :key="tag"
              as="a" basic color="grey"
            >
              {{tag}}
            </sui-label>
          </template>
        </div>

        <h4 is="sui-header">
          <sui-image
            :src="getAvatarUrl(data.getPost.author.avatarImg)"
            rounded
            size="mini"
          />
          <div class="content">
            {{data.getPost.author.displayName}}
            <sui-header-subheader>{{getDateStr(data.getPost)}}</sui-header-subheader>
          </div>
        </h4>
        
        <p 
          v-for="(para,index) in data.getPost.text.split('\n')"
          :key="index"
        >
          {{para}}
        </p>

        <div class="mt-3">
          <sui-comment-group
            v-for="comment in data.getPost.comments" 
            :key="comment.id"
          >
            <sui-comment>
              <sui-comment-avatar
                :src="getAvatarUrl(comment.author.avatarImg)"
                size="mini"
              />
              <sui-comment-content>
                <sui-comment-author as="a">
                  {{comment.author.displayName}}
                </sui-comment-author>
                <sui-comment-text>{{comment.text}}</sui-comment-text>
              </sui-comment-content>
            </sui-comment>
          </sui-comment-group>
        </div>
      </div>
    </template>
  </ApolloQuery>
</template>


<script>
import { DateTime } from "luxon"

export default {
  methods: {
    getAvatarUrl: (img) => img ?? "/" + Math.floor(Math.random() * (9 - 1) + 1) + ".svg",

    getDateStr(post) {
      let dateStr = "at some unknown time"

      if (post.datePublished) {
        dateStr = DateTime.fromISO(post.datePublished).toRelative() ?? dateStr
      }

      return dateStr;
    }
  }
}
</script>

Now you can click on a post from the home screen and navigate to its page.

post component

This Step in GitHub

This step is also available in the tutorial GitHub repo and is this code diff.

If you have the app running (yarn start) you can navigate to http://localhost:3000 to see the post list on the home screen and click on a post’s title to navigate to the post’s page. In the diff, we’ve added a little extra, like the Diggy logo, that’s also a link to navigate you home.