Starting Simple

Using a GraphQL API from the client side doesn’t necessitate a big framework or a lot of tooling. A plain old JavaScript script will do. We’re going to start by building the simplest of JavaScript projects just to illustrate how to configure a basic client.

We’ll set this up as a separate project from the PlateSlate Phoenix-based application. As noted in System Dependencies, you’re going to need Node.js,[32] so if you don’t already have a working installation, now’s the time to get it set up and running. We recommend installation by using your favorite package manager, or a Node.js version manager if you see yourself using different versions in the future.

Once you have Node.js installed, you should be able to execute commands using the Node Package Manager (npm). Something like this should work:

 $ ​​npm​​ ​​--version
 3.10.3

Now let’s install yarn,[33] which will make management of our project dependencies easier:

 $ ​​npm​​ ​​install​​ ​​-g​​ ​​yarn

We’ll make a new directory for our project (alongside rather than inside our Elixir PlateSlate application), change to that directory, and instruct yarn to bootstrap it with a basic package.json file to keep track of our project metadata and dependencies:

 $ ​​mkdir​​ ​​plate-slate-basic-ui
 $ ​​cd​​ ​​plate-slate-basic-ui
 $ ​​yarn​​ ​​init​​ ​​-y

The -y option here tells yarn just to assume we typed y (yes) to the various questions it would normally ask.

If you open up package.json in an editor, you’ll see the basic boilerplate content that yarn added:

 {
 "name"​: ​"plate-slate-basic-ui"​,
 "version"​: ​"1.0.0"​,
 "main"​: ​"index.js"​,
 "license"​: ​"MIT"
 }

There’s not a lot here yet; we’ll be adding a bit to this file shortly.

Now, let’s create a subdirectory within our project that will hold the HTML and JavaScript files we’ll be adding:

 $ ​​mkdir​​ ​​public

Dashes and Underscores

images/aside-icons/info.png

We use dashes instead of underscores when creating JavaScript projects because it’s common JavaScript package naming style, and it makes it easy to identify Elixir vs. JavaScript projects from directory listings.

 

Inside that directory, we’ll create a basic HTML document—something that we’ll expand on later:

 <!doctype html>
 <html lang=​"en"​>
  <head>
  <title>PlateSlate (Basic UI)</title>
  </head>
  <body>
  Hi.
  </body>
 </html>

For the moment, we have it just greet the viewer; it doesn’t hurt to be polite, and it would be nice to see something besides a blank page when we check to make sure we’re serving the page. We could do that with a number of different tools, but since we’re already using Node.js, let’s configure a tiny static web server to make index.html available via a localhost port.

Adding a Static Web Server

The most common web server utility package for Node.js projects is Express,[34] and configuring it to serve a directory of static assets is pretty straightforward.

First, we need to add it as a development dependency using yarn:

 $ ​​yarn​​ ​​add​​ ​​express​​ ​​--dev

Now that we have Express available, let’s write the configuration we need in our index.js file. We’ll configure it to serve up our public/ directory on port 3000:

 const​ express = require(​'express'​);
 const​ app = express();
 
 app.use(express.​static​(​'public'​));
 app.listen(3000, () => console.log(​'Listening on port 3000!'​))

To make kicking off this little web server as easy as possible, let’s add a script entry to our package.json file. It will just execute node ./ (which will run the script defined by the main setting—our index.js file):

 {
 "name"​: ​"plate-slate-basic-ui"​,
 "version"​: ​"1.0.0"​,
 "main"​: ​"index.js"​,
 "license"​: ​"MIT"​,
 "devDependencies"​: {
 "express"​: ​"^4.16.2"
  },
 "scripts"​: {
 "dev"​: ​"node ./"
  }
 }

This lets us use yarn dev to kick off our web server:

 $ ​​yarn​​ ​​dev
 Listening on port 3000!

If we check http://localhost:3000 in our browser, we’ll see our Hi!.

So we have a (very) basic static web server set up, just using Node.js! Our web application is just a bit underwhelming at the moment. To test our ability to access data from our GraphQL API, we’d like to pull the list of menu items and display them.

Fetching a GraphQL Query

Let’s shut down our fancy “Hi!” web server and work on our web application a bit more. We’ll start by modifying our index.html document to do two things:

  • Load a polyfill for the browser fetch[35] API
  • Load our application code, which we’ll put at public/js/app.js

Polyfill

images/aside-icons/note.png

A web polyfill is a piece of code that acts as a shim, adding a capability to older or less advanced browsers so that applications can transparently use modern browser APIs without needing to work around issues with browser-specific support.

This will involve two script tags—and we might as well remove our pithy greeting while we’re at it:

 <!doctype html>
 <html lang=​"en"​>
  <head>
  <title>PlateSlate (Basic UI)</title>
» <script src=​"https://cdn.jsdelivr.net/npm/whatwg-fetch"​></script>
» <script src=​"/js/app.js"​></script>
  </head>
  <body>
 
  </body>
 </html>

We’ll define three named functions in our JavaScript application—first, the piece that uses fetch() to retrieve the menu items from our GraphQL API. Unsurprisingly, we’ll call it fetchMenuItems():

 function​ fetchMenuItems() {
 return​ window.fetch(​'http://localhost:4000/api'​, {
  method: ​'POST'​,
  headers: {
 'Content-Type'​: ​'application/json'
  },
  body: JSON.stringify({
  query: ​'{ menuItems { name } }'
  })
  }).then(​function​(response) {
 return​ response.json();
  });
 }

Notice this looks like a completely normal HTTP POST—because it is. Since we’re doing a query operation to get our menu items, we could be using GET, but it’s easier and more consistent to encode our GraphQL document as part of an application/json POST body. We return the result of fetch(), which just happens to be a JavaScript Promise—an object that represents the completion or failure of an asynchronous action.

If fetchMenuItems() is unsuccessful, we want to display a simple message to users and log the details to the console. We’ll do that with a function, displayFetchError():

 function​ displayFetchError(response) {
 var​ element = document.createElement(​'p'​);
  element.innerHTML = ​'Could not contact API.'​;
  console.error(​"Fetch Error"​, response);
  document.body.appendChild(element);
 }

If fetchMenuItems() is successful, however, we’ll take a look at the result from our GraphQL API and then show the appropriate information:

 function​ displayMenuItems(result) {
 var​ element;
 if​ (result.errors) {
 var​ element = document.createElement(​'p'​);
  element.innerHTML = ​'Could not retrieve menu items.'​;
  console.error(​"GraphQL Errors"​, result.errors);
  } ​else​ ​if​ (result.data.menuItems) {
 var​ element = document.createElement(​'ul'​);
  result.data.menuItems.forEach(​function​(item) {
 var​ itemElement = document.createElement(​'li'​);
  itemElement.innerHTML = item.name;
  element.appendChild(itemElement);
  });
  }
  document.body.appendChild(element);
 }

It’s possible that the GraphQL API might return an error, so we deal with that similarly to how we handled a fetchMenuItems() failure: we’ll display a simple error message and log the details. It’s much more likely that we’ll get a list of menu items back, however—and in that case, we’ll build up a list of the menu items and display them for our users.

You’ll notice that in both our failure and success cases, we’re manipulating the HTML Document Object Model (DOM). To do this, our application script needs to make sure that our document is fully loaded—so we can safely muck around with the contents of its body.

We can do this by watching for the DOMContentLoaded event with an event listener:

 document.addEventListener(​'DOMContentLoaded'​, ​function​() {
 // Stuff to do once the page is loaded
 });

We’ll go ahead and wire in our fetchMenuItems() there. We’ll configure the resulting Promise object to trigger displayMenuItems() when it’s successful, and displayFetchError() when it fails:

 document.addEventListener(​'DOMContentLoaded'​, ​function​() {
» fetchMenuItems()
» .then(displayMenuItems)
» .​catch​(displayFetchError);
 });

If we were to run both our PlateSlate GraphQL API (on port 4000) and our little web server (on port 3000) right now, we’d run full tilt into an issue with browser security, and we’d see something like this:

images/chp.frontend/no-cors.png

First off, congratulate yourself for a nicely inserted “Could not contact API” message, just as we planned for fetch errors.

The error messages you see in the console here are because we need to use cross-origin resource sharing (CORS)[36] to contact our server.

Configuring for CORS

CORS is a mechanism used by browsers and servers to allow resources with certain restrictions to be requested from another location on the web. It does this using a series of HTTP headers whose contents lay out a server’s specific rules about what resources can be accessed and how.

The default rule for asynchronous JavaScript requests limits access to a same-origin policy, and because our web application isn’t being served up by our PlateSlate itself, we need to configure our server to respond to the CORS-preflight requests appropriately, allowing cross-origin requests. Thankfully, there’s a plug for that—cors_plug.[37] We’ll use it directly in our Phoenix endpoint, with the default options.

Restrict Your CORS

images/aside-icons/tip.png

You can restrict access to your server to a limited set of origin domains and HTTP methods by passing additional options to plug CORSPlug. Check the documentation for the cors_plug package for more information.

First, let’s add the package to the list of declared dependencies in our PlateSlate mix.exs (the server, not our JavaScript UI, of course):

 defp​ deps ​do
  [
 # Other deps
» {​:cors_plug​, ​"​​~> 1.5"​},
  ]
 end

We make sure we stop our PlateSlate Elixir application if it’s still running, and install the package using mix:

 $ ​​mix​​ ​​deps.get

Once that is downloaded and ready to go, let’s add the plug to our endpoint, which lives under lib/plate_slate_web. We will put it right after the socket configuration:

 defmodule​ PlateSlateWeb.Endpoint ​do
 use​ Phoenix.Endpoint, ​otp_app:​ ​:plate_slate
 use​ Absinthe.Phoenix.Endpoint
 
  socket ​"​​/socket"​, PlateSlateWeb.UserSocket
 
» plug CORSPlug
 
 # Rest of configuration
 end

We also need to tweak our web application to use CORS. We do this by setting the mode option for fetch():

 function​ fetchMenuItems() {
 return​ window.fetch(​'http://localhost:4000/api'​, {
  method: ​'POST'​,
» mode: ​'cors'​,
  headers: {
 'Content-Type'​: ​'application/json'
  },
  body: JSON.stringify({
  query: ​'{ menuItems { name } }'
  })
  }).then(​function​(response) {
 return​ response.json();
  });
 }

If we start up our PlateSlate application (with mix phx.server) and our web application (with yarn dev), now we’ll see a much more encouraging result in our web browser at http://localhost:3000 as shown in the figure.

images/chp.frontend/menu-items.png

Congratulations, you’ve rendered a list of menu items—in a tiny web application using basic, vanilla JavaScript from another server. Sure, the list is almost unforgivably ugly—Cascading Style Sheets (CSS) being an entirely different book, after all—but you’re displaying live data, and the full capabilities of GraphQL queries are available to you, even from this toy-sized web application.

Want to show a menu item price? Displaying it—or any other data you could request via walking your data graph–is a simple matter of modifying your query and inserting the resulting data via the DOM elements of your choice. You’re not limited to GraphQL query operations either. You can use mutation operations just as easily from an HTTP POST.

To use subscription operations, however, you’re going to need to add some additional dependencies.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.217.5