Crownpeak Digital Experience Management (DXM) - React Basics

Posted by Paul Taylor on October 28, 2019 · 21 mins read

Crownpeak Digital Experience Management (DXM) - React Basics

As the user-agent gains ever more power, so the processing that traditionally relied on server-side logic now moves to the client - JavaScript frameworks are now becoming the norm when agencies and system integrators start a new project for a customer. One category of JavaScript frameworks is “Single Page Applications (SPA)”, which rely on manipulation of the document object model (DOM), using JavaScript to show data to the end-user in a dynamic fashion: They are lightweight. They are fast. They are a developer’s preference.

Many content management platforms require a fundamental shift in how they are architected to support the delivery of content into a SPA - not so, Crownpeak Digital Experience Management (DXM) - this is thanks to DXM’s Decoupled Content Deployment Architecture, which allows the delivery of any content, using any technology or framework, anywhere in the world.

In this post, I’m going to show you how to create a simple React application that leverages Bootstrap. Then, I’m going to show you how to import it into Crownpeak DXM to allow non-technical practitioners to manage the content within the CMS, which will deploy as JSON into the same environment as the React App, for consumption at runtime.

React Basics Logical Architecture

Creating a React SPA

Follow the guide at https://github.com/facebook/create-react-app to create a React App on your local development environment.

$ npx create-react-app my-app
npx: installed 91 in 7.797s

Creating a new React app in /Users/paul.taylor/Documents/Downloads/Post/my-app.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts...


> fsevents@1.2.9 install /Users/paul.taylor/Documents/Downloads/Post/my-app/node_modules/chokidar/node_modules/fsevents
> node install

[fsevents] Success: "/Users/paul.taylor/Documents/Downloads/Post/my-app/node_modules/chokidar/node_modules/fsevents/lib/binding/Release/node-v64-darwin-x64/fse.node" is installed via remote

> fsevents@1.2.9 install /Users/paul.taylor/Documents/Downloads/Post/my-app/node_modules/jest-haste-map/node_modules/fsevents
> node install

[fsevents] Success: "/Users/paul.taylor/Documents/Downloads/Post/my-app/node_modules/jest-haste-map/node_modules/fsevents/lib/binding/Release/node-v64-darwin-x64/fse.node" is installed via remote

> core-js@2.6.10 postinstall /Users/paul.taylor/Documents/Downloads/Post/my-app/node_modules/babel-runtime/node_modules/core-js
> node postinstall || echo "ignore"


> core-js@3.2.1 postinstall /Users/paul.taylor/Documents/Downloads/Post/my-app/node_modules/core-js
> node scripts/postinstall || echo "ignore"

+ react-dom@16.11.0
+ react@16.11.0
+ react-scripts@3.2.0
added 1603 packages from 705 contributors and audited 904909 packages in 76.333s
found 0 vulnerabilities


Initialized a git repository.

Success! Created my-app at /Users/paul.taylor/Documents/Downloads/Post/my-app
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd my-app
  npm start

Happy hacking!

The installer will deploy a series of files into the directory, most of which we’ll delete - however, create the following files:

/public/index.html - A simple Bootstrap HTML page, with placeholders for React Components.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="Serverless Deployment POC">
        <meta name="author" content="Paul Taylor (me@ptylr.com)">
        <link href="../favicon.ico" rel="icon" type="image/x-icon">

        <title>Jumbotron Template for Bootstrap</title>
        <link rel="canonical" href="//serverless.skunkworks.ptylr.com/bootstrap/">

        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="//stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

        <style>
            .bd-placeholder-img {
                font-size: 1.125rem;
                text-anchor: middle;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
            }

            @media (min-width: 768px) {
                .bd-placeholder-img-lg {
                    font-size: 3.5rem;
                }
            }
        </style>

        <!-- Custom styles for this template -->
        <link href="../css/jumbotron.min.css" rel="stylesheet">
    </head>

    <body>
        <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
            <a class="navbar-brand" href="#">Navbar</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="navbarsExampleDefault">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">Link</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link disabled" href="#">Disabled</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
                        <div class="dropdown-menu" aria-labelledby="dropdown01">
                            <a class="dropdown-item" href="#">Action</a>
                            <a class="dropdown-item" href="#">Another action</a>
                            <a class="dropdown-item" href="#">Something else here</a>
                        </div>
                    </li>
                </ul>
                <form class="form-inline my-2 my-lg-0">
                    <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
                </form>
            </div>
        </nav>

        <main role="main">

            <!-- Main jumbotron for a primary marketing message or call to action -->
            <div id="hero_container" class="jumbotron"></div>

            <div id="secondary_container" class="container">
                <hr>
            </div> <!-- /container -->

        </main>

        <footer class="container">
            <p>© Company 2017-2019</p>
        </footer>

        <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.bundle.min.js" integrity="sha256-fzFFyH01cBVPYzl16KT40wqjhgPtq6FFUB6ckN2+GGw=" crossorigin="anonymous"></script>

        <script src="//unpkg.com/react@16/umd/react.development.js" crossorigin></script>
        <script src="//unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

        <!-- Load our React component. -->
        <script src="index.js"></script>
    </body>
</html>

/public/content/resources.json - A simple JSON file containing three strings.

{
  "heading": "Hello, world!",
  "description": "This is a template for a simple marketing or informational website. It includes a large callout called a jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.",
  "link_text": "Learn more »"
}

/src/index.js - The React script file, which creates Hero and SecondaryContainer components and populates them based upon values fetched from the above JSON file.

import React from 'react';
import ReactDOM from 'react-dom';

class SecondaryContainerItem extends React.Component {
    render() {
        return <div className="col-md-4">
            <h2>Heading</h2>
            <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.</p>
            <p><a className="btn btn-secondary" href="#" role="button">View details »</a></p>
        </div>;
    }
}

class SecondaryContainer extends React.Component {
    render() {
        return <div className="row">
                <SecondaryContainerItem/>
                <SecondaryContainerItem/>
                <SecondaryContainerItem/>
            </div>;
    }
}

class HeroContainer extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            resources: {},
            isLoaded: false,
        }
    }

    componentDidMount() {
        fetch('./content/resources.json')
            .then(res => {return res.json();})
            .then(json => {
                this.setState({
                    isLoaded: true,
                    resources: json,    })
            });
    }

    render () {
        const { isLoaded, resources } = this.state;
        if (!isLoaded) {
            return <div className="container">
                <h1>Loading...</h1>
            </div>
        }
        else {
            return <div className="container">
                <h1>{resources.heading}</h1>
                <p>{resources.description}</p>
                <p><a className="btn btn-primary btn-lg" href="#" role="button">{resources.link_text}</a></p>
            </div>
        }
    }
}

ReactDOM.render(<HeroContainer />, document.getElementById('hero_container'));
ReactDOM.render(<SecondaryContainer />, document.getElementById('secondary_container'));

Everything else within the /public/ and /src/ folders can be safely deleted from the React App.

Start the React App to show the Bootstrap page in a browser.

$ npm start
Compiled with warnings.

./src/index.js
  Line 9:16:   The href attribute requires a valid value to be accessible. Provide a valid, navigable address as the href value. If you cannot provide a valid href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md  jsx-a11y/anchor-is-valid
  Line 54:20:  The href attribute requires a valid value to be accessible. Provide a valid, navigable address as the href value. If you cannot provide a valid href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md  jsx-a11y/anchor-is-valid

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.

React App running.

Create a production build of the React App.

$ npm run build

> bootstrap-react@0.1.0 build /Users/paul.taylor/Documents/Repos/Personal/ptylr.com/skunkworks/serverless/bootstrap-react
> react-scripts build

Creating an optimized production build...
Compiled with warnings.

./src/index.js
  Line 9:16:   The href attribute requires a valid value to be accessible. Provide a valid, navigable address as the href value. If you cannot provide a valid href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md  jsx-a11y/anchor-is-valid
  Line 54:20:  The href attribute requires a valid value to be accessible. Provide a valid, navigable address as the href value. If you cannot provide a valid href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md  jsx-a11y/anchor-is-valid

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.

File sizes after gzip:

  40.23 KB      build/static/js/2.b2e85505.chunk.js
  856 B (+7 B)  build/static/js/main.906cdcf7.chunk.js
  778 B         build/static/js/runtime-main.8169166c.js

The project was built assuming it is hosted at the server root.
You can control this with the homepage field in your package.json.
For example, add this to build it for GitHub Pages:

  "homepage" : "http://myname.github.io/myapp",

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  https://bit.ly/CRA-deploy

Deploy the React App to your chosen delivery tier.

Create DXM Configuration

Over to DXM to create Templates to handle both the production of the JSON file and support high-fidelity preview. Assuming that an existing DXM SiteRoot and Project exist, create a new Template for Input, Output and Preview.

Input - Three fields to manage the heading, description and link text in the Hero.

<%@ Page Language="C#" Inherits="CrownPeak.Internal.Debug.InputInit" %>
<%@ Import Namespace="CrownPeak.CMSAPI" %>
<%@ Import Namespace="CrownPeak.CMSAPI.Services" %>
<%
	Input.StartTabbedPanel(new string[] {"Content"});
		Input.ShowTextBox("Heading", "heading");
		Input.ShowTextBox("Description", "description");
		Input.ShowTextBox("Link Text", "link_text");
	Input.EndTabbedPanel();
%>

Output - Configuration to produce a JSON file upon publish.

<%@ Page Language="C#" Inherits="CrownPeak.Internal.Debug.OutputInit" %>
<%@ Import Namespace="CrownPeak.CMSAPI" %>
<%@ Import Namespace="CrownPeak.CMSAPI.Services" %>
<%
Out.Write(Util.SerializeDataContractJson(new Home(asset)));
%>

<script runat="server" data-cpcode="true">
  [DataContract]
  public class Home
  {
    [DataMember(Name = "heading", Order = 0)]
    public string Heading { get; set; }
    [DataMember(Name = "description", Order = 1)]
    public string Description { get; set; }
    [DataMember(Name = "link_text", Order = 2)]
    public string LinkText { get; set; }
    
    public Home(Asset asset)
    {
      Heading = asset["heading"];
      Description = asset["description"];
      LinkText = asset["link_text"];
    }
  }
</script>

Preview - A static-HTML render of the React App’s Homepage, with field-level decoration for heading, description and link text.

<%@ Page Language="C#" Inherits="CrownPeak.Internal.Debug.PreviewInit" %>
<%@ Import Namespace="CrownPeak.CMSAPI" %>
<%@ Import Namespace="CrownPeak.CMSAPI.Services" %>

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="Serverless Deployment POC">
        <meta name="author" content="Paul Taylor (me@ptylr.com)">
        <link href="//serverless.skunkworks.ptylr.com/favicon.ico" rel="icon" type="image/x-icon">

        <title>Jumbotron Template for Bootstrap</title>
        <link rel="canonical" href="//serverless.skunkworks.ptylr.com/bootstrap/">

        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="//stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

        <style>
            .bd-placeholder-img {
                font-size: 1.125rem;
                text-anchor: middle;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
            }

            @media (min-width: 768px) {
                .bd-placeholder-img-lg {
                    font-size: 3.5rem;
                }
            }
        </style>

        <!-- Custom styles for this template -->
        <link href="//serverless.skunkworks.ptylr.com/css/jumbotron.min.css" rel="stylesheet">
    </head>

    <body>
        <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
            <a class="navbar-brand" href="#">Navbar</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="navbarsExampleDefault">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">Link</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link disabled" href="#">Disabled</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
                        <div class="dropdown-menu" aria-labelledby="dropdown01">
                            <a class="dropdown-item" href="#">Action</a>
                            <a class="dropdown-item" href="#">Another action</a>
                            <a class="dropdown-item" href="#">Something else here</a>
                        </div>
                    </li>
                </ul>
                <form class="form-inline my-2 my-lg-0">
                    <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
                </form>
            </div>
        </nav>

        <main role="main">

            <!-- Main jumbotron for a primary marketing message or call to action -->
            <div class="jumbotron">
                <div class="container">
                    <h1 class="display-3"><%= asset["heading"] %></h1>
                    <p><%= asset["description"] %></p>
                    <p><a class="btn btn-primary btn-lg" href="#" role="button"><%= asset["link_text"] %></a></p>
                </div>
            </div>

            <div class="container">
                <!-- Example row of columns -->
                <div class="row">
                    <div class="col-md-4">
                        <h2>Heading</h2>
                        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
                        <p><a class="btn btn-secondary" href="#" role="button">View details »</a></p>
                    </div>
                    <div class="col-md-4">
                        <h2>Heading</h2>
                        <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p>
                        <p><a class="btn btn-secondary" href="#" role="button">View details »</a></p>
                    </div>
                    <div class="col-md-4">
                        <h2>Heading</h2>
                        <p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
                        <p><a class="btn btn-secondary" href="#" role="button">View details »</a></p>
                    </div>
                </div>

                <hr>

            </div> <!-- /container -->

        </main>

        <footer class="container">
            <p>© Company 2017-2019</p>
        </footer>

        <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.bundle.min.js" integrity="sha256-fzFFyH01cBVPYzl16KT40wqjhgPtq6FFUB6ckN2+GGw=" crossorigin="anonymous"></script>
    </body>
</html>

Create an Asset in DXM using the Template that we’ve configured, and an appropriate Workflow. Ensure that the workflow deploys Assets to the same target delivery location that you deployed your production React App. DXM Asset Creation

Preview and Inline Edit your Homepage in DXM. DXM Asset Inline Editing

Whilst a high-fidelity editing experience is provided, when using View Output on the Homepage Asset, JSON will be distributed to the filesystem. DXM Asset JSON Output

The end result is a React App that consumes its dynamic content from a remote JSON file, which is curated by Crownpeak DXM, via a high-fidelity preview experience for the content practitioner. React Basics Logical Architecture