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-searchcd ramda-wikipedia-searchyarn install (or npm install)Copy the code

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

git checkout startCopy the code

And start the project!

yarn start (or npm start)Copy the code

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';Copy the code
const inputElement = document.querySelector('input');Copy the code
inputElement.addEventListener('keyup', event => { console.log('value:', event.target.value); });Copy the code

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';Copy the code
Const getInputValue = pathOr(' ', [' target ', 'value']);Copy the code

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';Copy the code
const getInputValue = (event) => trim(pathOr('', ['target', 'value'], event));Copy the code

Nesting 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';Copy the code
const getInputValue pipe(  pathOr('', ['target', 'value']),  trim);Copy the code

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 en.wikipedia.org/w/api.php?o…

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

En.wikipedia.org/w/api.php?o…

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';Copy the code
const getWikipediaSearchUrlFor = concat('https://en.wikipedia.org/w/api.php?origin=*&action=opensearch&search=');Copy the code

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';Copy the code
const makeUrlFromInput = pipe(  getInputValue,  getUrl,  tap(console.warn));Copy the code
const inputElement = document.querySelector('input');Copy the code
inputElement.addEventListener('keyup', makeUrlFromInput);Copy the code

This new code was 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));Copy the code

Don’t forget to change your event listener too!

inputElement.addEventListener('keyup', searchAndRenderResults);Copy the code

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>`;Copy the code

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:

.Copy the code
import Results from './Results';Copy the code
.Copy the code
const searchAndRenderResults = pipe(  getInputValue,  getUrl,  url =>    fetch(url)    .then(res => res.json())    .then(Results)    .then(console.warn));Copy the code

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');Copy the code
resultsElement.innerHTML = markup; };Copy the code

Replace console.warn with the render function.

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

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();Copy the code
console.log(joined); / / 1, 2, 3Copy the code
console.log(typeof joined); // stringCopy the code

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

const nums = [1, 2, 3]; const msg = `My favorite nums are ${nums}`;Copy the code
console.log(msg); // My favorite nums are 1,2,3Copy the code

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('')}`;Copy the code
console.log(msg); // My favorite nums are 123Copy the code

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>`;Copy the code

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 = () => {};Copy the code

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));Copy the code

Import ifElse and isEmpty from Ramda.

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

Add another function, makeSearchRequestIfValid.

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

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);Copy the code

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.

And holding that clap 👏 button 50x is appreciated as well! ❤ ️

Until next time.

Take care,

Yazeed Bzadough

yazeedb.com/