react https://www.jamesdflynn.com/ en GatsbyJS + Drupal: Create Content Type Landing Pages https://www.jamesdflynn.com/development/gatsbyjs-drupal-create-content-type-landing-pages <span class="field field--name-title field--type-string field--label-hidden">GatsbyJS + Drupal: Create Content Type Landing Pages</span> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">jflynn</span></span> <span class="field field--name-created field--type-created field--label-hidden">Sun, 12/01/2019 - 16:30</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>At <a href="https://nedcamp.org/">NEDCamp</a> I had the pleasure of seeing many wonderful people and sessions, but, as you may know, my favorite track at most events is the "Hallway Track". If you're not familiar, the Hallway Track is the time in-between sessions where some real magic happens. You have a chance to talk with some of the most amazing minds in the world about topics you're passionate about. You can share knowledge, bounce ideas, have epiphanies, and realize that your current problems (code-wise) are things that other people have run into. One such conversation happened that inspired me to think outside the box to solve a problem.</p> <p>In my <a href="/development/gatsbyjs-drupal-create-custom-graphql-schema-empty-fields">last post</a> we went over how to create a related content section with references to entities that may have empty fields. Today, we're going to take that one step further and create landing pages for content types.</p> <p>In Drupal, we would ordinarily build these by attaching a view to a content type based on contextual filters or similar in order to get a collection of all content of that content type. Since we don't have Views in GatsbyJS, we need another solution. There are a couple of options out there, including the recently released <a href="https://www.drupal.org/project/jsonapi_cross_bundles">JSON:API Cross Bundles</a> module, compliments of Centarro and Matt Glaman. However, at the time of this writing there is an issue with the JSON:API Cross Bundles conflicting with <a href="https://www.drupal.org/project/jsonapi_extras">JSON:API Extras</a>. So, if you're relying on JSON:API Extras, you'll need another solution.</p> <h2>The problem:</h2> <p>Out of the box, JSON:API does not create a route to retrieve all nodes with any ease. However, there's no need to fear. This post is here!</p> <p>Now if you're not using JSON:API Extras, I strongly recommend looking into JSON:API Cross Bundles. It creates a route to all content and will simplify your life. If you are using JSON:API Extras, have I got something for you.</p> <p>Let's dive in.</p> <h2>Scenario:</h2> <p>You're building a decoupled Drupal site and you want to have a reusable template for landing pages that display all content of a content type. This is easy enough to do in Drupal using Views, but we lose Views when going decoupled so we need another way. How do we accomplish this in a decoupled application using GatsbyJS as the front-end?</p> <h2>Solution:</h2> <p>Strap in folks, this one gets a little bumpy.</p> <h3>Drupal side:</h3> <p>We need to do some work on both sides of the application for this to work. First, we will setup a content type in Drupal to use for our Content Landing Pages. This is kind of a choose your own adventure scenario, but one thing that you <strong><em>absolutely must have</em></strong> is an Entity Reference field that references the Config Entity: Content Type with a cardinality of 1. </p> <p>Select this field type:</p> <div alt="Dropdown select with Reference: Other selected" data-embed-button="media" data-entity-embed-display="media_image" data-entity-type="media" data-entity-uuid="5ae4689b-2121-4c63-82de-ea2d206fd13c" data-langcode="en" title="Select" class="embedded-entity"> <img src="/sites/default/files/Screen%20Shot%202019-12-01%20at%2011.33.23%20AM_0.png" alt="Dropdown select with Reference: Other selected" title="Select" typeof="foaf:Image" /></div> <p> </p> <p> And this is the entity type to reference:</p> <div alt="Dropdown select with Configuration: Content Type selected" data-embed-button="media" data-entity-embed-display="media_image" data-entity-type="media" data-entity-uuid="d7930f66-d6fc-4deb-bcb5-61240d79d76c" data-langcode="en" title="content type selected" class="embedded-entity"> <img src="/sites/default/files/Screen%20Shot%202019-12-01%20at%2011.36.44%20AM.png" alt="Dropdown select with Configuration: Content Type selected" title="content type selected" typeof="foaf:Image" /></div> <p> </p> <p>Now that we have our field created, select any content type that will require a content landing page as an available option.</p> <p>Whatever else you want to put on this page is up to you. Go wild. Want a hero image? Add a hero image. Want a dancing baby gif? That's your choice and I respect it. Once you've finished making the <strong>greatest landing page content type ever™ </strong>we can move on to the fun part in GatsbyJS.</p> <h3>GatsbyJS side:</h3> <p>JSON:API gives us something that can help us out a bit. It goes by the name <code>allNodeTypeNodeType</code> and it will let us workaround the lack of a base all-content route. If we explore this in GraphiQL we'll see that we can drill down a bit and get exactly what we need.</p> <pre> <code class="language-javascript">{ allNodeTypeNodeType { nodes { relationships { node__article node__landing_page node__basic_page } } } }</code></pre> <p><strong>NOTE: </strong>I didn't drill down too far, but all fields are available here.</p> <p>Let's first create our landing page template in Gatsby.</p> <p>First, let's just create a simple, empty component for our Landing Page with a basic graphql query.</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js import React from 'react' import { graphql } from 'gatsby' function LandingPage({ data }) { return ( &lt;div&gt; &lt;/div&gt; ) } export default LandingPage export const query = graphql` query($LandingPageID: String!){ nodeLandingPage(id: { eq: $LandingPageID }) { id title } } `</code></pre> <p>Nothing too fancy here, right? Just a template that we can use to build our pages out without Gatsby yelling at us on build.</p> <p>Next, we're going to add this template to <code>gatsby-node.js</code> so that we create our pages dynamically.</p> <pre> <code class="language-javascript">// gatsby-node.js exports.createPages = async ({ graphql, actions }) =&gt; { const { createPage } = actions const LandingPageTemplate = require.resolve(`./src/components/templates/LandingPage/index.js`) const LandingPage = await graphql(`{ allNodeLandingPage { nodes { id path { alias } } } } `) LandingPage.data.allNodeLandingPage.nodes.map(node =&gt; { createPage({ path: node.path.alias component: LandingPageTemplate, context: { LandingPageID: node.id, } }) }) }</code></pre> <p>This is pretty straightforward so far, right? Let's think about what we're going to need in order for this to work the way we want it to and pull all of a single content type into our landing page.</p> <p>We're going to need:</p> <ul><li>The content type we want to build a landing page for.</li> <li>A query to fetch all content a content type.</li> <li>The landing page template with logic to display the content type.</li> <li>Probably some other things, but we'll sort that out along the way. We're in this together, remember?</li> </ul><p>How are we going to get these things? Let's go down the list.</p> <p><strong>The content type we want to build a landing page from:</strong></p> <p>We have this from our Drupal side. Remember, we created the Content Type field on our Landing Page content type? This can be placed in our <code>gatsby-node.js</code> and passed to our query via the <code>context</code> option.</p> <p>Let's add it in.  First we need to update our graphql query to pull it in:</p> <pre> <code class="language-javascript">// gatsby-node.js const LandingPage = await graphql(`{ allNodeLandingPage { nodes { id relationships { // &lt;---- add from this line field_content_type { drupal_internal__type name } } // &lt;---- to this line } } } `)</code></pre> <p>What we're doing here is looking at GraphiQL and exploring our data to see what we have available. If we drill down into <code>allNodeLandingPage.nodes</code> we can see that in <code>relationships</code> we have <code>field_content_type</code> with some useful things. Specifically, our <code>drupal_internal__type</code> and <code>name</code> values. Also, notice that we removed <code>nodes.path.alias</code> from the query.</p> <p>By adding these to our query we can now pass the info through to our created pages. We're going to do a bit of data manipulation here to create our paths dynamically as well. I follow the convention that a landing page's path should reflect the content type that it's a landing page for. So, if we were making a landing page for "Articles" the path would be <code>path-to-my.site/articles</code> and articles would have that as a base path to <code>path-to-my.site/articles/my-awesome-article</code>. However, you can follow whatever convention you see fit.</p> <p>To do this, we're going to manipulate the name from the content type into a URL-friendly string by using the JavaScript <code>.replace()</code> function and then pass that to the <code>path</code> option. Since we also want to query for the content type on our landing page, we're going to pass the <code>drupal_internal__type</code> through the <code>context</code> option.</p> <p>Let's do that:</p> <pre> <code class="language-javascript">// gatsby-node.js LandingPage.data.allNodeLandingPage.nodes.map(node =&gt; { const pathName = node.relationships.field_content_type.name.toLowerCase().replace(/ /g, '-') // &lt;---- New line createPage({ path: pathName, // &lt;--- Changed line component: LandingPageTemplate, context: { LandingPageID: node.id, ContentType: node.relationships.field_content_type.drupal_internal__type, // &lt;---- New line } }) })</code></pre> <p>What does the the context option do? It passes data to our component as props. GraphQL already pulls the context data for the queries, which you can see in any query that has a variable for the filter. Usually this is the content ID so that it can build a page for a specific piece of content from Drupal, but we can leverage this to add more variables and more filtering however we see fit.</p> <p>Our next step is going to be to actually USE this additional info to do something amazing with.</p> <p><strong>A query to fetch all content a content type:</strong></p> <p>Let's look back at our <code>src/components/templates/LandingPage/index.js</code> and see what we need to query. We know we want to get all nodes of a certain content type, and we know that we want to reuse this template for any landing pages with content listing. Since we've established that allNodeTypeNodeType gives us access to all content on available to Gatsby, let's query on that.</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js export const query = graphql` query($LandingPageID: String!, $ContentType: String!){ nodeLandingPage(id: { eq: $LandingPageID }) { id title } allNodeTypeNodeType(filter: { drupal_internal__type: { eq: $ContentType }}) { // &lt;---- New section nodes { relationships { node__article { id title path { alias } } node__page { id title path { alias } } } } } } `</code></pre> <p>What we're doing here is using that variable we passed via the context option in gatsby-node.js and filtering to only return the content type we're wanting to see. One 'gotcha' here is that this query will also return the landing page that references the content type. However, if you're not creating a landing page of landing pages then you should be alright.</p> <p>Since we're only creating landing pages for two content types, this is fine, although we're not getting a lot back. Most projects that I've worked on have had some kind of "teaser" display for these kinds of pages. I'm not going to cover the specifics of creating a teaser template here, but the TL;DR is: start with your full display and take out everything but what you want on the teaser. For this post, we're going to create the list of links using the titles.</p> <p>Now, if the content types that we're creating landing pages for don't have any content, then you're going to have a bad time. In this case, go back to my previous post about empty entity reference fields and see if you can use that to create some default fields and prevent errors or just create some content of the missing type.</p> <p>Next, let's flesh out our landing page template a bit.</p> <p><strong>The landing page template with logic to display the content type:</strong></p> <p>So far, our template, minus the query, is pretty empty and not doing a lot. Let's add in the title of this landing page.</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js function LandingPage({ data }) { const landingPage = data.nodeLandingPage return ( &lt;div&gt; &lt;h1&gt;{landingPage.title}&lt;/h1&gt; &lt;div className='content-list'&gt;&lt;/div&gt; &lt;/div&gt; ) }</code></pre> <p>I like to clean up the variables a bit and rename <code>data.nodeLandingPage</code> to <code>landingPage</code>. It's a bit cleaner to me, but do what you want.</p> <p>Alright, we have the title of this content, but what about the list of content we want to show on this page? Well, we're going to need to do some logic for that. First off, we need to know which content type we're looking for. Second, we need a way find it. Third, we need to clean this data into something usable. Finally, we need to display it.</p> <p>We could just display everything returned from our <code>allNodeTypeNodeType</code> query, but there would be a lot of nulls and issues parsing the arrays. Here's an example of what that query returns before we massage the data, using the Drupal internal type <code>article</code>:</p> <pre> <code class="language-json">{ "data": { "allNodeTypeNodeType": { "nodes": [ { "drupal_internal__type": "article", "relationships": { "node__article": [ { "id": "0e68ac03-8ff2-54c1-9747-3082a565bba6", "title": "Article Template", "path": { "alias": "/article/article-template" } } ], "node__basic_page": null } } ] } } }</code></pre> <p>Now, to get the content this way we could do some complex mapping and sorting and filtering, but I tried that and it wasn't fun. Fortunately, Gatsby is here to rescue us and make life easier. Our context option gets passed into our page component as props. If you're unfamiliar with the concept of Props in React, and therefore Gatsby, props are properties that are passed into components. The line </p> <pre> <code>function LandingPage({ data }) {</code></pre> <p>could be rewritten as</p> <pre> <code>function LandingPage(props) { const data = props.data</code></pre> <p>but we're using a concept called <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring">Destructuring</a> to only pass in the prop that we need. This allows us to create variables from object keys without having to take the extra steps. Our page component props object also contains the key <a href="https://www.gatsbyjs.org/docs/actions/#createPage">pageContext</a> which is where anything in the context option gets stored to give the page template access to.</p> <p>Let's bring that in:</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js function LandingPage({ data, pageContext }) { const landingPage = data.nodeLandingPage const nodeType = data.allNodeTypeNodeType const contentType = 'node__' + pageContext.ContentType</code></pre> <p>Since we set our <code>ContentType</code> in <code>gatsby-node.js</code> we're able to use that here. Note that we're concatenating the string <code>node__</code> with our <code>pageContext.ContentType</code>. We're doing this because everything in Gatsby is a node, including content types. This allows us to do the next steps.</p> <p>Next, we want to clear out all of the non-content type data from the <code>allNodeTypeNodeType</code> query. This is what it looks like if we were to <code>console.log(nodeType.nodes)</code>:</p> <pre> <code>Array(1) 0: relationships: node__article: Array(1) 0: {id: "0e68ac03-8ff2-54c1-9747-3082a565bba6", title: "Article Template", path: {…}, …} length: 1 __proto__: Array(0) node__page: null</code></pre> <p>We only want the <code>node__article</code> array, so how do we get that? Well, we need to use <code>.map()</code> and a concept called <a href="https://www.sitepoint.com/currying-in-functional-javascript/">currying</a>. This is essentially creating a function that allows us to use a variable from outside of the <code>.map()</code> scope inside of the <code>.map()</code> callback. It allows us to break down a function into more functions so that we have more control over it, which is what we need here.</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js function LandingPage({ data, pageContext }) { const landingPage = data.nodeLandingPage const nodeType = data.allNodeTypeNodeType const contentType = 'node__' + pageContext.ContentType const getContentArray = (contentType) =&gt; { // &lt;---- Curry function, but not as delicious return (node) =&gt; (node.relationships[contentType]) } const contentArray = nodeType.nodes.map(getContentArray(contentType))</code></pre> <p>We created our curry function that takes our <code>contentType</code> as an argument. From within there, it completes the mapping and returns our desired array... almost.</p> <p>Here's what we get back if we <code>console.log(contentArray)</code>:</p> <pre> <code>[Array(1)] 0: Array(1) 0: {id: "0e68ac03-8ff2-54c1-9747-3082a565bba6", id: 1, title: "Article Template", …} length: 1 __proto__: Array(0) length: 1 __proto__: Array(0) </code></pre> <p>We're almost there, but now we have an array of our content within another array. If only there were a function to help us out here...</p> <p>Just kidding, there is! For this, we're going to use <code>.flat()</code>. The <code>.flat()</code> function flattens out a nested array into a single level. However, there's a gotcha with it, as mentioned in this <a href="https://stackoverflow.com/questions/56593033/array-prototype-flat-creates-an-error-when-building-gatsby-site">Stack/Overflow question</a>. We can get around this by using the array-flat-polyfill polyfill.</p> <p>Add gatsby-plugin-polyfill-io to your project by installing with yarn or npm</p> <pre> <code>npm install array-flat-polyfill // or yarn add array-flat-polyfill</code></pre> <p>and in your component file add the following within</p> <pre> <code>import 'array-flat-polyfill'</code></pre> <p>So, let's flatten that array!</p> <pre> <code class="language-javascript">function LandingPage({ data, pageContext }) { const landingPage = data.nodeLandingPage const nodeType = data.allNodeTypeNodeType const contentType = 'node__' + pageContext.ContentType const getContentArray = (contentType) =&gt; { return (node) =&gt; (node.relationships[contentType]) } const contentArray = nodeType.nodes.map(getContentArray(contentType)) const contentArrayFlat = contentArray.flat()</code></pre> <p>And the resulting <code>console.log(contentArrayFlat)</code>:</p> <pre> <code>0: id: "0e68ac03-8ff2-54c1-9747-3082a565bba6" path: {alias: "/article/article-template"} title: "Article Template" length: 1 __proto__: Array(0)</code></pre> <p>Now we've got exactly what we wanted! The final step is to put this to work. We'll do that by creating a list of titles that link to the content. Your finished component should look like:</p> <pre> <code class="language-javascript">// src/components/templates/LandingPage/index.js import React from 'react' import { graphql, Link } from 'gatsby' // &lt;--- added 'Link' here to use the link component import 'array-flat-polyfill' function LandingPage({ data, pageContext }) { const landingPage = data.nodeLandingPage const nodeType = data.allNodeTypeNodeType const contentType = 'node__' + pageContext.ContentType const getContentArray = (contentType) =&gt; { return (node) =&gt; (node.relationships[contentType]) } const contentArray = nodeType.nodes.map(getContentArray(contentType)) const contentArrayFlat = contentArray.flat() return ( &lt;div&gt; &lt;h1&gt;{landingPage.title}&lt;/h1&gt; &lt;div className='content-list'&gt; &lt;ul&gt; // One-liner to create the list of items. {contentArrayFlat.map((item, i) =&gt; &lt;li key={i}&gt;&lt;Link to={item.path.alias}&gt;{item.title}&lt;/Link&gt;&lt;/li&gt;)} &lt;/ul&gt; &lt;/div&gt; &lt;/div&gt; ) } export default LandingPage export const query = graphql` query($LandingPageID: String!, $ContentType: String!){ nodeLandingPage(id: { eq: $LandingPageID }) { id title } allNodeTypeNodeType(filter: { drupal_internal__type: { eq: $ContentType }}) { nodes { relationships { node__article { id title path { alias } } node__page { id title path { alias } } } } } } `</code></pre> <p>And that's all there is to it. Hopefully you find this useful and it helps speed up your development with Gatsby a little bit.  If I missed anything on here, please don't hesitate to let me know in the comments. Always feel free to reach out to me on Twitter or Slack or any way you want to.</p> <p>Credit where credit is due: Shane Thomas (AKA <a href="https://twitter.com/codekarate">@codekarate</a>) and Brian Perry (AKA <a href="https://twitter.com/bricomedy">@bricomedy</a>) helped me work through this issue at NEDCamp.</p> <h2>Patron thanks:</h2> <p>Thank you to my Patreon Supporters</p> <ul><li>David Needham</li> <li>Tara King</li> <li>Lullabot</li> </ul><p>For helping make this post. If you'd like to help support me on Patreon, check out my page <a href="https://www.patreon.com/jddoesthings">https://www.patreon.com/jddoesthings</a></p> </div> <div class="field field--name-field-category field--type-entity-reference field--label-above"> <div class="field__label">Category</div> <div class="field__item"><a href="/development" hreflang="en">Development</a></div> </div> <div class="field field--name-field-tags field--type-entity-reference field--label-above"> <div class="field__label">Tags</div> <div class="field__items"> <div class="field__item"><a href="/taxonomy/term/12" hreflang="en">Drupal Planet</a></div> <div class="field__item"><a href="/taxonomy/term/21" hreflang="en">GatsbyJS</a></div> <div class="field__item"><a href="/taxonomy/term/14" hreflang="en">react</a></div> <div class="field__item"><a href="/taxonomy/term/11" hreflang="en">Drupal</a></div> </div> </div> <div class="field field--name-field-comments field--type-disqus-comment field--label-visually_hidden"> <div class="field__label visually-hidden">Comments</div> <div class="field__item"><drupal-render-placeholder callback="Drupal\disqus\Element\Disqus::displayDisqusComments" arguments="0=GatsbyJS%20%2B%20Drupal%3A%20Create%20Content%20Type%20Landing%20Pages&amp;1=https%3A//www.jamesdflynn.com/development/gatsbyjs-drupal-create-content-type-landing-pages&amp;2=node/26" token="GaA9ILHgT3y4UhF4FRNYk6J_ecF9-IfpigWMrIa47fM"></drupal-render-placeholder></div> </div> <div class="field field--name-field-header-image-entity field--type-entity-reference field--label-hidden field__item"><article class="media media--type-image media--view-mode-embedded"> <div class="field field--name-image field--type-image field--label-hidden field__item"> <img src="/sites/default/files/negative-space-compiled-code-screen.jpg" width="1200" height="800" alt="Generic &quot;Look at my screen full of code&quot; screen" loading="lazy" typeof="foaf:Image" /> </div> </article> </div> Sun, 01 Dec 2019 22:30:00 +0000 jflynn 26 at https://www.jamesdflynn.com GatsbyJS + Drupal: Create Custom GraphQL Schema for Empty Fields https://www.jamesdflynn.com/development/gatsbyjs-drupal-create-custom-graphql-schema-empty-fields <span class="field field--name-title field--type-string field--label-hidden">GatsbyJS + Drupal: Create Custom GraphQL Schema for Empty Fields</span> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">jflynn</span></span> <span class="field field--name-created field--type-created field--label-hidden">Mon, 11/18/2019 - 14:40</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><h2>Stop me if you've heard this one...</h2> <p>A developer is trying to connect Drupal to Gatsby. Drupal says, "I've got a Related Content field that allows SEVEN different entity types to be referenced, but only THREE slots for content. What do you think?"</p> <p>Developer says, "Sure, no problem, Drupal!"</p> <p>Gatsby jumps in and says, "You better make sure that all seven of those entity types are in those three slots."</p> <p>Developer and Drupal look at each other in confusion. Developer says, "Gatsby, that's not GREAT"</p> <p><em>** Laughs go here **</em></p> <div alt="That's the joke meme" data-embed-button="media" data-entity-embed-display="media_image" data-entity-embed-display-settings="medium" data-entity-type="media" data-entity-uuid="133fbad1-73ba-4652-a245-437da93b9ba2" data-langcode="en" title="Simpsons" class="embedded-entity"> <img src="/sites/default/files/styles/medium/public/joke.jpg?itok=4QDapDCs" alt="That's the joke meme" title="Simpsons" typeof="foaf:Image" class="image-style-medium" /></div> <h2>What happened?</h2> <p>It turns out the way that Gatsby builds what it calls "nodes" is by querying the data available. This usually works perfectly, but there are occasions where all the data might not be present, so Gatsby doesn't build the nodes, or it builds the nodes, but doesn't create all of the fields.  Any optional field can fall into this if there is no content in that field on a Drupal entity, and if you're building templates for your decoupled site, then you might just want some content in that field.</p> <p><strong>NOTE: </strong>A Drupal "node" and a GraphQL "node" are not the same thing. This may take some time to sink in. It definitely did for me! For the remainder of this post I will refer to GraphQL nodes as "nodes" and Drupal nodes as "entities". Everybody got that? Good, moving on.</p> <p>Because the nodes with your hypothetical content have not yet been created, or the fields on your entity don't have anything in them, Gatsby just passes right over them and acts like they don't even exist.</p> <p>Commonly, you'll see something like this pop up during <code>gatsby build</code> or <code>gatsby develop</code>:</p> <pre> <code class="language-bash">Generating development JavaScript bundle failed /var/www/gatsby/src/components/templates/homepage/index.js 56:9 error Cannot query field "field_background_media" on type "node__homepageRelationships".</code></pre> <p>This can be frustrating. And when I say "frustrating" I mean that it's table-flipping, head slamming into desk, begging for help from strangers on the street frustrating. The quick fix for this one is simple: ADD SOMETHING TO THE FIELD. In fact, you don't even have to add it to every instance of that field. You can get away with only putting some dummy content in a single entity and you'll have access to that field in Gatsby.</p> <p>You could also make it a required field so that there MUST be something in it or else the author can't save. This is valid, but in the event that your project's powers-that-be decide that it should be optional, you may need another alternative. This is where GraphQL schema customization comes in.</p> <h2>GraphQL schema and Gatsby</h2> <p>First, a little primer on GraphQL schema. Outside of Gatsby, a system using GraphQL usually needs to define the schema for GraphQL nodes. Gatsby takes this out of the developer's hands and builds the schema dynamically based on the content available to it from its source, in this case <code>gatsby-source-drupal</code>. However, empty data in the source results in no schema in Gatsby.</p> <p>Let's look at the fix for the optional image above and why it works:</p> <p>In <code>gatsby-node.js</code> we build our schema through <code>createPages</code>, <code>createPage</code>, and GraphQL queries. This is also where we're going to place our code to help GraphQL along a little bit. This will live in a function called <code>createSchemaCustomization</code> that we can use to customize our GraphQL schema to prevent empty field errors from ruining our day.</p> <p>The Gatsby docs outline this in the <a href="https://www.gatsbyjs.org/docs/schema-customization/">Schema Customization section</a>, but it didn't really click there for me because the examples are based on sourcing from Markdown as opposed to Drupal or another CMS. Things finally clicked a little when I found <a href="https://github.com/gatsbyjs/gatsby/issues/16809">this issue</a> on Github that showed how to work around a similar problem when sourcing from WordPress.</p> <h2>Three scenarios and three solutions</h2> <p><strong>Scenario 1: Empty field</strong></p> <p>This is by far the simplest scenario, but that doesn't make it simple. If you have a basic text field on a Drupal entity that does not yet have any content or at some point may not have any content, you don't want your queries to break your site. You need to tell GraphQL that the field exists and it needs to create the matching schema for it.</p> <p>Here's how it's done:</p> <pre> <code class="language-javascript">exports.createSchemaCustomization = ({ actions }) =&gt; { const { createTypes } = actions const typeDefs = ` type node__homepage implements Node { field_optional_text: String ` createTypes(typeDefs) }</code></pre> <p>Now, to walk through it a bit. We're using <code>createSchemaCustomization</code> here and passing in <code>actions</code>. The <code>actions</code> variable is an object that contains several functions so we destructure it to only use the one we need here: <code>createTypes</code>.</p> <p>We define our types in a GraphQL query string that we lovingly call <code>typeDefs.</code> The first line of the query tells which type we're going to be modifying, in this case <code>node__homepage</code>, and then what interface we're implementing, in this case <code>Node</code>. </p> <p><strong>NOTE: </strong>If you're looking at GraphiQL to get your field and node names it may be a bit misleading. For example, in GraphiQL and in my other queries <code>node__homepage</code> is called as <code>nodeHomepage</code>. You can find out what the type name is by running a query in GraphiQL that looks like:</p> <pre> <code>query MyQuery { nodeHomepage { internal { type } } }</code></pre> <p>or by exploring your definitions in GraphiQL.</p> <p>Next, we add the line <code>field_optional_text: String</code> which is where we define our schema. This is telling GraphQL that this field should exist and it should be of the type <code>String</code>. If there are additional fields, Gatsby and GraphQL are smart enough to infer them so you don't have to redefine every other field on your node.</p> <p>Finally, we pass our customizations into createTypes and restart our development server. No more errors!</p> <p><strong>Scenario 2: Entity reference field</strong></p> <p>Suppose we have a field for media that may not always have content. We still want our queries to work in Gatsby, but if there's only one Drupal entity of this content type and it happens to not have an image attached, then our queries are going to break, and so will our hearts. This requires a foreign key relationship because the entity itself is a node to GraphQL. </p> <p>Here's how it's done:</p> <pre> <code class="language-javascript">exports.createSchemaCustomization = ({ actions }) =&gt; { const { createTypes } = actions const typeDefs = ` type node__homepage implements Node { relationships: node__homepageRelationships } type node__homepageRelationships { field_background_media: media__image @link(from: "field_background_media___NODE") } ` createTypes(typeDefs)</code></pre> <p>Looks a little similar to the previous scenario, but there's a bit more to it. Let's look at the changes.</p> <p>Instead of defining our custom schema under the <code>type node__homepage implements Node {</code> line, we reference our <code>relationships</code> field which is where all referenced nodes live. In this case, we tell GraphQL that the <code>relationships</code> field is of type <code>node__homepageRelationships</code>. This is another thing you can find using GraphiQL. </p> <p>The next section further defines our relationships field. We declare that <code>node__homepageRelationships</code> should have the field <code>field_background_media</code> of type <code>media__image</code>, but what's that next part? That's where our foreign key link is happening. Since <code>field_background_image</code> is an entity reference, it becomes a node. This line links the field to the node created from the field. If you don't include the <code>@link(from: "field_background_media___NODE)</code> in your query you'll get a deprecation notice:</p> <pre> <code class="language-bash">warn Deprecation warning - adding inferred resolver for field node__homepageRelationships.field_background_media. In Gatsby v3, only fields with an explicit directive/extension will get a resolver.</code></pre> <p><em>Edit: This post was edited to reflect the deprecation notice instead of indicating that </em><code>@link</code><em> is optional.</em></p> <p><strong>Scenario 3: Related content field with multiple content types</strong></p> <p>This is the one that had me crying. I mean scream-crying. This is the widowmaker, the noisy killer, the hill to die upon. At least it WAS, but I am triumphant!</p> <p>Like my horrible joke from the beginning of this post, I have a related content field that is supposed to allow pretty much any content type from Drupal, but only has three slots. I need to have queries available for each content type, but if one doesn't exist on the field, then GraphQL makes a fuss. I can't say for sure, but I've got a feeling that it also subtly insulted my upbringing somewhere behind the scenes.</p> <p>Now, I'm here to save you some time and potentially broken keyboards and/or monitors.</p> <p>Here's how it's done:</p> <pre> <code class="language-javascript">exports.createSchemaCustomization = ({ actions }) =&gt; { const { createTypes } = actions const typeDefs = ` union relatedContentUnion = node__article | node__basic_page | node__blog_post | node__cat_food_reviews | node__modern_art_showcase | node__simpsons_references type node__homepage implements Node { relationships: node__homepageRelationships } type node__homepageRelationships { field_related_content: [relatedContentUnion] @link(from: "field_related_content___NODE") } ` createTypes(typeDefs) }</code></pre> <p>The biggest changes here are the union and the brackets around the type. From the GraphQL docs:</p> <blockquote class="pullquote"> <p>Union types are very similar to interfaces, but they don't get to specify any common fields between the types.</p> </blockquote> <p>This means that any type in the union can be returned and they don't need to have the same fields. Sounds like exactly what we need, right?</p> <p>So we define our union of our six content types and create the relationship using a foreign key link. We use the node created from our field, <code>field_related_content___NODE</code> as our foreign reference, and we get results. However, the brackets around the union are there, and they must do something. In fact they do. They indicate that what will be returned is an array of content in this union, meaning that there will be multiple values instead of one single value returned.</p> <h2><strong>Summary</strong></h2> <p>This seriously took me much longer than it should have to figure out. Hopefully, you don't suffer my same fate and find this post useful. One thing to note that almost destroyed me is that there are THREE, count them 1, 2, 3, underscores between the field name and <code>NODE</code> in the link. <code>_ _ _ NODE</code>. I only put two when I was fighting this and it cost me more time than I'm willing to admit on a public forum.</p> <p>Special thanks to Shane Thomas (AKA <a href="http://codekarate.com/">Codekarate</a>) for helping me sort through this and being able to count to 3 better than me.</p> </div> <div class="field field--name-field-category field--type-entity-reference field--label-above"> <div class="field__label">Category</div> <div class="field__item"><a href="/development" hreflang="en">Development</a></div> </div> <div class="field field--name-field-tags field--type-entity-reference field--label-above"> <div class="field__label">Tags</div> <div class="field__items"> <div class="field__item"><a href="/taxonomy/term/12" hreflang="en">Drupal Planet</a></div> <div class="field__item"><a href="/taxonomy/term/21" hreflang="en">GatsbyJS</a></div> <div class="field__item"><a href="/taxonomy/term/14" hreflang="en">react</a></div> <div class="field__item"><a href="/taxonomy/term/11" hreflang="en">Drupal</a></div> </div> </div> <div class="field field--name-field-comments field--type-disqus-comment field--label-visually_hidden"> <div class="field__label visually-hidden">Comments</div> <div class="field__item"><drupal-render-placeholder callback="Drupal\disqus\Element\Disqus::displayDisqusComments" arguments="0=GatsbyJS%20%2B%20Drupal%3A%20Create%20Custom%20GraphQL%20Schema%20for%20Empty%20Fields&amp;1=https%3A//www.jamesdflynn.com/development/gatsbyjs-drupal-create-custom-graphql-schema-empty-fields&amp;2=node/25" token="Fu4Xzhm6LhLP1mehXXs8EvSN1gc2t-1a52sQbhgkkE4"></drupal-render-placeholder></div> </div> <div class="field field--name-field-header-image-entity field--type-entity-reference field--label-hidden field__item"><article class="media media--type-image media--view-mode-embedded"> <div class="field field--name-image field--type-image field--label-hidden field__item"> <img src="/sites/default/files/negative-space-compiled-code-screen.jpg" width="1200" height="800" alt="Generic &quot;Look at my screen full of code&quot; screen" loading="lazy" typeof="foaf:Image" /> </div> </article> </div> Mon, 18 Nov 2019 20:40:00 +0000 jflynn 25 at https://www.jamesdflynn.com Connecting React to Drupal in a Progressive Decoupled App https://www.jamesdflynn.com/development/connecting-react-drupal-progressive-decoupled-app <span class="field field--name-title field--type-string field--label-hidden">Connecting React to Drupal in a Progressive Decoupled App</span> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">jflynn</span></span> <span class="field field--name-created field--type-created field--label-hidden">Thu, 01/18/2018 - 09:09</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><h2>Keeping up with the times</h2> <p>ReactJS is all the rage these days, and for good reason. It's a great library, it's well known, it's well documented, and it's easy to jump into if you have any JavaScript experience. Between its nice learning curve and the ability to use <a href="https://jsx.github.io/">JSX</a> it makes sense to use it on applications that you want to be fast and scalable.</p> <p>Headless is all the rage these days in the Drupal community, and for good reason. It gives Drupal the chance to do what it's best at, managing content. Sure, Drupal is great at other stuff, but the first two words of CMS are "Content" and "Management".</p> <p>When we look back at the release of Drupal 8, we notice that one of the biggest pieces of D8 was a built in REST API with minimal configuration needed. This opened up a world of possibility for developers to merge the speed and scalability of JavaScript with the content management framework strengths of Drupal.</p> <h2>How Decoupled Should You Go</h2> <p><a href="https://dri.es/how-to-decouple-drupal-in-2018">In a recent blog post</a>, Dries goes into the Future of Decoupled Drupal. In it he mentions progressive vs fully decoupled. Personally, with a background specifically in Drupal and PHP and only a side of JavaScript, I prefer the progressively decoupled approach that keeps all the familiar parts of the Drupal ecosystem available, like the theme layer, the admin side, the block rendering engine, etc., but also allows the flexibility to render an app that uses React or Angular or any of the other libraries out there that can consume data from a REST API.</p> <p>I have more familiarity with theming Drupal than anything, so that makes it less painful to build something with Drupal than it would if I were to try and build a SPA with a completely different front end that still consumes the Drupal data.  For this post I'm going to be focusing primarily on a progressively decoupled solution.</p> <h2>My Progressively Decoupled Solution</h2> <p>The project that brought us to this involved a site that revolved around a single search app and we wanted it to be fast and easy to use, but there were other aspects to the site that would benefit from being Drupally.  We needed blocks in certain places, a user configurable menu system, login/logout capabilities, and a decent content management side for users to enter content in a friendly way, but we also wanted to have some decoupled/headless elements using the ReactJS library.</p> <p>For React to work inside Drupal, the React side needed something to hold onto, known as a Mount Node.  To the Drupal dev, the term "mount node" can be misleading since nodes have a completely different meaning in the Drupal world, but in the React world, a mount node is a node in the DOM that the JavaScript side can grab onto and take control of.  The index.js file contains the code that defines the expected ID of the mount node and it looks a little like this:</p> <pre> <code class="language-javascript">ReactDOM.render(&lt;App /&gt;, document.getElementById('react-div'));</code></pre> <p>Where 'react-div' is the ID of the div that React will be mounting to.  Essentially, the React app will take that div and inject all of its code inside of there as the parent of the React app's DOM.  What does that mean for us in the Drupal world?  Basically, we need to have a div somewhere inside a page for or Drupal node for the React app to grab onto.</p> <p>There are a few different ways that we could handle this.</p> <ul><li>A custom twig file built specifically for a single use case</li> <li>Full HTML body field in a Drupal node with a matching ID</li> <li>A custom content type with a twig file</li> <li>Probably more, but I don't like lists going longer than they should</li> </ul><p>For my project, I decided to use the third option:  A custom content type with an associated twig file, and I wanted it to be reusable so I made a module for that, <a href="http://drupal.org/project/reactjs_mount">React Mount Node</a>, and I contributed that module back to Drupal so that anyone can use it.</p> <p>Thanks to the magic of <a href="https://drupalconsole.com/">Drupal Console</a> I was able to quickly bootstrap everything that I needed for a module including the <code>.module</code> file, the <code>.info.yml</code> file, and a composer.json file in case I wanted to contribute the module back to the community.</p> <p>After getting the base of the module setup, there were a few other things that needed to happen.  Specifically, I needed a content type to be built within the module.  Fortunately, there's a pretty simple way to do that outlined in <a href="https://www.drupal.org/docs/8/creating-custom-modules/include-default-configuration-in-your-drupal-8-module">this documentation</a>.  We need a <code>config/install/node.type.&lt;content-type&gt;.yml</code> in our module to give us some info about our new content type, and then we need our config to build out a content type.  A few ways to do this are by exporting config and cleaning it up a bit by drush or through the UI, or we can use Drupal Console to do it.  Either way, we need to make sure that the UUID gets stripped so we don't run into any errors.</p> <p>Depending on how complex your content type is, you may only need do edit one or two files, maybe more.  The important thing is that you look through and delete the UUID line in every relevant <code>.yml</code> file that comes from your new content type.</p> <pre> <code class="language-yaml">uuid: be001aff-1508-485c-916b-86062ebdb811 &lt;&lt;&lt; GET RID OF ME langcode: en status: true</code></pre> <p>Drupal Console does this automatically when you export a content type.</p> <p>The content type created by this module is called React Mount Node because it creates a node that you use to mount a React app.  Get it?  GET IT???</p> <p>React Mount Node has three fields: Title, Div ID, and Library. The Title field is just for the title of the node that you're creating.  The Div ID is where you set the ID that you're using inside of your React app that you set in <code>index.js</code>. The Library field is where you tell the module what library it should be looking for.  We'll get to that in a moment.</p> <p>If you're building the React side of things in addition to the Drupal side, you may notice that out-of-the-box, React spits code out using the naming convention app-<code>name.hash.min.js. </code>I highly recommend changing that to something a little more friendly.  For the setup I use, <code>create-react-app</code>, I can do that inside of the webpack config file at the line in the output object array under filename that should look something like:</p> <pre> <code class="language-javascript"> output: { // The build folder. path: paths.appBuild, // Generated JS file names (with nested folders). // There will be one main bundle, and one file per asynchronous chunk. // We don't currently advertise code splitting but Webpack supports it. filename: 'path/to/your/libraries/react-app/react-app.js', chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',</code></pre> <p>It's my suggestion that you do this so that every time you compile your React app you don't have to change your libraries files.  It's a pain.  I speak from experience.  Trust me.</p> <p>This will give you a reusable filename to that you can, as the infomercials say, "SET IT AND FORGET IT!"</p> <p>What this doesn't give you, however, is the actual library.  In Drupal 8 we've changed from <code>drupal_add_js()</code> and <code>drupal_add_css()</code> to something a little more elegant.  Libraries.</p> <h2>Creating and Connecting Your Library</h2> <p>Libraries are essentially collections of assets.  If you look in your theme folder, you'll see a file that ends in <code>.libraries.yml</code>.  This is true for every theme, including Bartik and Stark.  The <code>themename.libraries.yml</code> file is where the libraries are built, at least those associated with that theme.  It's slightly different for libraries defined in modules, but the base concepts remain the same.  If you were inclined to place your compiled React app into a module, it would be a very similar procedure.</p> <p>For our purposes though, we added the React app into the theme folder and defined the library in <code>themename.libraries.yml</code>.  It looks like this </p> <pre> <code class="language-yaml">react-app: js: libraries/react-app/react-app.js: {} </code></pre> <p>This is where the Library field on our React Mount Node content type comes into play.  Because we defined the library in our theme, the library name is <code>themename/react-app</code> and that's what we place in the field.</p> <p>Another beauty of D8 is that we don't have to load every JS file on every page.  That library will only be loaded on the node(s) it's attached to.  How do we do this? Through the magic of twig.  Twig allows us to attach a library on a per-node/page/block/site basis with one simple line of code:</p> <pre> <code class="language-twig"> {{ attach_library(my_lib) }}</code></pre> <p>That's the line from the twig file that's used to render the React Mount Node content type and it tells Drupal to attach this library to this node that has this ID.  The end result is a React app living inside of a Drupal node.</p> <div alt="React app in a node" data-embed-button="media" data-entity-embed-display="media_image" data-entity-embed-display-settings="crop_freeform" data-entity-type="media" data-entity-uuid="a7b65001-a14d-466d-953e-ee4585fbf052" data-langcode="en" title="React app in a node" class="embedded-entity"> <img src="/sites/default/files/styles/crop_freeform/public/Screenshot%20from%202018-01-18%2014-43-23.png?h=97ee99ce&amp;itok=S8c49klQ" width="610" height="343" alt="React app in a node" title="React app in a node" loading="lazy" typeof="foaf:Image" class="image-style-crop-freeform" /></div> <p>This is really the tip of the iceberg though.  With this module and the flexibility of React and a REST API we're able to do a ton of stuff.  I highly suggest getting to know the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a> that makes getting data a breeze and learning more about how to setup Drupal as a REST endpoint.</p> <p>I'll be doing posts in reference to the REST API, normalization, and one of my new favorite things -- REST Flagging in the near future.  In the meantime, what excites you the most about going decoupled with Drupal?</p> </div> <div class="field field--name-field-category field--type-entity-reference field--label-above"> <div class="field__label">Category</div> <div class="field__item"><a href="/development" hreflang="en">Development</a></div> </div> <div class="field field--name-field-tags field--type-entity-reference field--label-above"> <div class="field__label">Tags</div> <div class="field__items"> <div class="field__item"><a href="/taxonomy/term/11" hreflang="en">Drupal</a></div> <div class="field__item"><a href="/taxonomy/term/12" hreflang="en">Drupal Planet</a></div> <div class="field__item"><a href="/taxonomy/term/13" hreflang="en">reactjs</a></div> <div class="field__item"><a href="/taxonomy/term/14" hreflang="en">react</a></div> </div> </div> <div class="field field--name-field-comments field--type-disqus-comment field--label-visually_hidden"> <div class="field__label visually-hidden">Comments</div> <div class="field__item"><drupal-render-placeholder callback="Drupal\disqus\Element\Disqus::displayDisqusComments" arguments="0=Connecting%20React%20to%20Drupal%20in%20a%20Progressive%20Decoupled%20App&amp;1=https%3A//www.jamesdflynn.com/development/connecting-react-drupal-progressive-decoupled-app&amp;2=node/11" token="-uKCUddNbDvlZF9abdI-CNb4HqmkplDAmRlbONNNHGc"></drupal-render-placeholder></div> </div> Thu, 18 Jan 2018 15:09:06 +0000 jflynn 11 at https://www.jamesdflynn.com