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:
- Query (what you searched for)
- Array of result names
- Array of summaries
- 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
, andlinks
. - Using ES6 template literals, it returns an HTML string with a title and a list.
- Inside the
<ul>
we mapnames
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 ❤️