All Articles

How to use Wikipedia’s search API to build a user interface with RamdaJS

Our Wikipedia Search UI

In this tutorial, we’ll build a UI using Wikipedia’s public search API along with some JavaScript + RamdaJS.

Getting Started

Here’s the GitHub link and Codesandbox link. Open your terminal and pick a directory to clone it.

git clone [https://github.com/yazeedb/ramda-wikipedia-search](https://github.com/yazeedb/ramda-wikipedia-search)
cd ramda-wikipedia-search
yarn install (or npm install)

The master branch has the finished project, so check out the start branch if you wish to code along.

git checkout start

And start the project!

npm start

Your browser should automatically open localhost:1234.

Getting the Input Value

Here’s the initial app.

To capture the user’s input as they type, our input element needs an event listener.

Your src/index.js file is already hooked up and ready to go. You’ll notice we imported Bootstrap for styling.

Let’s add a dummy event listener to get things going.

import 'bootstrap/dist/css/bootstrap.min.css';

const inputElement = document.querySelector('input');

inputElement.addEventListener('keyup', (event) => {
  console.log('value:', event.target.value);
});

We know event.target.value’s the standard way to access an input’s value. Now it shows the value.

How can Ramda help us achieve the following?

  • Grab event.target.value
  • Trim the output (strip leading/trailing whitespace)
  • Default to empty string if undefined

The pathOr function can actually handle the first and third bullet points. It takes three parameters: the default, the path, and the data.

So the following works perfectly

import { pathOr } from 'ramda';

const getInputValue = pathOr('', ['target', 'value']);

If event.target.value is undefined, we’ll get an empty string back!

Ramda also has a trim function, so that solves our whitespace issue.

import { pathOr, trim } from 'ramda';

const getInputValue = (event) => trim(pathOr('', ['target', 'value'], event));

Instead of nesting those functions, let’s use pipe. See my article on pipe if it’s new to you.

import { pathOr, pipe, trim } from 'ramda';

const getInputValue = pipe(
  pathOr('', ['target', 'value']),
  trim
);

We now have a composed function that takes an event object, grabs its target.value, defaults to '', and trims it.

Beautiful.

I recommend storing this in a separate file. Maybe call it getInputValue.js and use the default export syntax.

Getting the Wikipedia URL

As of this writing, Wikipedia’s API search URL is https://en.wikipedia.org/w/api.php?origin=*&action=opensearch&search=

For an actual search, just append a topic. If you need bears, for example, the URL looks like this:

https://en.wikipedia.org/w/api.php?origin=*&action=opensearch&search=bears

We’d like a function that takes a topic and returns the full Wikipedia search URL. As the user types we build the URL based off their input.

Ramda’s concat works nicely here.

import { concat } from 'ramda';

const getWikipediaSearchUrlFor = concat(
  'https://en.wikipedia.org/w/api.php?origin=*&action=opensearch&search='
);

concat, true to its name, concatenates strings and arrays. It’s curried so providing the URL as one argument returns a function expecting a second string. See my article on currying if it’s new!

Put that code into a module called getUrl.js.

Now let’s update index.js. Import our two new modules, along with pipe and tap from Ramda.

import 'bootstrap/dist/css/bootstrap.min.css';
import { pipe, tap } from 'ramda';
import getInputValue from './getInputValue';
import getUrl from './getUrl';

const makeUrlFromInput = pipe(
  getInputValue,
  getUrl,
  tap(console.warn)
);

const inputElement = document.querySelector('input');

inputElement.addEventListener('keyup', makeUrlFromInput);

This new code’s constructing our request URL from the user’s input and logging it via tap.

Check it out.

Making the AJAX Request

Next step is mapping that URL to an AJAX request and collecting the JSON response.

Replace makeUrlFromInput with a new function, searchAndRenderResults.

const searchAndRenderResults = pipe(
  getInputValue,
  getUrl,
  (url) =>
    fetch(url)
      .then((res) => res.json())
      .then(console.warn)
);

Don’t forget to change your event listener too!

inputElement.addEventListener('keyup', searchAndRenderResults);

Here’s our result.

Making a Results Component

Now that we have JSON, let’s create a component that pretties it up.

Add Results.js to your directory.

Look back at our Wikipedia search JSON response. Note its shape. It’s an array with the following indices:

  1. Query (what you searched for)
  2. Array of result names
  3. Array of summaries
  4. Array of links to results

Our component can take an array of this shape and return a nicely formatted list. Through ES6 array destructuring, we can use that as our function signature.

Edit Results.js

export default ([query, names, summaries, links]) => `
  <h2>Searching for "${query}"</h2>
  <ul class="list-group">
    ${names.map(
      (name, index) => `
        <li class="list-group-item">
          <a href=${links[index]} target="_blank">
            <h4>${name}</h4>
          </a>
          <p>${summaries[index]}</p>
        </li>
      `
    )}
  </ul>
`;

Let’s go step by step.

  • It’s a function that takes an array of our expected elements: query, names, summaries, and links.
  • Using ES6 template literals, it returns an HTML string with a title and a list.
  • Inside the <ul> we map names to <li> tags, so one for each.
  • Inside those are <a> tags pointing to each result’s link. Each link opens in a new tab.
  • Below the link is a paragraph summary.

Import this in index.js and use it like so:

// ...

import Results from './Results';

// ...

const searchAndRenderResults = pipe(
  getInputValue,
  getUrl,
  (url) =>
    fetch(url)
      .then((res) => res.json())
      .then(Results)
      .then(console.warn)
);

This passes the Wikipedia JSON to Results and logs the result. You should be seeing a bunch of HTML in your DevTools console!

All that’s left is to render it to the DOM. A simple render function should do the trick.

const render = (markup) => {
  const resultsElement = document.getElementById('results');

  resultsElement.innerHTML = markup;
};

Replace console.warn with the render function.

const searchAndRenderResults = pipe(
  getInputValue,
  getUrl,
  (url) =>
    fetch(url)
      .then((res) => res.json())
      .then(Results)
      .then(render)
);

And check it out!

Each link should open in a new tab.

Removing Those Weird Commas

You may have noticed something off about our fresh UI.

It has extra commas! Why??

Template Literals

It’s all about how template literals join things. If you stick in an array, it’ll join it using the toString() method.

See how this becomes joined?

const joined = [1, 2, 3].toString();

console.log(joined);
// 1,2,3

console.log(typeof joined);
// string

Template literals do that if you put arrays inside of them.

const nums = [1, 2, 3];
const msg = `My favorite nums are ${nums}`;

console.log(msg);
// My favorite nums are 1,2,3

You can fix that by joining the array without commas. Just use an empty string.

const nums = [1, 2, 3];
const msg = `My favorite nums are ${nums.join('')}`;

console.log(msg);
// My favorite nums are 123

Edit Results.js to use the join method.

export default ([query, names, summaries, links]) => `
  <h2>Searching for "${query}"</h2>
  <ul class="list-group">
    ${names
      .map(
        (name, index) => `
        <li class="list-group-item">
          <a href=${links[index]} target="_blank">
            <h4>${name}</h4>
          </a>
          <p>${summaries[index]}</p>
        </li>
      `
      )
      .join('')}
  </ul>
`;

Now your UI’s much cleaner.

Fixing a Little Bug

I found a little bug while building this. Did you notice it?

Emptying the input throws this error.

That’s because we’re sending an AJAX request without a search topic. Check out the URL in your Network tab.

That link points to a default HTML page. We didn’t get JSON back because we didn’t specify a search topic.

To prevent this from happening we can avoid sending the request if the input’s empty.

We need a function that does nothing if the input’s empty, and does the search if it’s filled.

Let’s first create a function called doNothing. You can guess what it looks like.

const doNothing = () => {};

This is better known as noOp, but I like doNothing in this context.

Next remove getInputValue from your searchAndRenderResults function. We need a bit more security before using it.

const searchAndRenderResults = pipe(
  getUrl,
  (url) =>
    fetch(url)
      .then((res) => res.json())
      .then(Results)
      .then(render)
);

Import ifElse and isEmpty from Ramda.

import { ifElse, isEmpty, pipe, tap } from 'ramda';

Add another function, makeSearchRequestIfValid.

const makeSearchRequestIfValid = pipe(
  getInputValue,
  ifElse(isEmpty, doNothing, searchAndRenderResults)
);

Take a minute to absorb that.

If the input value’s empty, do nothing. Else, search and render the results.

You can gather that information just by reading the function. That’s expressive.

Ramda’s isEmpty function works with strings, arrays, objects.

This makes it perfect to test our input value.

ifElse fits here because when isEmpty returns true, doNothing runs. Otherwise searchAndRenderResults runs.

Lastly, update your event handler.

inputElement.addEventListener('keyup', makeSearchRequestIfValid);

And check the results. No more errors when clearing the input!

This tutorial was from my completely free course on Educative.io, Functional Programming Patterns With RamdaJS!

Please consider taking/sharing it if you enjoyed this content.

It’s full of lessons, graphics, exercises, and runnable code samples to teach you a basic functional programming style using RamdaJS.

Thank you for reading ❤️