loading...

Creating a Search app with Vue.js + Parcel + TypeScript: Part 3 of 3

scottlepp profile image Scott Lepper ・6 min read

In Part 2 we added bootstrap-vue and set up a basic layout for a search app. Now we will create components and fetch/display our search results.

In Part 2 we put all our html in our app component. This provided a quick prototype to view our layout, but a real working app will have separate components. Some advantages of separate components are to encapsulate the complexity of each component and in some cases provide reuse of components.

Here is the original template from our app component:

<template>
  <div id="app">
    <b-navbar toggleable="md" type="light" variant="light">
      <b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
      <b-navbar-brand href="#">Zerch</b-navbar-brand>
    </b-navbar>
    <div class="container-fluid">
      <div class="row mx-auto">
        <!-- Search input section -->
        <section class="col-sm-12 pt-3 px-0">
          <b-form inline class="d-flex justify-content-center">
            <!-- Bug in bootstrap-vue - need div around input or button disappears -->
            <div class="col-md-6 col-8 pl-0">
              <b-input class="w-100 mr-sm-2" type="text" placeholder="Enter Search Term"/>
            </div>
            <b-button class="my-2 my-sm-0" type="submit">Search</b-button>
          </b-form>
        </section>
        <!-- Results section -->
        <section class="results">
          <div class="card-columns">
            <div class="card">
              <img class="card-img-top" src="https://dummyimage.com/mediumrectangle/222222/eeeeee" alt="Card image cap">
              <div class="card-body">
                <h5 class="card-title">Card title that wraps to a new line</h5>
                <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
              </div>
            </div>
          </div>
        </section>
      </div>
     </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Our new app component will now look like this:

<template>
  <div id="app">
    <b-navbar toggleable="md" type="light" variant="light">
      <!-- <b-navbar-toggle target="nav_collapse"></b-navbar-toggle> -->
      <b-navbar-brand href="#">Zerch</b-navbar-brand>
    </b-navbar>
    <div class="container-fluid">
      <div class="row mx-auto">
        <!-- Search input section -->
        <section class="col-sm-12 pt-3 px-0">
          <vs-input @search="onSearch"></vs-input>
        </section>
        <!-- Results section -->
        <section class="results">
          <vs-results :data="results"></vs-results>
        </section>
      </div>
     </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Notice we now have vs-input and vs-results tags. Lets create these new components.

We'll create a file called vs-input.vue and add the following code:

<template>
  <b-form inline class="d-flex justify-content-center">
    <!-- Bug in bootstrap-vue - need div around input or button disappears -->
    <div class="col-md-6 col-8 pl-0">
      <b-input v-model="term" class="w-100 mr-sm-2" type="text" placeholder="Enter Search Term"/>
    </div>
    <b-button class="my-2 my-sm-0" @click="search()">Search</b-button>
  </b-form>
</template>

<script lang="ts">
  import { Component, Vue, Provide } from 'vue-property-decorator'
  @Component
  export default class VsInput extends Vue {
    @Provide()
    term = '';
    search() {
      this.$emit('search', this.term);
    }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

So what is this component doing? Capturing input for our search and providing an event to the app component to indicate the user wants to search.

  • b-input element contains the v-model directive. This will bind the input to the "term" variable
  • b-button element has the @click directive to fire the "search" function on click of the button.
  • In our script tag we have our typescript code to declare the term variable and the search function. The search function just emits an event with the term, so the app knows when to perform the search.

Now let's create a results component to show our results. Add a new file called vs-results.vue with the following code:

<template>
  <div class="card-columns" >
    <div class="card" v-for="item in results" :key="item.id">
      <img v-if="item.thumb" class="card-img-top" :src="item.thumb" :alt="item.title" @error="error(item)">
      <div class="card-body">
        <h5 class="card-title">{{item.name}}</h5>
        <p class="card-text" v-html="truncate(item.description || item.abstract, 50)"></p>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop, Provide } from 'vue-property-decorator'

@Component
export default class VsResults extends Vue {

  @Prop()
  data;

  get results() {
    return this.data;
  }

  truncate(text, limit) {
    text = text === undefined ? '' : text;    
    const content = text.split(' ').slice(0, limit);
    return content.join(' ');
  }

  error(item) {
    delete item.thumb;
    this.$forceUpdate();
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Let's first focus on the html above:

  • v-for will iterate over our results array, which is passed in from the app component, as we will see later.
  • The img tag uses the v-if directive to conditionally display a thumbnail, which is bound to the item.thumb property of our search result item.
  • The card title is bound to item.title
  • The card body is bound to item.description or item.abstract. Note here we use the v-html directive since this content may be html and we want to render it as html not just text. We also call a truncate method to keep the text limited.

Now let's look closely at the typescript code:

  • We have a property called data. The app component will pass this in.
  • We have a computed function called results. This is what we reference in our template v-for to iterate over the results.
  • The truncate function will keep our description limited to 50 words.
  • The error function will handle result images that fail to download. This is reference on our img element with the @error directive.

The app.vue component needs to be changed now to handle the event from the vs-input component, perform the search, then pass the results to the vs-results component:

<template>
  <div id="app">
    <b-navbar toggleable="md" type="light" variant="light">
      <!-- <b-navbar-toggle target="nav_collapse"></b-navbar-toggle> -->
      <b-navbar-brand href="#">Zerch</b-navbar-brand>
    </b-navbar>
    <div class="container-fluid">
      <div class="row mx-auto">
        <!-- Search input section -->
        <section class="col-sm-12 pt-3 px-0">
          <vs-input @search="onSearch"></vs-input>
        </section>
        <!-- Results section -->
        <section class="results">
          <vs-results :data="results"></vs-results>
        </section>
      </div>
     </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop, Provide } from 'vue-property-decorator'
import VsResults from './search-results/vs-results.vue';
import VsInput from './search-input/vs-input.vue';
import voyagerApi from './search-results/search-api';

@Component({
  components: {
    VsResults,
    VsInput
  }
})
export default class App extends Vue {

  @Provide() 
  results = [];

  async onSearch(term) {
    this.results = await voyagerApi.search(term);
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode
  • Notice in the template above the vs-input uses the @search directive to bind the onSearch function. This will fire our onSearch function above when vs-input emits the event.
  • The onSearch function will call an api to fetch results. We'll look at this next.
  • Also notice in the template the vs-results :data attribute. This is where the app component will pass the results variable to the vs-results component.

Almost done. Notice above we import voyagerApi. We need to create that. Add a file called search-api.ts with the following which will fetch search results from a solr index.

export default {
  search: async function(term: string): Promise<Array<any>> {
    // solr endpoint
    const host = 'http://voyagerdemo.com/';
    const path = 'daily/solr/v0/select';
    const fields = 'id,name:[name],thumb:[thumbURL],abstract,description'; // fields we want returned
    const api = `${host}${path}?q=${term}&fl=${fields}&wt=json&rows=20`;
    const call = await fetch(api);
    const json = await call.json();
    return json.response.docs;
  }
}
Enter fullscreen mode Exit fullscreen mode

The code above defines an "api" which is the url to a solr index. The fields define which fields we would like returned. The q param passes the "term" the user input and wants to filter the results on. We use the built in "fetch" function to make an ajax call to the api and "await" the results which is a Promise.

You may also notice the search function is an async function. This allows us to use await to make the code appear more synchronous, rather than using Promise.then() syntax.

That's it! Now if you check the browser you should be able to enter a search term and click "Search" an see something like:

Run

If something went wrong you can grab the full working version from here: https://github.com/scottlepp/search-vue-parcel-typescript/tree/final

Discussion

pic
Editor guide