Skip to content Skip to navigation

Vue.js

Similarly, you can integrate Boox with a Vue.js application using computed properties and methods to manage the search state and update the UI. Here's a basic example:

Project setup

Create a new Vue.js project using Vite with TypeScript template:

npm create vite@latest boox-vue --template vue-ts
BASH

Now run:

cd boox-vue && npm install
BASH

Install Boox and other dependencies:

npm install -D boox vue-debounce metaphone stemmer stopword @types/stopword
BASH

File structure

boox-vue
├── src
│   ├── components
│   │   ├── Footer.vue
│   │   ├── Search.vue
│   │   ├── SearchInfo.vue
│   │   ├── SearchResult.vue
│   │   └── SearchResults.vue
│   ├── App.vue
│   ├── main.ts
│   └── types.ts
├── index.html
└── vite.config.ts
TEXT

Did you know?

You can remove unnecessary files from the default Vite installation.

Code snippets

Kindly replicate the code snippets provided below:

<!doctype html>
<html lang="en" data-bs-theme="dark">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite example - Boox</title>

    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
      crossorigin="anonymous"
    />
  </head>

  <body class="bg-body-secondary">
    <div id="app" class="vstack min-vh-100"></div>

    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
HTML
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: { external: ['os'] }
  }
})
TS

Below is the content of the src/* files:

<template>
  <main
    class="mx-auto my-4 bg-body w-100 rounded-3 shadow"
    style="max-width: 30rem"
    role="main"
  >
    <Search :model="boox" />
  </main>

  <Footer />
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import Boox from 'boox'
import { metaphone } from 'metaphone'
import { stemmer } from 'stemmer'
import { removeStopwords } from 'stopword'
import Footer from './components/Footer.vue'
import Search from './components/Search.vue'
import type { Pokemon } from './types'

export default defineComponent<{ boox: Boox<Pokemon> }>({
  components: {
    Footer,
    Search
  },
  computed: {
    boox() {
      return new Boox<Pokemon>({
        features: ['name', 'caption', 'set_name'],
        attributes: ['hp', 'image_url'],
        modelOptions: {
          tokenizer(input) {
            return removeStopwords(Array.from(input.match(/\b\w+\b/g) || []))
          },
          stemmer: stemmer,
          phonetic: metaphone
        }
      })
    }
  },
  async mounted() {
    try {
      const response = await fetch(
        'https://stilearning.com/boox/demo/datasets/pokemon-100r.json',
        { cache: 'default' }
      )

      if (!response.ok) {
        throw new Error('Network response was not ok')
      }

      const pokemons: Pokemon[] = await response.json()
      await this.boox.addDocuments(pokemons)
    } catch (error) {
      console.error(error)
      throw error
    }
  }
})
</script>
VUE
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
TS
export interface Pokemon {
  id: string
  image_url: string
  caption: string
  name: string
  hp: number
  set_name: string
}

export interface Result {
  id: string
  image_url: string
  image_alt: string
  hp: number
  name: string
  set_name: string
  caption: string
}
TS

Below is the content of the src/components/* files:

<template>
  <footer class="container mt-auto py-3" role="contentinfo">
    <p class="d-flex justify-content-center text-muted">
      Search by
      <a class="icon-link ms-2" :href="githubLink" target="_blank">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 180 180"
          height="28"
          aria-hidden="true"
        >
          <g fill="none" fill-rule="nonzero">
            <path
              fill="currentColor"
              d="M111.223 75.983 91.937 66.34a3.214 3.214 0 0 0-2.877 0l-19.286 9.643A3.214 3.214 0 0 0 68 78.859v22.5c0 1.219.688 2.333 1.778 2.877l19.285 9.643a3.214 3.214 0 0 0 2.874 0l19.286-9.643A3.214 3.214 0 0 0 113 101.36v-22.5a3.214 3.214 0 0 0-1.777-2.876ZM90.5 72.81l12.099 6.05L90.5 84.908l-12.099-6.05L90.5 72.81ZM74.429 84.06l12.857 6.429v15.313l-12.857-6.429V84.06Zm19.285 21.742V90.489l12.857-6.429v15.313l-12.857 6.429Z"
            ></path>
            <path
              fill="var(--bs-secondary-color)"
              d="M180 90c0-19.343-18-36.203-44.692-45.307C126.203 18 109.343 0 90 0 61.005 0 37.5 40.297 37.5 90c-.01 9.195.825 18.371 2.498 27.412C24.323 110.063 15 99.75 15 90a23.678 23.678 0 0 1 8.288-16.335A154.035 154.035 0 0 1 27 52.552C10.343 62.079 0 75.33 0 90c0 19.343 18 36.203 44.693 45.308C53.797 162 70.657 180 90 180c14.67 0 27.922-10.343 37.448-27a154.035 154.035 0 0 1-21.113 3.683A23.677 23.677 0 0 1 90 165c-9.75 0-20.063-9.322-27.413-24.998A149.798 149.798 0 0 0 90 142.5c49.702 0 90-23.505 90-52.5ZM90 15c9.75 0 20.063 9.323 27.412 24.998A149.798 149.798 0 0 0 90 37.5c-3.06 0-6.08.09-9.06.27a64.89 64.89 0 0 0-7.77 15.75A140.557 140.557 0 0 1 90 52.5a131.4 131.4 0 0 1 33.345 4.155A131.4 131.4 0 0 1 127.5 90c0 5.625-.339 11.245-1.013 16.83a64.89 64.89 0 0 0 15.75-7.77c.175-2.985.263-6.005.263-9.06.01-9.195-.825-18.371-2.498-27.413C155.678 69.938 165 80.25 165 90c0 17.737-30.795 37.5-75 37.5a131.4 131.4 0 0 1-33.345-4.155A131.4 131.4 0 0 1 52.5 90c0-44.205 19.763-75 37.5-75Z"
            ></path>
          </g>
        </svg>
        <span class="fw-bold text-body">Boox</span>
      </a>
    </p>
  </footer>
</template>

<script lang="ts">
export default {
  data() {
    return {
      githubLink: 'https://github.com/bent10/boox'
    }
  }
}
</script>
VUE
<template>
  <form id="search-form" class="p-3" role="search">
    <div class="form-group">
      <input
        type="search"
        class="form-control form-control-lg"
        id="search-query"
        placeholder="Search (e.g. pikachu, water pwer)"
        aria-label="Search pokemons"
        autoFocus
        v-model.debounce.300ms="query"
      />

      <div class="form-text">
        <strong>Note:</strong>
        <span> the </span>
        <code class="bg-body-secondary">pwer</code>
        <span> demonstrates a typing error for </span>
        <code class="bg-body-secondary">power</code>.
      </div>
    </div>
  </form>

  <SearchInfo :results="results" :documents="model.currentState.documents" />

  <SearchResults :results="results" />
</template>

<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
import Boox from 'boox'
import type { Pokemon, Result } from '../types'
import SearchInfo from './SearchInfo.vue'
import SearchResults from './SearchResults.vue'

export default defineComponent({
  components: {
    SearchInfo,
    SearchResults
  },
  props: {
    model: {
      type: Boox<Pokemon>,
      required: true
    }
  },
  setup(props) {
    const query = ref('')
    const results = ref<Result[]>([])

    watch(
      query,
      async newQuery => {
        if (newQuery) {
          const searchResults = await props.model.search(newQuery)

          results.value = searchResults.map(result => {
            const {
              id,
              image_url,
              name: image_alt,
              hp
            } = result.attributes as unknown as Pokemon

            return {
              id,
              image_url,
              image_alt,
              hp,
              name: result.context('name').text,
              set_name: result.context('set_name').text,
              caption: result.context('caption', 80).text
            }
          })
        } else {
          results.value = []
        }
      },
      { immediate: false } // Perform search on initial render
    )

    return { query, results }
  }
})
</script>
VUE
<template>
  <p
    id="results-length"
    class="px-3 text-muted"
    :class="{ 'd-none': !results.length }"
  >
    {{
      results.length
        ? `Found ${results.length} of ${Object.keys(documents).length} pokemons`
        : ''
    }}
  </p>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import type { Documents } from 'boox'
import type { Pokemon, Result } from '../types'

export default defineComponent({
  props: {
    results: {
      type: Array as () => Result[],
      required: true
    },
    documents: {
      type: Object as () => Documents<Pokemon>,
      required: true
    }
  }
})
</script>
VUE
<template>
  <a
    class="list-group-item list-group-item-action d-flex gap-3 py-3"
    :href="result.image_url"
    target="_blank"
    rel="noreferrer"
  >
    <img
      loading="lazy"
      :src="result.image_url"
      :alt="result.image_alt"
      width="32"
      height="32"
      class="rounded-circle flex-shrink-0"
    />
    <div class="d-flex gap-2 w-100 justify-content-between">
      <div>
        <h6 class="mb-1">
          <span v-html="result.name"></span> –
          <span v-html="result.set_name"></span>
        </h6>
        <p class="mb-0 opacity-75"><span v-html="result.caption"></span>...</p>
      </div>
      <small class="opacity-50 text-nowrap">HP {{ result.hp }}</small>
    </div>
  </a>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import type { Result } from '../types'

export default defineComponent({
  props: {
    result: {
      type: Object as () => Result,
      required: true
    }
  }
})
</script>
VUE
<template>
  <div
    id="results"
    class="list-group list-group-flush overflow-auto rounded-3"
    :class="{ 'd-none': !results.length }"
  >
    <SearchResult v-for="result in results" :key="result.id" :result="result" />
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import type { Result } from '../types'
import SearchResult from './SearchResult.vue'

export default defineComponent({
  components: {
    SearchResult
  },
  props: {
    results: {
      type: Array as () => Result[],
      required: true
    }
  }
})
</script>
VUE

Running the app

1. Start the development server:

npm run dev
BASH

Open http://localhost:5173 in your browser to see the search app in action.

2. Build for Production:

npm run build
BASH

Heads up

Remember to consult the documentation for Boox and other libraries for more advanced usage and configuration options.