GraphQL(Sitecore JSS) + Sitecore MVC = What a lovely couple

Oye Beltalowdas!
I hope you guys had a lovely Christmas, I spent my time watching The Expanse – season 4. What a show, best ever sci-fi show! And what an ending! Klaes Ashford, my favourite character. Can’t wait for season 5 πŸ™‚

Today’s topic will be about GraphQL and using it in a classic Sitecore MVC web app. For playground/sandbox, we will use the best ever project – Sitecore Helix Examples.

But first a shout out to Pieter Brinkman and his great presentation(from Sitecore Symposium 2019) – Whats New in Sitecore land
It’s all about Sitecore 9.3

Anyway, let’s proceed. We all have heard about Sitecore JSS, if not you should read all about it here – https://jss.sitecore.com/
Sitecore JSS is all about headless CMS:

Build Headless JavaScript applications with the power of Sitecore

So why bother with Sitecore JSS in a “classic”(or as it is called in Sitecore 9.3 – Sitecore Custom) Sitecore MVC web app. Well, there is this wonderful gem, GraphQL.

The Sitecore GraphQL API is an implementation of a GraphQL server on top of Sitecore. It is designed to be a generic GraphQL service platform – meaning it’s designed to host your API and present it via GraphQL queries. The API also supports real-time data using GraphQL subscriptions.

Will it be a replacement for Sitecore ItemService? I guess only time will tell… πŸ™‚

My mission for today will be to use GraphQL(client-side) to get me some data and present it in a view.

Let’s enter the Sol gate πŸ™‚

First up is to prepare your Sitecore instance for Sitecore JSS. Just install the package:
Sitecore JavaScript Services Server for Sitecore 9.3 XP 13.0.0 rev. 190924.zip (If you are on Sitecore 9.3)

Next is to set up an endpoint for GraphQL. We will do this by adding a config patch. I’ve added a Foundation Project – BasicCompany.Foundation.GraphQL. It will contain all the stuff regarding GraphQL. Here is the config file – Foundation.GraphQL.config:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
  <sitecore>
    <api>
      <GraphQL>
        <defaults>
          <security>
            <systemService type="Sitecore.Services.GraphQL.Hosting.Security.GraphQLSecurity, Sitecore.Services.GraphQL">
              <requireAuthentication>false</requireAuthentication>
            </systemService>
          </security>
        </defaults>
        <endpoints>
          <basiccompany url="/api/basiccompany" type="Sitecore.Services.GraphQL.Hosting.GraphQLEndpoint, Sitecore.Services.GraphQL.NetFxHost" resolve="true">
            <url>$(url)</url>

            <enabled>true</enabled>

            <enableSubscriptions>true</enableSubscriptions>

            <graphiql role:require="ContentDelivery">false</graphiql>
            <enableSchemaExport role:require="ContentDelivery">false</enableSchemaExport>
            <enableStats role:require="ContentDelivery">false</enableStats>
            <enableCacheStats role:require="ContentDelivery">false</enableCacheStats>
            <disableIntrospection role:require="ContentDelivery">true</disableIntrospection>

            <schema hint="list:AddSchemaProvider">
              <!-- defaults are defined in Sitecore.Services.GraphQL.Content.config -->
              <content ref="/sitecore/api/GraphQL/defaults/content/schemaProviders/systemContent" param1="web" />
            </schema>

            <!-- Determines the security of the service. Defaults are defined in Sitecore.Services.GraphQL.config -->
            <security ref="/sitecore/api/GraphQL/defaults/security/publicService" />

            <!-- Determines how performance is logged for the service. Defaults are defined in Sitecore.Services.GraphQL.config -->
            <performance ref="/sitecore/api/GraphQL/defaults/performance/standard" />

            <!--
                            Cache improves the query performance by caching parsed queries.
                            It is also possible to implement query whitelisting by implementing an authoritative query cache;
                            WhitelistingGraphQLQueryCache is an example of this, capturing queries to files in open mode and allowing only captured queries in whitelist mode.
                        -->
            <cache type="Sitecore.Services.GraphQL.Hosting.QueryTransformation.Caching.GraphQLQueryCache, Sitecore.Services.GraphQL.NetFxHost">
              <param desc="name">$(url)</param>
              <param desc="maxSize">10MB</param>
            </cache>

            <!-- 
                            Extenders allow modifying schema types after they are created by a schema provider but before they are added to the final schema.
                            This is useful when you want to _extend_ a generated schema, for example to add external API
                            data onto the item API, or to add in custom internal data (e.g. custom layout data to power an app)
                            without having to directly modify a schema provider.
                        
                            Extenders must derive from SchemaExtender.
                        -->
            <extenders hint="list:AddExtender">
              <!--<example type="Name.Space.ExtenderType, Assembly" resolve="true" />-->
            </extenders>
          </basiccompany>
        </endpoints>
      </GraphQL>
    </api>
  </sitecore>
</configuration>

Notice the new endpoint, /api/basiccompany. We also want it to be open for anonymous access but require an SSC API key, we do that with this line(in the config):

<security ref="/sitecore/api/GraphQL/defaults/security/publicService" />

Next will be to set up the API key. Navigate to /sitecore/system/Settings/Services/API Keys and create the API key – BasicCompany:

Let’s test it out. We do it by using the GraphiQL GUI, which is accessed by adding /ui to the endpoint URL:

 
http://basic-company/api/basiccompany/ui?sc_apikey=FB719F13-EDF3-42B3-B250-CA0B9507AA95

It works like a charm πŸ™‚

Next will be to figure out where we want to use GrapQL. I was thinking the PromoCard’s. Instead of render the PromoCard’s server-side, we will use GraphQL and render them client-side

Cool, now we will design our GraphQL query. We need to identify what fields are used in the PromCard’s(And if any inputs are needed, when fetching the promocard data). So let’s have a look at the view rendering – PromoCard.cshtml.

@model Sitecore.Mvc.Presentation.RenderingModel
@using BasicCompany.Feature.BasicContent
@using BasicCompany.Foundation.FieldRendering
@{ 
	var promoImageOptions = new Sitecore.Resources.Media.MediaUrlOptions
	{
		MaxWidth = 960,
		AllowStretch = false,
	};
}
@* Disable editing to improve component selection in editor. Can be edited via field editor button. *@
@Html.Sitecore().BeginField(Templates.PromoCard.Fields.Link.ToString(), new { @class = "column promo-column", DisableWebEdit = true })
<div class="card">
    <div class="card-image">
        <figure style="background-image: url(@Html.Sitecore().MediaUrl(Templates.PromoCard.Fields.Image, promoImageOptions))"></figure>
    </div>
    <div class="card-content">
        <div class="content">
            <h4>@Html.Sitecore().Field(Templates.PromoCard.Fields.Headline)</h4>
            @Html.Sitecore().Field(Templates.PromoCard.Fields.Text)
        </div>
    </div>
</div>
@Html.Sitecore().EndField()

Ok, there are four fields:

  • Image
  • Headline, plain text
  • Text, plain text
  • Link
  • In order to get the fields/data, we need to figure out the inputs:

  • datasource
  • language (current context language)
  • image options
  • Ok, we have the fields and the inputs. Let’s design our GraphQL query! To query the PromoCard we will use the Item query:

    item(
     path: String = null
     language: String = null
     version: Int = null
    )
    

    * Path will be the datasource id

    Here is our designed query:

    query 
    {
      datasource: item(path: "{E74405CA-A09B-468C-AF74-5FFBD6CDBAED}", language: "en") {
        Link: field(name:"Link"){
      	 	rendered
        }
        Image: field(name:"Image"){
          rendered(fieldRendererParameters: "$MaxWidth=960&AllowStretch=true")
        }
        Headline:field(name:"Headline"){
          rendered
        }
        Text: field(name:"Text"){
          rendered
        }
        
      }
    }
    

    Let’s test it out in the GraphiQL GUI tool:

    Notice that we are using rendered. Also notice how we use the fieldRendererParameters to set the image options:

    rendered(fieldRendererParameters: "$MaxWidth=960&AllowStretch=true")
    

    We have a working GraphQL query πŸ™‚

    Next will be to add it to our Helix solution. But before that, we need to decide what library to use for handling the GraphQL stuff.
    Apollo seems to be the most popular library when working with GraphQL. But to be honest, it’s a bit too much. I want something clean and small and I found a very nice little library that is perfect for us – graphql.js: lightweight graphql client. That is the one we are going to use πŸ™‚

    We will download the graphQL.js file and put it in our new BasicCompany.Foundation.GraphQL project:

    We also need to set up the connection to our GrapQL endpoint. We will do this in a separate js file – graphqlSetup.js:

    const graph = graphql("/api/basiccompany?sc_apikey={FB719F13-EDF3-42B3-B250-CA0B9507AA95}",
      {
        alwaysAutodeclare: true,
        asJSON: true,
        debug: true
      });
    

    Time to do some PromoCard stuff. First, we will have to do some changes in the view rendering – PromoCard.cshtml.
    We only want to use the GraphQL stuff when the page is in “Normal” mode(not using the Experience Editor). We also need to set data attributes for the datasource, language and the image options.

    @model Sitecore.Mvc.Presentation.RenderingModel
    @using BasicCompany.Feature.BasicContent
    @using BasicCompany.Foundation.FieldRendering
    @{
      var promoImageOptions = new Sitecore.Links.UrlBuilders.MediaUrlBuilderOptions()
      {
        MaxWidth = 960,
        AllowStretch = false,
      };
    }
    
    @if (!Sitecore.Context.PageMode.IsExperienceEditor)
    {
      <div class="column promo-column graphQlPromoCard" data-id="@Model.Item.ID" data-mediaUrlOptions="@promoImageOptions.ToString()" data-lang="@Sitecore.Context.Language.ToString()"></div>
    }
    else
    {
    
    <div class="column promo-column">
      <div class="card">
        <div class="card-image">
          <figure style="background-image: url(@Html.Sitecore().MediaUrl(Templates.PromoCard.Fields.Image, promoImageOptions))"></figure>
        </div>
        <div class="card-content">
          <div class="content">
            <h4>@Html.Sitecore().Field(Templates.PromoCard.Fields.Headline)</h4>
            @Html.Sitecore().Field(Templates.PromoCard.Fields.Text)
          </div>
          @Html.Sitecore().Field(Templates.PromoCard.Fields.Link)
        </div>
      </div>
    </div>
     
    }
    

    Now for the js file – promocard.js. It will have the GraphQL query, make the call and render the response.

    const getPromoCardGraphQL = graph.query(`datasource: item(path: $source, language: $language) {
        Link: field(name:"Link"){
      	 	rendered
        }
        Image: field(name:"Image"){
          rendered(fieldRendererParameters: $mediaUrlOptions)
        }
        Headline:field(name:"Headline"){
          rendered
        }
        Text: field(name:"Text"){
          rendered
        }
        
      }`);
    
    
    const loadPromoCard = function (dataSourceId, lang, urlOptions, sourceElement) {
    
      getPromoCardGraphQL({ source: dataSourceId, language: lang, mediaUrlOptions: urlOptions }).then(function (response) {
    
        const promoCardJson = response.datasource;
    
        const parser = new DOMParser();
        const el = parser.parseFromString(promoCardJson.Image.rendered, "text/xml");
        let imageSrc = el.getElementsByTagName("img")[0].getAttribute("src");
        imageSrc = parser.parseFromString(imageSrc, "text/html");
    
    
        const hrefElement = parser.parseFromString(promoCardJson.Link.rendered, "text/html");
        const hrefValue = hrefElement.getElementsByTagName("a")[0].getAttribute("href");
    
        const markup = `
          <div class="card">
            <div class="card-image">
                <figure style="background-image: url(${imageSrc.documentElement.textContent})"></figure>
            </div>
            <div class="card-content">
              <div class="content">
                <h4>${promoCardJson.Headline.rendered}</h4>
                ${promoCardJson.Text.rendered}
              </div>
              <a href="${hrefValue}" class="stretched-link"></a>
            </div>
          </div>`;
    
    
        sourceElement.innerHTML = markup;
    
        console.log(promoCardJson);
    
      }).catch(function (error) {
        console.log(error);
      });
    
    };
    
    document.addEventListener('DOMContentLoaded', function () {
    
      const promoCardElements = document.querySelectorAll(".graphQlPromoCard");
      for (let i = 0, len = promoCardElements.length; i < len; i++) {
    
        if (promoCardElements[i]) {
          const id = promoCardElements[i].getAttribute("data-id");
          const lang = promoCardElements[i].getAttribute("data-lang");
          const mediaUrlOptions = promoCardElements[i].getAttribute("data-mediaUrlOptions");
          loadPromoCard(id, lang, mediaUrlOptions, promoCardElements[i]);
        }
    
      }
    
    });
    
    

    *No jQuery here, only vanilla javascript πŸ™‚

    Just before the page is loaded, we will look for promo cards with the class name, graphQlPromoCard.
    Iterate through them all, grab the data attributes, make GraphQL call and render the result.

    Let’s test it out on the Home page. It has 5 promo cards, which means 5 requests…

    Since we have debug enabled(we did this when we set up the GraphQL connection), it will give us detailed info on the GraphQL queries:

    Now, would it not be better if we could put it all into one request, instead of 5 requests. Well, guess what? It’s possible πŸ™‚

    graphql.js supports query merging which allows you to collect all the requests into one request

    All we have to do is to use the .merge(mergeName, variables) command to put them into a merge buffer.

    getPromoCardGraphQL.merge('fetchGraphQl', { source: ...
    

    It will create a buffer with the name fetchGraphQl and append the queries to the buffer.

    In order to send it all, we will use commit(mergeName). Like this:

    graph.commit('fetchGraphQl').then(function(response) {
        console.log(response);
    });
    

    Let’s proceed and update the promocard.js file.

    
    const getPromoCardGraphQL = graph.query(`datasource: item(path: $source, language: $language) {
        Link: field(name:"Link"){
      	 	rendered
        }
        Image: field(name:"Image"){
          rendered(fieldRendererParameters: $mediaUrlOptions)
        }
        Headline:field(name:"Headline"){
          rendered
        }
        Text: field(name:"Text"){
          rendered
        }
        
      }`);
    
    
    const loadPromoCard = function (dataSourceId, lang, urlOptions, sourceElement) {
    
      getPromoCardGraphQL.merge('fetchGraphQl', { source: dataSourceId, language: lang, mediaUrlOptions: urlOptions }).then(function (response) {
    
        const promoCardJson = response.datasource;
    
        const parser = new DOMParser();
        const el = parser.parseFromString(promoCardJson.Image.rendered, "text/xml");
        let imageSrc = el.getElementsByTagName("img")[0].getAttribute("src");
        imageSrc = parser.parseFromString(imageSrc, "text/html");
    
    
        const hrefElement = parser.parseFromString(promoCardJson.Link.rendered, "text/html");
        const hrefValue = hrefElement.getElementsByTagName("a")[0].getAttribute("href");
    
        const markup = `
          <div class="card">
            <div class="card-image">
                <figure style="background-image: url(${imageSrc.documentElement.textContent})"></figure>
            </div>
            <div class="card-content">
              <div class="content">
                <h4>${promoCardJson.Headline.rendered}</h4>
                ${promoCardJson.Text.rendered}
              </div>
              <a href="${hrefValue}" class="stretched-link"></a>
            </div>
          </div>`;
    
    
        sourceElement.innerHTML = markup;
    
        console.log(promoCardJson);
    
      }).catch(function (error) {
        console.log(error);
      });
    
    };
    
    document.addEventListener('DOMContentLoaded', function () {
    
      const promoCardElements = document.querySelectorAll(".graphQlPromoCard");
      let hasPromoCards = false;
      for (let i = 0, len = promoCardElements.length; i < len; i++) {
    
        if (promoCardElements[i]) {
          const id = promoCardElements[i].getAttribute("data-id");
          const lang = promoCardElements[i].getAttribute("data-lang");
          const mediaUrlOptions = promoCardElements[i].getAttribute("data-mediaUrlOptions");
          loadPromoCard(id, lang, mediaUrlOptions, promoCardElements[i]);
          hasPromoCards = true;
        }
    
      }
    
      if (hasPromoCards) {
        reactor.dispatchEvent('fetchGraphQlEvent');
      }
      
    
    });
    
    

    Instead of calling the “graph.commit” here, we will raise a custom event – fetchGraphQlEvent.

    The event listener is set up in the graphqlSetup.js file. It will listen to the fetchGraphQlEvent and every time it’s raised it will make a graph.commit call.

    const graph = graphql("/api/basiccompany?sc_apikey={FB719F13-EDF3-42B3-B250-CA0B9507AA95}",
      {
        alwaysAutodeclare: true,
        asJSON: true,
        debug: true
      });
    
    const reactor = new Reactor();
    reactor.registerEvent('fetchGraphQlEvent');
    
    reactor.addEventListener('fetchGraphQlEvent', function () {
      graph.commit('fetchGraphQl').then(function(response) {
        console.log(response);
      });
    });
    

    And here is the Reactor. It will listen, register and dispatch events. Very neat πŸ™‚

    class Event {
        constructor(name) {
            this.name = name;
            this.callbacks = [];
        }
        registerCallback(callback) {
            this.callbacks.push(callback);
        }
    }
    
    class Reactor {
        constructor() {
            this.events = {};
        }
        registerEvent(eventName) {
            var event = new Event(eventName);
            this.events[eventName] = event;
        }
        dispatchEvent(eventName, eventArgs) {
            this.events[eventName].callbacks.forEach(function(callback) {
                callback(eventArgs);
            });
        }
        addEventListener(eventName, callback) {
            this.events[eventName].registerCallback(callback);
        }
    }
    

    So… Let’s have a look at the Home page again. Guess what? Only one request now πŸ™‚

    You can check it all out in my fork – https://github.com/GoranHalvarsson/Helix.Examples/tree/master/examples/helix-basic-tds-with-docker-vs2019-project-type

    That’s all for now folks πŸ™‚


    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out /  Change )

    Google photo

    You are commenting using your Google account. Log Out /  Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out /  Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out /  Change )

    Connecting to %s

    This site uses Akismet to reduce spam. Learn how your comment data is processed.