maanantai 11. huhtikuuta 2016

Using ReactJS and external rest-API in Sitecore Experience Editor

In this post I will talk you through a simple ReactJS-application that retrieves content from an external REST api, mixed with fields that can be edited in Sitecore Experience Editor. For this example, I mocked a REST-api to mockable.io, where you can define your json quite freely. The objective of the example is to use a field in Sitecore to pass an index of an apartment in a JSON to a controller, and then render the returned apartment to the page. All configured in Experience Editor.

But, first things first:
Create a new template in Sitecore 
The very first thing to do is to add a new data template to Sitecore. We'll use this template to save apartment index to, so we'll call this template "Apartment" and give it just one field called "Apartment index", with type "Number".

Create a new Controller rendering in Sitecore
Go to /sitecore/layout/Renderings/Views/[folder_path_to_your_liking] and create a new Controller rendering, called for example "Apartment". Or something. You'll need to select the Datasource Location and Datasource Template-fields, on location-field select just some folder you'll want to save items to, and to template you'd need to select the template just created above.

Install ReactJS.net through NuGet
I will not go into details with this, but you should install ReactJS.NET to your Visual Studio project through NuGet. Installation information here: http://reactjs.net/getting-started/download.html


Create a controller (.cs)
Back in Visual Studio, create a new controller. This controller will contact the external REST-api and populate the model and pass it to the view. My controller is called ApartmentController.cs, but yours can be something else. Following the MVC controller naming convention though, it needs to end to the word Controller.

Here we have an action in the controller that returns a JsonResult -> when called from the view (or in this case, the reactJS-app), this will return plain json back to the view.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public JsonResult json(int homeIndex)
{
    var model = new Models.Apartment();
    var webClient = new System.Net.WebClient();
    var apartments = webClient.DownloadString("http://[subdomain].mockable.io/[servicepath]");

    List<Models.Apartment> mylist = JsonConvert.DeserializeObject<List<Models.Apartment>>(apartments);
    var apartment = mylist[homeIndex];

    // before returning the data retrieved from REST, add the Sitecore translations for the view
    apartment.detailsText = ScLanguageProvider.GetResourceText("details");
    apartment.addressText = ScLanguageProvider.GetResourceText("address");
    apartment.descriptionText = ScLanguageProvider.GetResourceText("description");
    apartment.relatedText = ScLanguageProvider.GetResourceText("related apartments");

    return Json(apartment, JsonRequestBehavior.AllowGet);
}


Create a model (.cs)
A model is needed to hold some variables that are populated in the controller and rendered in the view / react app. My model is called Apartment (could be anything), here's a snippet of it (really just a POCO):


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 public class Related
 {
    public int id { get; set; }
    public string name { get; set; }
 }

 public class Apartment
 {
    public int index { get; set; }
    public string guid { get; set; }
    public string mainImage { get; set; }
    public string apartmentName { get; set; }
    public string apartmentMainImage { get; set; }
    public string apartmentDescription { get; set; }
    public string address { get; set; }
    public List<Related> related { get; set; }
    public string details { get; set; }
    public string detailsText { get; set; }
    public string addressText { get; set; }
    public string descriptionText { get; set; }
    public string relatedText { get; set; }
 }


Create a view (.cshtml)
Create a new view, give it some descriptive name. I gave mine a name of ReactApp.cshtml :) 
In the view, we'll have a script reference to our own reactJS-file (.jsx), and references to the react "core" and API at fb.me. Also we'll have a javascript variable "reactApartmentIndex" populated from a Sitecore field to pass as the index to the ReactJs-app that should be used to retrieve correct apartment from the external json.


Choosing an apartment by giving the index of it in a json is not probably the most intuitive way to go, but it's here only for demonstration purposes. A much better alternative would be for example to use another REST-api to populate a dropdown on the page, so that the content editor could only use an apartment that is really existing.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@{
    var isPageEditor = Sitecore.Context.PageMode.IsExperienceEditor;
    //var isPreview = Sitecore.Context.PageMode.IsPreview; // there could be different stuff for preview, but not used here
}

@if (isPageEditor)// show editable field with description on edit mode
{
    <div>Choose index for apartment</div>
    <div class="form-control">@Html.Sitecore().Field("Apartment index")</div>
}
<div id="content"><img src="/sandbox/images/ajax-loader.gif" alt="" /></div>
<script src="https://fb.me/react-0.14.0.js"></script>
<script src="https://fb.me/react-dom-0.14.0.js"></script>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.1.js"> </script>
<script type="text/javascript">
    @* this var for reactjs-application usage -> passes the given index to an ajax call *@
    var reactApartmentIndex = "@Html.Sitecore().Field("Apartment index", new { DisableWebEdit = true })";
</script>
<script src="@Url.Content("~/sandbox/js/reactapp.jsx")"></script> // the react app reference


Create a React-file (.jsx)
Last, but not least, we'll create the ReactJS-file which is the actual application, all others are basically plumbing for this. Or not, since our ReactJS wouldn't do much without them. Oh well... :). My ReactJS-app is called... ta-daa: ReactApp.jsx. Maybe a more descriptive name could be in order.

First, lets take a look to the "parent" react-class that will call the other methods (functions?) in the React app:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var ApartmentApp = React.createClass({
    displayName: 'ApartmentApp',
    loadApartmentFromServer: function () {
        $.ajax({
            url: this.props.url,
            type: "POST",
            dataType: 'json',
            contentType: "application/json; charset=utf-8",
            data: JSON.stringify({ homeIndex: reactApartmentIndex }),
            success: function (msg) {
                this.setState({ data: msg });
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
    getInitialState: function () {
        return { data: [] };
    },
    componentDidMount: function () {
        this.loadApartmentFromServer();
    },
    render: function() {
        return (
            <div>
                <ApartmentContent data={this.state.data } />
                <ApartmentBlocks data={this.state.data } />
            </div>
      );
    }

});

Above we'll set the initial state of the component (getInitialState), and use the componentDidMount-method to call the ajax-method with the passed in index. Also the url of the ajax method to call is passed in from the ReactDOM.render-function that can be found further down. Also we'll call the render-function to render the outcome using two additional components that we'll pass the data to, ApartmentContent and ApartmentBlocks.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
var ApartmentContent = React.createClass({
    render: function () {
        var apartmentNode = this.props.data;
        return (
            <div>
                <div className="row">
                    <div className="col-md-12">
                        <h1>{apartmentNode.apartmentName}</h1>
                    </div>
                </div>
                
            </div>
     );
    }
});

var ApartmentBlocks = React.createClass({
    render: function () {
        var apartmentNode = this.props.data;
        var related = this.props.data.related != undefined ? this.props.data.related : [];
        return (
        <div className="row">
            <div className="col-md-8">
                <img className="thumbnail" src={apartmentNode.apartmentMainImage} />
            </div>
            <div className="col-md-4">
                <div className="alert alert-info">
                    <div className="container">
                        <h4 dangerouslySetInnerHTML={{__html: apartmentNode.addressText}}></h4>
                        <div>{apartmentNode.address}</div>
                    </div>
                </div>
                <div className="alert alert-info">
                    <div className="container">
                        <h4 dangerouslySetInnerHTML={{__html: apartmentNode.descriptionText }}></h4>
                        <div>{apartmentNode.apartmentDescription}</div>
                    </div>
                </div>
                <div className="alert alert-info">
                    <div className="container">
                        <h4 dangerouslySetInnerHTML={{__html: apartmentNode.detailsText}}></h4>
                        <div>{apartmentNode.details}</div>
                    </div>
                </div>
                <div className="alert alert-warning">
                    <div className="container">
                        <h4 dangerouslySetInnerHTML={{__html: apartmentNode.relatedText }}></h4>
                        <div>
                            <ol>
                                {related.map(function (rel) {
                                return (
                                    <li key={ rel.id }>{rel.name}</li>);
                                })}
                            </ol>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        );
    }
});

// ReactDOM.render has to be the last element
ReactDOM.render(
  <ApartmentApp url="/api/sitecore/Apartment/json" index="1" />,
  document.getElementById('content')
);

The ApartmentContent-component only renders the apartmentName passed in the props, while ApartmentBlocks renders the field titles that can be edited in Sitecore (for example
dangerouslySetInnerHTML={{__html: apartmentNode.relatedText }})
and of course the other content of the apartment, passed in in the props.
See also the how the related apartments are rendered from a generic list on line 50.

The last (but definitely the most important) thing is to call the render-method of the ReactDOM, which orchestrates the whole functionality of the app (on line 65 above). The first parameter of the render-method is a reference to the ApartmentApp-component with the default parameters (or props as they are called in React) url and index, and the latter part is an html-element where to put the component outcome (div-element on line 11 of the view).

TL;DR; an example how to use ReactJS in Sitecore, with Experience Editor support