A library of customizable WordPress React-Apollo components
MIT License
WP-GraphQL Composer is a library of React-Apollo components for creating React apps served by a WordPress site.
This library was created to be an extension of the WPGraphQL plugin, and components and their respective queries won't work without a GraphQL server serving a schema identical to the one created by the plugin. I'd recommend using it because no other GraphQL server for WordPress has been developed and tested to the extent of WPGraphQL to my knowledge.
This component library is meant to serve a different purpose than most component libraries. The focus of the library is the minimize the logic workload of creating a React-Apollo app from a WPGraphQL-served endpoint. This is made possible by stitching together state/logic management components and view component using the Recompose library. Each component can be customized heavily using there .compose()
function. View the Creating New Composers for more detail. If you view any of the following examples you'll notice the components have minimal styling. The components are designed to be used as boilerplates.
Run the command npm install recompose wp-graphql-composer
in a the project working directory of the Apollo-React app.
Import HttpLink
from apollo-link-http
and WPProvider
from wp-graphql-composer
and wrap you root component in a WPProvider
component. WPProvider
is a wrapper component for ApolloProvider
. It handles authentication middleware and reads JWT user tokens from HTML5 localStorage.
import React from "react";
import ReactDOM from "react-dom";
import { HttpLink } from "apollo-link-http";
import { Menu, WPProvider } from "wp-graphql-composer";
// Create Link
const httpLink = new HttpLink({ uri: '/graphql', credentials: 'same-origin' })
// Note link prop
ReactDOM.render(
<WPProvider link={httpLink}>
<div className="theme">
<Header>
<Menu location="PRIMARY" />
<Login />
</Header>
<Main />
</div>
</WPProvider>
);
The following guide is a simple example of creating a custom Menu
component with the menu composer.
menu
, menuItem
, and subItem
view components from wp-graphql-composer
, as well as isEmpty
and map
from lodash
....
import { menu, menuItem, subItem } from 'wp-graphql-composer';
import { isEmpty, map } from 'lodash';
const subMenuView = ( { MenuItem, items } ) => (
<ul className="sub-menu">
{ _.map( items, ( { id, url, label, } ) => (
<li key={ id }>
<a href={ url }>{ label }</a>
</li>
) ) }
</ul>
);
const menuItemView = ( { url, label, items, SubMenu, } ) => (
<React.Fragment>
<a className="menu-item" href={ url }>{ label }</a>
{ !_.isEmpty( items ) && <SubMenu items={ items } /> }
</React.Fragment>
);
const customMenuView = ( { items, MenuItem, SubMenu } ) => (
<nav className="menu">
{ _.map( items, ( { id, ...r } ) => (
<div key={ id } className="menu-item">
<MenuItem { ...r } />
</div>
) ) }
</nav>
);
composer
assigned to compose
on each of the imported view components to compose a new CustomMenu
Component. const SubMenu = subMenu.compose({ view: subMenuView });
const MenuItem = menuItem.compose({ view: menuItemView });
const CustomMenu = menu.compose({
view: customMenuView,
MenuItem,
SubMenu
});
ReactDOM.render(
<WPProvider {...}>
<CustomMenu location="SOCIAL" />
</WPProvider>
);
The following view components have a composer
.
And customizing them is generally the same with a few key differences in the logic/state handling layers. Read more about the composers
below.
A Composer is component factory made up of higher-order component stitched together with compose from the Recompose library.
// attachment.jsx
...
/**
* Internal dependencies
*/
import { Error, Loading } from '../utils';
import { queryComposer } from '../composers';
import { CUSTOM_LOGO_QUERY, ATTACHMENT_QUERY } from './query';
import { customLogoMapper, attachmentMapper } from './attachment-mapper';
...
attachment.compose = queryComposer({
view: attachment,
whileLoading: { view: Loading },
forError: { view: Error, type: '404-image' },
queries: [
{
query: CUSTOM_LOGO_QUERY,
cond: ({ customLogo }) => !!customLogo,
mapper: customLogoMapper,
},
{
query: ATTACHMENT_QUERY,
cond: ({ customLogo }) => !customLogo,
mapper: attachmentMapper,
config: {
options: ({ id, mediaItemId, slug, uri }) => ({ id, mediaItemId, slug, uri }),
skip: ({ id, mediaItemId, slug, uri }) => !id && !mediaItemId && !slug && !uri
}
},
],
});
const Attachment = attachment.compose();
export { attachment, Attachment };
The above snippet is the definition for the Attachment composer.
The first thing you should notice is the queryComposer
function.
attachment.compose = queryComposer({ ... });
queryComposer
is one of three functions provided for creating composers. The others are baseComposer
and standardComposer
. standardComposer
is almost identical to queryComposer
except is doesn't have a queries
, and sharedMapper
is simply called mapper
property. The both accept an object as the parameter. The use cases are simple. Use queryComposer
if you need Apollo/GraphQL query data with reusable logic, use standardComposer
if you need a loading
and error-handling
layer, and for simply reusable logic use baseComposer
. The Error
and Loading
composers are created using baseComposer
.
The next is the first three properties of the object parameter.
view: attachment,
whileLoading: { view: Loading },
forError: { view: Error, type: '404-image' },
view
, whileLoading
, and forError
. These properties define loading, error, and view layers of the factory.
baseComposer
and second after queries
in the queryComposer
. It renders an alternative component base upon a conditional statement. Its takes an object with two properties as the parameter.
cond
returns truthy value.props
object is provided as a parameter. It defaults to props => !!props.data.loading
.whileLoading
. It handles errors thrown in the query layer HOC before its called and catches any error thrown in the layers called after it. And like whileLoading
it takes an object with properties as the parameter.
props
object is provided as a parameter. It defaults to props => (!!props[errorProp] || !!props.error)
.errorProp
in cond
. Defaults to data.error.message
.cond
returns truthy value.The last key thing to note is the queries
property.
queries: [
{
query: CUSTOM_LOGO_QUERY,
cond: ({ customLogo }) => !!customLogo,
mapper: customLogoMapper,
},
{
query: ATTACHMENT_QUERY,
cond: ({ customLogo }) => !customLogo,
mapper: attachmentMapper,
config: {
options: ({ id, mediaItemId, slug, uri }) => ({ id, mediaItemId, slug, uri }),
skip: ({ id, mediaItemId, slug, uri }) => !id && !mediaItemId && !slug && !uri
}
},
],
This is one of two properties unique to the queryComposer
and it's the most complex.
query
should be used based upon prop provided. Ex. props => !!props.id
.Also, take in account that the first configuration with a cond
that returns true is the configurations used.
There are a few more properties, you can find out more about in the next section. Try a remember the composers layer hierachy shown below and everything should work out.
// QueryComposer
...queries(loading(error(queryMapper(...defaultExtraHocs(...extraHocs(sharedMapper(view)))))))
// BaseComposer
loading(error(...defaultExtraHocs(...extraHocs(mapper(view)))))
baseComposer
-
const composer = baseComposer({
// default view layer component
view: viewComponent,
// default mapper function
mapper: propsMapper
});
// all default values can be overwritten in composed instances
const ComposedComponent = composer({ newView, newMapper });
standardComposer
- composers/factories created from this function are for creating components that require reusable logic wrapped in an loading state higher-order-component, error handling higher-order-component, and a props mapper.
const composer = standardComposer({
// default view layer component
view: ViewComponent,
// default properties passed to loading state handler
loading: { view: LoadingViewComponent, cond: props => !!props.loading },
// default properties passed to error state handler
error: { view: ErrorViewComponent, errorType: 'error', errorProp: 'error' },
// default HOCs wrapped around the mapper and view layer component
extraHocs: [],
// default mapper function
mapper: props => props,
// all other parameters are pass to the view component as a prop.
...extraDefaults,
});
// all default values can be overwritten in composed instances
const ComposedComponent = composer({ view, loading, error, extraHocs, mapper });
queryComposer
- similar to standardComposer
but it includes conditional GraphQL HOCs each can have a cond
function prop and mapper
.
const composer = queryComposer({
// default view layer component
view: ViewComponent,
// default query properties
queries: [{ query: GRAPHQL_QUERY, config: { options: {...}, ... }, mapper }]
// default properties passed to loading state handler
loading: { view: LoadingViewComponent, cond: props => !!props.loading },
// default properties passed to error state handler
error: { view: ErrorViewComponent, errorType: 'error', errorProp: 'error' },
// default HOCs wrapped around the mapper and view layer component
extraHocs: [],
// default mapper function shared by all queries
sharedMapper: props => props,
// all other parameters are pass to the view component as a props.
...extraDefaults,
});
// just like with baseComposer all default values can be overwritten in composed instances
const ComposedComponent = composer({ view, queries, loading, error, extraHocs, mapper });
Handles setting up an ApolloProvider
and authenication middleware for using WPGraphQL JWT Authentication
import React from react;
import ReactDOM from 'react-dom';
import { HttpLink } from 'apollo-link-http';
import { WPProvider } from 'wp-graphql-composer';
const httpLink = new HttpLink({
uri: endpoint,
credentials: 'same-origin',
});
ReactDOM.render (
<WPProvider link={ httpLink }>
{ children }
</WPProvider>
);
Handles routing by querying for WordPress reading and permalink setting and passing it on to a routing function that process the data and returns a Routing Component that is provided to the view component as prop named Routes
. The routing function can be substituted for a custom Routing setup.
The default routing function is designed to mimics WordPress' default pretty permalink and has two key requirements.
react-router-dom
package be installed and Main is wrapped in a BrowserRouter
, HashRouter
or the like.tag
and category
pathsreact-router-dom
Switch component.react-router-dom
Switch component. import React from 'react';
import { main } from 'wp-graphql-composer';
const view = ({ Archive, Page, Post, Routes, ...rest }) => {
return (
<main className="main" {...rest}>
<Routes archive={Archive} page={Page} post={Post}>
<Route exact path="/books/:id" component={Book} />
<Routes>
</main>
);
};
const Main = main.compose({ view });
Renders images stored in the WP media library
CUSTOM_LOGO_QUERY
unusable until WPGraphQL PR#571 merged*Renders component using WP Page data
Renders component using WP Post data
Queries a list of WP Posts based on props provided.
Renders Site Info(Title and Description)
Renders component using WP Menu data
Handles user login using login
mutation provided by the WPGraphQL-JWT-Authenication plugin and the authenication middleware managed by the WPProvider
component. This means that in order for this component to work the WPGraphQL-JWT-Authenication must be installed and activated on the WordPress site behind the GraphQL endpoint.
├── bin
├── dist
│ ├── index.js
│ ├── index.js.map
│ ├── index.module.js
│ └── index.module.js.map
├── src
│ ├── ... - components
│ └── index.js - library exporter
├── test
│ ├── __unit_tests__ - component tests
│ ├── __util_tests__ - util component tests
│ ├── composers.test.js - composer function and HOC tests
│ └── fragmentTypes.json - Introspection data for Apollo test utils
├── .babelrc
├── .gitignore
├── .npmignore
├── CHANGELOG
├── CODE_OF_CONDUCT.md
├── LICENSE
├── package.json
├── README.md
├── rollup.config.js
└── package.json
By default some WP settings aren't exposed by WPGraphQL. This is due to the fact that these settings aren't loaded using WordPress's Option API. While there have been talks of patching these settings in WPGraphQL nothing has been done as of yet. To get around this issue the settings can be added to the WPGraphQL schema manually. Below is an example that you can copy and patch into your theme's functions.php
or plugin's [plugin-name].php
. These are also the settings needed by a couple of the components in the library.
use GraphQLRelay\Relay;
use \WPGraphQL\Data\DataSource;
function wp_graphql_schema_patch() {
register_graphql_fields( 'Settings', [
/**
* Defines the home_url setting
*/
'homeUrl' => [
'type' => 'String',
'description' => __( 'The url to current site. Use this if site is a multisite' ),
'resolve' => function() {
return home_url();
},
],
] );
/**
* Holds the post type object permalink field
*/
$permalink = [
'type' => 'String',
'args' => [
'leavename' => [
'type' => 'Boolean',
'description' => __( 'Whether to keep post name or page name' ),
],
],
'description' => __( 'The permalink to the post object' ),
'resolve' => function( \WP_Post $post, $args ) {
if ( ! empty( $args['leavename'] ) && $args['leavename'] ) {
$leavename = true;
} else {
$leavename = false;
}
/**
* Strip site url for routing use
*/
$permalink = str_replace( home_url() . '/', '', get_permalink( $post, $leavename ) );
return ( $permalink ) ? $permalink : null;
},
];
register_graphql_field( 'post', 'permalink', $permalink );
register_graphql_field( 'page', 'permalink', $permalink );
register_graphql_field( 'attachment', 'permalink', $permalink );
$isGutenPost = [
'type' => 'Boolean',
'description' => __( 'Is post made with the Gutenberg' ),
'resolve' => function( \WP_Post $post, $args ) {
$is_guten_post = preg_match("/<!-- wp:(.*) -->/", $post->post_content ) ? true : false;
return $is_guten_post;
},
];
register_graphql_field( 'post', 'isGutenPost', $isGutenPost );
register_graphql_field( 'page', 'isGutenPost', $isGutenPost );
}
add_action( 'graphql_register_types', 'function wp_graphql_schema_patch' );
This scripts fetches schema fragment data for use with WPProvider to silent heuristic fragment
warnings.
Before using the script you have to install two dependencies. Run the following
npm install --save-dev chalk node-fetch
.
Run the script using wpg-intro <endpoint> <output>
. <endpoint>
is the WPGraphQL endpoint being used by the app and it's required. <output>
is the path the output json file should be saved to, it defaults to the project working directory root.
To use introspection data with the WPProvider
component, import introspection json file as a module and set it to WPProvider
as the fragmentData
prop.
...
import { WPProvider } from 'wp-graphql-composer';
import json from './path/to/fragment/file';
...
ReactDOM.render(
(
<WPProvider link={httpLink} fragmentData={json}>
...
</WPProvider>
),
document.getElementById('root'),
);