via GIPHY

This series has gotten a little dusty while I worked on integrating Svelte with Sitecore JSS. If you missed the announcement for that project you can read about it here. It's time to push forward with this project though and we'll be picking it back up with the article detail page implementation. I have quite a few things to show in this post.

First, here's a screenshot of what the page looks like:

Everything you see on this page is a component. By the way, if you're interested in watching the video that is embedded in the post you can check it out here.

The simplest component on this page is just a paragraph component that will allow the user to embed some rich text. This component just pulls from the content field on the data source item.

src/components/TextContent-Component/index.js

import React from 'react';
import { RichText } from '@sitecore-jss/sitecore-jss-react';

const TextContent = ({fields}) => (
  <div>
    <RichText field={fields.content} />
  </div>
);

export default TextContent;

I like the flexibility of having a generic component like this so it can be placed anywhere in the main content well and we can build out support for other types of components.

The next component in the list is a YouTube embed.

src/components/YouTubeEmbed-Component/index.js

import React from 'react';
import { getFieldValue, withSitecoreContext } from '@sitecore-jss/sitecore-jss-react';

const YouTubeEmbedComponent = ({fields, sitecoreContext}) => {

  const video_id = getFieldValue(fields, 'video_id'),
        video_height = getFieldValue(fields, 'video_height') || '315',
        video_width = getFieldValue(fields, 'video_width') || '100%',
        video_controls = getFieldValue(fields, 'video_controls') || '1',
        src = `https://www.youtube.com/embed/${video_id}?controls=${video_controls}`,
        thumbnail = `https://img.youtube.com/vi/${video_id}/0.jpg`,
        isEditing = sitecoreContext && sitecoreContext.pageEditing;

  return (
    <React.Fragment>
      {!isEditing && <iframe width={video_width} height={video_height} src={src} frameborder="0" 
      allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" 
      allowfullscreen></iframe>}
      {isEditing && <img src={thumbnail} /> }
    </React.Fragment>
  );

};

export default withSitecoreContext()(YouTubeEmbedComponent);

There are a few things I wanted to call out with this component:

  • We're using getFieldValue to retrieve the value for the properties that we're using to build out the video iframe. getFieldValue has an option for a default value, but if the field is present and just not filled out, it'll return an empty string and not the value you pass in to be returned. Because of this behavior, I'm just checking if the returned value evaluates to true and if it does not, we'll provide a default value using null coalescing.
  • sitecoreContext is being injected by withSitecoreContext and is being injected so we can check if the page is in pageEditing mode. This is needed here because the YouTube embed is an iframe and it doesn't play nicely with Experience Editor so we can provide an alternate display by just displaying the thumbnail for the video in its place.
  • We're able to display the YouTube thumbnail for the video by using the provided video id and building out the address for the thumbnail using it: https://img.youtube.com/vi/${video_id}/0.jpg

After another text component, we arrive at a block quote component, this one just contains two data source fields.

src/components/BlockQuote-Component/index.js

import React from 'react';
import { Text } from '@sitecore-jss/sitecore-jss-react';

const BlockQuoteComponent = ({ fields }) => (
  <blockquote class="blockquote text-right">
    <Text field={fields.quote} tag="p" class="mb-0" />
    <Text field={fields.citation} tag="footer" class="blockquote-footer" />
  </blockquote>
);

export default BlockQuoteComponent;

I came to really appreciate the tag prop on the JSS React components, it really produces clean components.

The other component I glossed over is at the very top of the main well and contains the meta for the article. This component doesn't contain any data source fields and just pulls from the context item.

src/components/ArticleHeader-Component/index.js

import React from 'react';
import { Text, DateField, withSitecoreContext } from '@sitecore-jss/sitecore-jss-react';

const ArticleHeaderComponent = ({ sitecoreContext }) => {

  const dateOptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };

  return (
    <React.Fragment>
      <Text field={sitecoreContext.route.fields.title} tag="h2" className="blog-post-title" />
      <p class="blog-post-meta">
        <DateField field={sitecoreContext.route.fields.date} 
          render={(date) => date.toLocaleDateString('en-US', dateOptions)} tag="strong" />
        {' by '}
        <Text field={sitecoreContext.route.fields.author.fields.first_name} tag="strong" />
        <hr />
      </p>
    </React.Fragment>
  );

};

export default withSitecoreContext()(ArticleHeaderComponent);

Some notes on this component implementation:

  • DateField is a really useful component and allows us to pass a render function as a prop. In this component, I'm displaying a formatted date with some provided options.
  • We're also injecting the Sitecore context with withSitecoreContext here so we can access the fields on the current route.
  • Originally I had this component implemented using a GraphQL query but I found this implementation to be cleaner since we already have the data we need on the sitecoreContext prop.

Now, let's move over the to right rail and go over the two components there. First up, an image promo component that allows an editor to feature an image.

src/components/ImagePromo-Component/index.js

import React from 'react';
import { Text, Image } from '@sitecore-jss/sitecore-jss-react';
import styled from 'styled-components'

const StyledLink = styled(Image)`
  width: 100%;
`;

const ImagePromoComponent = ({fields}) => (
  <div>
    <StyledLink field={fields.image} />
  </div>
);

export default ImagePromoComponent;

There's not much to this component but I wanted to make a little style adjustment and thought this was the perfect place to try out styled-components that I saw Anastasiya Flynn demo back at SUGCON EU. It was completely seamless and I will be trying to use it in other places in the future. I loved that I could pass in the JSS Image component and the generated class was populated just like you would expect.

The last component on this page is just a simple Related Articles component. There's nothing dynamic about this component right now, but it will allow the editors add articles to a tree list to feature in the sidebar.

src/components/RelatedArticles-Component/index.js

There are a few parts to this component so let's look at them individually. First up is the GraphQL query:

const ArticleQuery = gql`
query ConnectedQuery($datasource: String!) {
  data:datasource(value: $datasource) {
    id
    name
    ... on RelatedArticlesComponent {
      articles {
      	targetItems {
          ... on ArticleRoute {
            title {
              jss
            }
            url
          }
        }
      }
    }
  }
}
`;

In this query, we're just getting our datasource component and fetching the articles off of it using targetItems, we're just looking for the title and url fields from these article items. We have to do this because we don't get the URL's populated on the datasource fields prop. There's some great info as to why this is here.

Our actual component implementation looks like this:

const RelatedArticlesDiv = styled.div`
  padding-top: 15px;
`;

const RelatedArticlesComponent = ({fields, articleQuery}) => {

  const queryData = articleQuery.data || {};

  const {articles} = queryData;

  return (
    <RelatedArticlesDiv>
      <Text field={fields.title} tag="h4" />
      <hr />
      <ul>
      {articles &&
          articles.targetItems.map((listItem, index) => (
            <li key={`related-article-${index}`}>
              <a href={listItem.url}>
                <Text field={listItem.title.jss} />
              </a>
            </li>
          ))}
      </ul>
    </RelatedArticlesDiv>
  )
};

export default GraphQLData(ArticleQuery, { name: 'articleQuery' })(RelatedArticlesComponent);

Some thoughts on this code:

  • Another place where I tried out styled-components, still love it. Just using it on a div here though, nothing fancy like passing in a JSS component.
  • The component will then try to pull in the data from the articleQuery prop that get's passed in to retrieve the data off of our GraphQL response. This data can and probably will be null the first time this component executes so there are checks to see if we have data available.
  • If we have data available, we'll use the articles array on the GraphQL response object to render our unordered list of article titles, linking to each item in the list.

Conclusion

There were a lot of components in this post, but I'm really happy with how the page turned out. Building these items out as components will really give authors flexibility to build out pages how they want. We're also pretty far out of being able to fully use disconnected mode at this point as well. I've been using jss start:connected for most of the development at this point, it's a little slower since you need to sync your app if you change component structure, etc but it's still an amazing development workflow.