Skip to content

A Drupal 8 ReactJS theme leveraging JSON API and Commerce Cart API

License

Notifications You must be signed in to change notification settings

mr-barbee/drupal-8-react-js-theme

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Drupal 8 ReactJS Theme

Progressive Decoupled

Progressive decoupling allows you to continue leveraging Drupal’s rendering system while simultaneously using a JavaScript framework to power client-side interactivity and potentially a richer user experience. Therefore we use Drupal to render all of the CSS and JS libraries and serve up all the content from Drupal backend. Using this approach allows content editors to still leverage Drupal’s contextualized interfaces, content workflow, site preview, and other features. Content editors are able to create pages that are not loaded with react as well, enabling pages to still be built without ReactJS.

Build Folder File Structure

.
├── ...
├── build                                  # The Root folder for the ReactJS application.
│  ├── Components                          # Site-wide Components accessible to the entire project.
|  │  ├── SiteComponent
|  |  |  ├── SubComponent                  # Each Component can have nested components that should ONLY be accessible to parent  
|  |  |  |   └── index.js                  # component and its sub components.
|  |  |  └── index.js
|  |  └── ...  
│  ├── Scenes
|  |  ├── Page                             # These are the different pages of the site.
|  |  │  └── Components                    # Each page can have its own nested component accessible to that page only.
|  |  |  |  └── PageComponent
|  |  |  |  └──└── index.js  
|  |  |  └── index.js
|  │  └── ...
│  ├── Services
|  |  └── AppService                       # A utility service helper classes for interacting with external API.
|  |  |  └── index.js
|  |  └── ...
│  ├── app.js                              # The main app file that pull in all of the pages.
│  └── index.js                            # This is the root index file that loads and renders the app.
└── ...

The purpose of this structure was to keep the files organized while keeping in mind that the site will grow and utilize more pages and sub components.

Semi-Headless drupal

Drupal is still loaded we just add a class in the body template so that it would load on all of the custom pages that I have built out. Therefore I can take advantage of the Drupal session and page variables. In the future I do plan on making this completely headless but since its a theme form it will still need Drupal to activate it.

Within a preprocess_html hook we add some variables to the Drupal settings the would pass over to the frontend to determine which pages to show the theme and some settings to determine maintenance mode.

// Add the intro video and logo add assets to JS.
$variables['#attached']['drupalSettings']['logo']['url']['src'] = theme_get_setting('logo.url');
$variables['#attached']['drupalSettings']['logo']['name']['src'] = $config->get('name');
// See if we should display the maintenance messages.
$variables['#attached']['drupalSettings']['maintenanceMode'] = $maintenance_mode;
$variables['#attached']['drupalSettings']['disableStore'] = $disable_store;

if (!empty(theme_get_setting('footer_image')[0])) {
  $footer_file = \Drupal\file\Entity\File::load(theme_get_setting('footer_image')[0]);
  if (gettype($footer_file) == 'object') {
    $variables['#attached']['drupalSettings']['footerImage']['src'] = file_create_url($footer_file->getFileUri());
  }
}
$variables['#attached']['drupalSettings']['menuItems']['src'] = $menu;

I set a page variable to determine if the app class should be applied to the page. I only apply it if the current page is listed in the menu or if the user is currently on the product or checkout pages. The grandera-application id determines if we should load the app on that page.

{% set page_container = show_application ? 'grandera-application' : 'normal-site' %}
<div id="{{ page_container }}">
{# Navbar #}
{% if page.navigation or page.navigation_collapsible %}
  {% block navbar %}

JSON API, Commerce Cart API, and custom API

In this project, I leverage the power of JSON and Commerce Cart API’s to pull my Drupal content into my react project. JSON API brings a lot of endpoints out of the box to pull and filter out content and commerce order information. And what that didn’t provide commerce cart added endpoints to retrieve and manipulate the cart. So i only had to add the endpoint to my build/services/drupalServices file to pull in the info and have my redux store save it.

const params = { operationId: orderId, checkoutStep: 'account' }
drupalServices.setOperationAndDispatch('commerceShippingProfiles', params, this.setShippingProfile)

This is were we setup the endpoint for the api call.

case 'commerceShippingProfiles':
  operation = {
    url: `${initialState.jsonApiBaseUrl}/checkout/${params.operationId}/get_shipping_profile`,
    reducer: 'callback',
    method: 'get',
    params: params.checkoutStep !== null ? {params: {'step_id': params.checkoutStep}} : {},
    type: [ 'commerceCart' ],
    callback: callback
  }
  break

Inside the actions.js file we call the endpoint using Axios package. This is different then saving to the redux store. Instead here we have a custom callback the handles the data returned.

export const customApiCallbck = (operation) => {
  return (dispatch, getState) => {
    axios[operation.method](operation.url, operation.params)
      .then(response => {
        // If the operation callback is set then we want to
        // call the fucntion instead of updating the state.
        if (typeof operation.callback === "function") {
          operation.callback({response: response.data})
        }
      })
      .catch(error => {
        // If the operation callback is set then we want to
        // call the fucntion instead of updating the state.
        if (typeof operation.callback === "function") {
          operation.callback({error: error.response})
        }
      })
  }
}

What Json and commerce cart API didn’t offer (such as retrieving site wide settings, save and update order information and state, custom DB queries and insertions) i had to build custom. With these endpoint i had to build out the requirements and access controls to ensure that the correct user only has access to that endpoint.

granderaent_core.get_shipping_profile:
  path: '/api/checkout/{commerce_order}/get_shipping_profile'
  defaults:
    _controller: 'Drupal\granderaent_core\Controller\CommerceApiController::loadCommerceAccountShippingInformation'
    _title: 'Load Shipping Profile'
  methods:  [GET]
  requirements:
    _custom_access: '\Drupal\commerce_checkout\Controller\CheckoutController::checkAccess'
    _module_dependencies: commerce_checkout
  options:
    parameters:
      commerce_order:
        type: entity:commerce_order

Once I’ve made these endpoints i made a custom commerceAPIController and CoreAPIController to handle all of the requests:

/**
 * [loadCommerceAccountShippingInformation description]
 * @param  [type] $order_id [description]
 * @return [type]           [description]
 */
public function loadCommerceAccountShippingInformation(RouteMatchInterface $route_match) {
  $status = 200;
  $response = [];

  try {
    /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
    $order = $route_match->getParameter('commerce_order');
    if (isset($_GET['step_id']) && $order->checkout_step->value !== $_GET['step_id']) {
      // Update the order state.
      $order->set('checkout_step', $_GET['step_id']);
      $order->save();
    }
    /** @var \Drupal\commerce_shipping\ShipmentStorageInterface $shipment_storage */
    $shipment_storage = $this->entityTypeManager->getStorage('commerce_shipment');
    // Get all of the shipments for the current order and
    // load the first shipment for order.
    $order_shipment = $shipment_storage->loadMultipleByOrder($order);
    $shipment = reset($order_shipment);
    // Gather the shippment profile.
    if ($shipment) {
      /** @var \Drupal\profile\Entity\ProfileInterface $shipping_profile */
      $shipping_profile = $shipment->getShippingProfile();
      if ($shipping_profile) {
        $response['shippingProfile'] = $shipping_profile->address->getValue()[0];
      }
    }
    // Add the order email to the json array.
    $response['shippingProfile']['email'] = $order->getEmail();
  }
  catch (\Exception $e) {
    \Drupal::logger('commerce_payment')->error($e->getMessage());
    $response = ['message' => 'Error loading the account Information'];
    $status = 400;
  }

  return new JsonResponse($response, $status);
}

About

A Drupal 8 ReactJS theme leveraging JSON API and Commerce Cart API

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published