Development https://www.jamesdflynn.com/ en A Docksal Training -- ON DEMAND! https://www.jamesdflynn.com/development/docksal-training-demand <span class="field field--name-title field--type-string field--label-hidden">A Docksal Training -- ON DEMAND!</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">Fri, 12/18/2020 - 18:49</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>It has been quite a while since I last posted anything, mainly because 2020 has been 2020 and I haven't been in the best mindset for several months. I've had a lot of difficulty focusing on anything, and as a result, I've found my motivation has gone pretty much away.</p> <p>However, last month I decided that lack of motivation be damned. I spent quite a bit of time converting a training that I had prepped to give at camps and conferences into a YouTube video series. This is a 12 lesson series that takes the viewer from a brief intro to Docker all the way up to building a custom Docksal application that combines 2 servers with a NodeJS backend and a PHP backend to simulate a production decoupled web application setup.</p> <p>Here is a quick rundown of the videos and what you might expect from each:</p> <h2>Part 1 - Intro and Docker Basics</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/IXSUohtU4Rc&amp;max_width=854&amp;max_height=480&amp;hash=DooTW5_8qSJ-BVamUudATEMilxTtugwEfXjxKzxzJ_M" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 1: About This Series and Intro to Docker"></iframe> </div> </article><p>The first video is a starting point for someone who may or may not have much experience with Docker. I wouldn't recommend learning React without knowing some JavaScript, so I can't recommend using Docksal without having a bit of an understanding of Docker. </p> <h2>Part 2 - The Gush</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/wqc5crJhs-4&amp;max_width=854&amp;max_height=480&amp;hash=pZ_kX-InTzF17oeArpYEKP9R1mAOYMHJojkcaFhu4ho" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 2: History of Docksal and Why it's Great"></iframe> </div> </article><p>This second part is basically where I fanboy about Docksal and why I think it's great.  If you want to hear me ranting about the benefits of Docksal and everything it brings to the party, this is your clip!</p> <h2>Part 3 - Docksal Stacks</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/ujvAwNLIHeM&amp;max_width=854&amp;max_height=480&amp;hash=YQbsmeToTL81b7SiFg1aJ-CULMona-HP52iLXgXGWcQ" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 3: Docksal Stacks"></iframe> </div> </article><p>Docksal uses .yml files that define various stacks. Stacks are collections of services that make up an application. If you want to learn more, stop reading and check out the video!</p> <h2>Part 4 - Docksal Services</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/4wrMI4PoMFo&amp;max_width=854&amp;max_height=480&amp;hash=ZUii_krDd6_cAkbYlQ6-UC1GbDyY0MrOpnbyRo4yHao" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 4: Docksal Services"></iframe> </div> </article><p>Remember way back under the last video when I used the word "services"? I remember. This video goes into what services are available to make up the stacks that are preconfigured or that you build on your own.</p> <h2>Part 5 -Getting Started with Docksal</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/EEpSdOj-JhI&amp;max_width=854&amp;max_height=480&amp;hash=CW7zH_Z1nnCjYubZ-Xu1fPzaa5pmgt_Vae_Dwg4Hlrs" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 5: Getting Started Using Docksal"></iframe> </div> </article><p> Yeah, I know... 5 videos in is a long time to wait for a "Getting Started" vid, but the wait is worth it. Get your editors ready, roll up your sleeves, and get learning!</p> <h2>Part 6 - Using a Boilerplate</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/c0ftkoQeP5U&amp;max_width=854&amp;max_height=480&amp;hash=fUJC5kw8UbOopx9ejFUwGo1lzshGiMxSIkejwXANWqI" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 6: Starting with a Boilerplate"></iframe> </div> </article><p>It's not just for warming the kettle anymore! A boilerplate is a ready-to-go application that you just need to start and it works. In part 6 we're going to look at spinning up a project with one of these boilerplates.</p> <h2>Part 7 - Installing a Drupal Site</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/BtmWR4z8KUI&amp;max_width=854&amp;max_height=480&amp;hash=tnYAUE15JXTMj4g12hOO9pvx_6dpDxLhppiXk6Fgtn4" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 7: Installing a Drupal Site"></iframe> </div> </article><p>This is what we've been working up to. This video will show you how to use Docksal to install and run a brand new Drupal site on your local machine. It takes less time than you think!</p> <h2>Part 8 - Doing More with Docksal and Drupal</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/6TbRidTTnf8&amp;max_width=854&amp;max_height=480&amp;hash=7uGDRCyMELiy5SZ4TWwusbHF7-Z1VlZrasPsFfddEtI" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 8: Doing More with Docksal and Drupal"></iframe> </div> </article><p> Let's take it to 11. This tutorial goes over some of the ways we can further customize a local site, going so far as making the local Docksal environment actually mirror our hosting platform!</p> <h2>Parts 9, 10, 11 - Advanced Customization Projects</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/Z-71qNytH4I&amp;max_width=854&amp;max_height=480&amp;hash=mprWZtQkAZ73y-NbwAh6dbK6y__OQEkByaeZgd9P2es" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 9: Advanced Customization Project 1"></iframe> </div> </article><article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/cchLa7o7iDw&amp;max_width=854&amp;max_height=480&amp;hash=NXD_G7J8FxN6Tq7h7S9RzXOE2GYBph157KQQ8VbGpQY" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 10: Advanced Customization Project 2"></iframe> </div> </article><p> </p> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/5G5o3VWvm6w&amp;max_width=854&amp;max_height=480&amp;hash=7jjqbXLoSVOH3LstuAfQ1NuB4MbnkmTJDRzub8pOtr8" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 11: Advanced Customization Project 3"></iframe> </div> </article><p>Each of these gets a little more in-depth with the customizations and they build off the previous for the most part.  I had a lot of fun coming up with Project 3.  What do you think?</p> <h2>Part 12 - Local Files</h2> <article class="media media--type-video media--view-mode-embedded" data-align="center"><div class="field field--name-field-media-video-embed-field field--type-string field--label-hidden field__item"><iframe src="/media/oembed?url=https%3A//youtu.be/xrTg-1bI43g&amp;max_width=854&amp;max_height=480&amp;hash=TGmzg7t7rH-6jMfYRZMwhB0P7xBL7Ip73hCOKF4mb_U" frameborder="0" allowtransparency="" width="854" height="480" class="media-oembed-content" title="Let's Learn Docksal Part 12: Using Local Files"></iframe> </div> </article><p> Let's call this one a bit of housekeeping. It doesn't matter what you use Docksal for if you end up committing your API keys to a public repo. (Don't do that!) This one goes over how to use local Docksal files to keep your safe, secret info safe and secret.  </p> <h2>Conclusion</h2> <p>This is my first attempt at instructional videos, but hopefully not my last.  I'll be posting stuff as I can when I have time, but I'd really like to know what you all would like to see me cover. Feel free to comment on one of the videos above, on this post, on Twitter, or wherever else I might see it. Be sure to subscribe to my channel <a href="https://www.youtube.com/channel/UC8CnBd5PYNhtA0SWcYqO5hQ">JDDoesDev</a> on YouTube to get notified when I put new stuff up.  Until then, you're my favorite person.</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/24" hreflang="en">Docksal</a></div> <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> </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=A%20Docksal%20Training%20--%20ON%20DEMAND%21&amp;1=https%3A//www.jamesdflynn.com/development/docksal-training-demand&amp;2=node/27" token="3CCsxH3kt-RDlNfcljLCt3Ce6GoXYD77q8d_yOL60RY"></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> Sat, 19 Dec 2020 00:49:45 +0000 jflynn 27 at https://www.jamesdflynn.com 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 Docksal gets a Training https://www.jamesdflynn.com/development/docksal-gets-training <span class="field field--name-title field--type-string field--label-hidden">Docksal gets a Training</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">Tue, 11/12/2019 - 17:19</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p name="c1e8" id="c1e8">In July of last year I started a new job as a developer with a new agency. During my first week, in between meetings, HR trainings, and all the other fun things that happen during onboarding, I was introduced to the preferred local development environment that was being used on most of the projects.</p> <p name="360e" id="360e">It was lightweight, based on Docker, it ran easily, and it was extremely easy to configure. Prior to this, I had bounced around from local setup to local setup. My local dev environment resume included such hits as MAMP, WAMP, Acquia Dev Desktop, Kalabox, VAMPD, DrupalVM, Vagrant, ScotchBox, VirtualBox, native LAMP stacks, and everything in between. All of them had their strengths and weaknesses, but none of them really had that spark that really hooked me.</p> <h2 name="532e">Enter Docksal.</h2> <p name="fabb" id="fabb">When I first started using Docksal, I thought it was just like any other setup, and to a point, it is. It creates a reusable environment that can be shared across multiple developers and setup to mimic a hosting provider to a certain point, but the two things that really grabbed me were how easy it was to get started and how fast it was compared to other systems. Docksal has one key, killer feature in my opinionated mind, and that’s the fact that the entire application is written in Bash. The primary binary (which may or may not be the name of my upcoming one-man, off-Broadway, off-any stage show) begins with <code>#! /usr/bin/env bash</code> and runs on any system that has the bash executable, which encompasses Linux (of course), macOS, and now Windows thanks to WSL and the ability to add Ubuntu.</p> <p name="0b23" id="0b23">One thing that was missing, though, was a training guide. It has AMAZING documentation, available at <a data-href="https://docs.docksal.org" href="https://docs.docksal.io" rel="noopener" target="_blank">https://docs.docksal.io</a>, including a great getting started walkthrough, but for someone just starting out using it who might not have guidance and support from people they work with, it might take a little getting used to.</p> <p name="0346" id="0346">If you know me, you know that I enjoy talking at conferences. I’ve given over two dozen presentations at several types of events from local meetup groups to national level conferences. If you don’t know me, you just learned something new about me. Since I enjoy talking in front of people so much, the next logical step was to find something I’m familiar with and make a training of it. Turns out, I’m familiar with Docksal.</p> <p name="6a19" id="6a19">I submitted my pitch for a training to <a data-href="https://www.nedcamp.org" href="https://www.nedcamp.org" rel="noopener" target="_blank">NEDCamp</a>, the New England Drupal Camp, and they accepted it. Since I now had a reason to write a training, I began writing a training. Initially, I started with a very high-level outline, and eventually built a framework for my training. Thanks to the nature of open source, I was able to use many of the features that <a href="https://docs.docksal.io">https://docs.docksal.io</a> already had in order to make my training seem a little familiar to current users and easily accessible to new users.</p> <p name="6a19" id="6a19">The first go at this training will be at NEDCamp 2019 on Friday, November 22nd. This will be the first time a dedicated training spot has been used to train on Docksal, and I'm extremely excited to see how it goes and how to improve. After that training, I will make my handbook available online, eventually to be merged into the <a href="https://github.com/docksal">Docksal Github</a> repo as part of the documentation. I have had help from numerous people in building this training, especially from the Docksal maintainers, Sean Dietrich, Leonid Makarov, Alexei Chekiulaev; folks who have reviewed what I've written so far, Dwayne McDaniel and Wes Ruvalcaba; and people who have challenged me to learn more about Docksal, whose numbers are too high to list them all.</p> <p name="6a19" id="6a19">If you're interested in learning how to use Docksal or what it's all about, consider attending my training at NEDCamp on November 22nd. You can find all the details on the <a href="https://nedcamp.org/trainings/2019/modern-local-development-docksal">NEDCamp training page</a>, and if you can't make it, be sure to watch for the handbook to be released soon.</p> <p name="6a19" id="6a19">Since I'm still working on the finishing touches, why not take the time to let me know what you would like to get out of this type of training or what you wish you would have known when learning how to use Docksal or a similar product in the comments and where you feel extra attention should be placed.</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/24" hreflang="en">Docksal</a></div> <div class="field__item"><a href="/taxonomy/term/11" hreflang="en">Drupal</a></div> <div class="field__item"><a href="/taxonomy/term/25" hreflang="en">NEDCamp</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=Docksal%20gets%20a%20Training&amp;1=https%3A//www.jamesdflynn.com/development/docksal-gets-training&amp;2=node/24" token="Bz1-Riqy3ai2IYaGF2R2NRgnrMNZO8JIy7b8U9rGNhM"></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/Screen%20Shot%202019-11-12%20at%205.40.36%20PM.png" width="703" height="436" alt="Screenshot of VSCode with minified HTML" loading="lazy" typeof="foaf:Image" /> </div> </article> </div> Tue, 12 Nov 2019 23:19:43 +0000 jflynn 24 at https://www.jamesdflynn.com Using Drupal Blocks in a Decoupled GatsbyJS Application https://www.jamesdflynn.com/development/using-drupal-blocks-decoupled-gatsbyjs-application <span class="field field--name-title field--type-string field--label-hidden">Using Drupal Blocks in a Decoupled GatsbyJS Application</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, 11/03/2019 - 12:40</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>One of the most useful components of Drupal is the <a href="https://www.drupal.org/docs/8/core/modules/block">Block system</a>. This system allows for pieces of content to be created and reused throughout a site in various regions of the page structure. You can also select which bundles or content types this piece of content should appear on, making it so that a blog post gets a certain Call to Action section while an article gets something similar, but different.</p> <p>When we use GatsbyJS for a decoupled front-end solution with a Drupal back-end, we lose out on quite a bit, if not everything, that the theme layer of Drupal provides. This means that Drupal regions mean nothing to Gatsby, nor does block placement. In fact, any functionality that would go into a <code>.theme</code> file is no longer available on the Drupal side.</p> <p>Before I get into the "how" of using Drupal blocks in Gatsby, I want to cover a little bit of the "why".</p> <h2>Why Use Drupal Blocks for Content in a Gatsby Front End?</h2> <p>The main advantage of blocks in Drupal is that the content is created in one place, but can be placed in several spots. I have worked with solutions that use Paragraphs (from the Paragraphs contrib module) for similar functionality, but the problem remains where the content needs to be recreated on every new parent entity. For example, I can create a Call to Action (CTA) Paragraph and place fields that reference them on every content type, but the Paragraphs themselves remain separate. A Paragraph is more like a collection of fields to be filled out than a reusable entity, and that's okay.</p> <p>In contrast, a Block can be created in one place, usually using the Block UI, and the content of the Block remains the same no matter where it is placed. A CTA block on a blog post will have identical content to the CTA block on an article. This makes content changes extremely fast compared to having to update every article and blog post if the wording or link need to change.</p> <p>It is entirely possible to create these types of entities in Gatsby by defining a reusable component, however the editing experience doesn't really pan out. It may require a developer to go in and edit a React component, adding a middle-person to the process. Using reusable Drupal blocks can save time and budget when set up appropriately.</p> <h2>How to Use Drupal Blocks for Content in a Gatsby Front End</h2> <p>This section is going to make a few assumptions.</p> <ol><li>You have a basic understanding of Gatsby and React components.</li> <li>You understand the Drupal theme layer.</li> <li>You have a basic understanding of the Drupal block system.</li> <li>Your decoupled application is already setup and pulling content from Drupal (not necessary, but it helps if you can try it out)</li> <li>You have the JSON:API module enabled on Drupal</li> <li>You're sourcing content from Drupal.</li> </ol><p>If you're having problems with any of these, feel free to reach out to me on Drupal Slack, I go by Dorf there.</p> <p>Now the "how".  We're going to start by creating a Custom Block Type. Let's keep going with the CTA theme and call it "CTA Block". We do this by logging into our site as someone with permissions to access the Block UI and going to <code>admin/structure/block/block-content/types.</code> Once there, select "Add custom block type".</p> <p>Let's label this CTA Block and begin creation. After we create it, we need to add some fields, so let's add the following fields:</p> <ul><li><strong>CTA Heading</strong> <ul><li>A plain text field, max 255 chars</li> </ul></li> <li><strong>CTA Link</strong> <ul><li>A Link field using URL and label, internal and external links, Label required.</li> </ul></li> <li><strong>CTA Content Types</strong> <ul><li>This is the field that will make all the difference. Create this field as Reference: Other... to begin with.</li> <li>Label it CTA Content Type.</li> <li>Under "Type of entity to reference" choose "Content type" from the select list, under Configuration.</li> <li>Set unlimited cardinality.</li> <li>Go through the remaining screens to create this field. Once you're back on the field list, select "Manage form display"</li> <li>From here, we're going to change the widget from Autocomplete to Check boxes/radio buttons.</li> <li>Save your changes</li> </ul></li> </ul><p>Now we're going to create a new block using this Custom Block Type. When we create the block under <code>block/add/cta_block</code> the form should look something like this:</p> <div alt="Screenshot of Block form" data-embed-button="media" data-entity-embed-display="media_image" data-entity-embed-display-settings="crop_freeform" data-entity-type="media" data-entity-uuid="016cec0b-963a-4042-a527-566d7f30d5d5" data-langcode="en" title="Block Form" class="embedded-entity"> <img src="/sites/default/files/styles/crop_freeform/public/Screen%20Shot%202019-11-03%20at%205.48.19%20PM_0.png?itok=ku5y9vEF" alt="Screenshot of Block form" title="Block Form" typeof="foaf:Image" class="image-style-crop-freeform" /></div> <p>Now, add whatever text you want to the fields, but only select a single content type in the CTA Content Type field.  Save the block and spin up your Gatsby dev environment. We're going to switch over there for a bit.</p> <p>Let's fire up our develop environment and take a look at GraphiQL to see what we have going on.</p> <div alt="GraphiQL query" data-embed-button="media" data-entity-embed-display="media_image" data-entity-type="media" data-entity-uuid="ea08daca-c426-4823-be5a-199d6ac056ba" data-langcode="en" title="GraphiQL" class="embedded-entity"> <img src="/sites/default/files/Screen%20Shot%202019-11-03%20at%206.54.59%20PM.png" alt="GraphiQL query" title="GraphiQL" typeof="foaf:Image" /></div> <p>As you can see, we now have access to <code>allBlockContentCtaBlock</code> in GraphiQL, but what are we going to do with it? Well, we are first going to create the CTA Block React component. We'll do that by creating the file <code>src/components/blocks/CtaBlock.js</code> and adding the following:</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' const CtaBlock = ({ data }) =&gt; { return &lt;div class='ctaBlock'&gt; &lt;h3&gt;CTA Heading&lt;/h3&gt; &lt;p&gt;CTA Text goes here and here and here.&lt;/p&gt; &lt;a href="http://example.com"&gt;Link Text&lt;/a&gt; &lt;/div&gt; } export default CtaBlock </code></pre> <p>This is pretty simple and doesn't include anything having to do with our GraphQL query yet, but we have the structure in place. Now, let's look at the data we can pull from GraphQL. We want to get the heading, body, link, and content type, so our query is going to look something like this:</p> <pre> <code class="language-json">query MyQuery { allBlockContentCtaBlock { nodes { field_cta_heading field_cta_link { title uri } body { value } relationships { field_cta_content_type { name } } } } } </code></pre> <p>Which will give us back:</p> <pre> <code class="language-json">{ "data": { "allBlockContentCtaBlock": { "nodes": [ { "field_cta_heading": "Heading for the CTA Block", "field_cta_link": { "title": "Learn more!", "uri": "http://google.com" }, "body": { "value": "&lt;p&gt;Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Vestibulum facilisis, purus nec pulvinar iaculis, ligula mi congue nunc, vitae euismod ligula urna in dolor. Aenean massa.&lt;/p&gt;\r\n\r\n&lt;p&gt;Praesent nec nisl a purus blandit viverra. Nullam nulla eros, ultricies sit amet, nonummy id, imperdiet feugiat, pede. Phasellus dolor.&lt;/p&gt;\r\n" }, "relationships": { "field_cta_content_type": [ { "name": "Blog Post" } ] } } ] } } }</code></pre> <p>Perfect! Now I want you to notice a couple of things here. First, we're querying ALL of the CTA blocks here. Second, we're using the content type in the query. Let's get this into our component. Add in the query to our CtaBlock.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' const CtaBlock = ({ data }) =&gt; { return &lt;div class='ctaBlock'&gt; &lt;h3&gt;CTA Heading&lt;/h3&gt; &lt;p&gt;CTA Text goes here and here and here.&lt;/p&gt; &lt;a href="http://example.com"&gt;Link Text&lt;/a&gt; &lt;/div&gt; } export default CtaBlock export const CtaBlockQuery = graphql` allBlockContentCtaBlock { nodes { field_cta_heading field_cta_link { title uri } body { value } relationships { field_cta_content_type { name } } } }`</code></pre> <p>But wait, this isn't a page template, and it shouldn't be. What's the problem then? We have a query in a non-page template, and that's not good. What we need to do is figure out which of our templates are going to use this block and add it in there. Since we chose to have the block show on the blog post content type, we're going to use the blog post template. This will probably differ for your setup.</p> <p>Let's open up our page template and take a look:</p> <pre> <code class="language-javascript"> import React from 'react' import { graphql } from 'gatsby' import Layout from '../layouts' const BlogPostTemplate = ({ data }) =&gt; { const blogBody = data.nodeBlogPost.body.value return &lt;Layout&gt; &lt;h3&gt;{data.nodeBlogPost.title}&lt;/h3&gt; {data.nodeBlogPost.body.value} &lt;/Layout&gt; } export default BlogPostTemplate export const query = graphql` query($BlogPostID: String!){ nodeBlogPost(id: { eq: $BlogPostID }) { id title body { processed } } } ` </code></pre> <p>Now let's update this to have the block appear.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' import Layout from '../layouts' import CtaBlock from '../blocks/CtaBlock' const BlogPostTemplate = ({ data }) =&gt; { const blogBody = data.nodeBlogPost.body.value return &lt;Layout&gt; &lt;h3&gt;{data.nodeBlogPost.title}&lt;/h3&gt; {data.nodeBlogPost.body.value} &lt;CtaBlock /&gt; &lt;/Layout&gt; } export default BlogPostTemplate export const query = graphql` query($BlogPostID: String!){ nodeBlogPost(id: { eq: $BlogPostID }) { id title body { processed } } } ` </code></pre> <p>But we still need to include the query for the block itself. To do this, we're going to make a few edits to both of our components. We'll start with the CtaBlock component and convert the query into a fragment that we can reuse in multiple places if need be.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' const CtaBlock = ({ data }) =&gt; { return &lt;div class='ctaBlock'&gt; &lt;h3&gt;CTA Heading&lt;/h3&gt; &lt;p&gt;CTA Text goes here and here and here.&lt;/p&gt; &lt;a href="http://example.com"&gt;Link Text&lt;/a&gt; &lt;/div&gt; } export default CtaBlock export const CtaBlockQuery = graphql` fragment CtaBlockQuery on block_content__cta_block { field_cta_heading field_cta_link { title uri } body { value } relationships { field_cta_content_type { name } } }`</code></pre> <p>If you have a keen eye, you'll notice that we've removed the nodes section of the query. This is because we're now going to query for a single block. However, there is some risk to this. In the event that a content creator creates a new CTA block on Drupal for this content type instead of editing the existing one, the old one will remain in place because a single item query will only return a single item.</p> <p>Now, let's move back over to our page template and use this fragment to query for our block.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' import Layout from '../layouts' import CtaBlock from '../blocks/CtaBlock' const BlogPostTemplate = ({ data }) =&gt; { const blogBody = data.nodeBlogPost.body.value return &lt;Layout&gt; &lt;h3&gt;{data.nodeBlogPost.title}&lt;/h3&gt; {data.nodeBlogPost.body.value} &lt;CtaBlock /&gt; &lt;/Layout&gt; } export default BlogPostTemplate export const query = graphql` query($BlogPostID: String!){ nodeBlogPost(id: { eq: $BlogPostID }) { id title body { processed } } blockContentCtaBlock (relationships: {field_cta_content_types: {elemMatch: {name: {eq: "Blog Post"} } } } ){ ...CtaBlockQuery } } `</code></pre> <p>Take a look at what we've done here.  We've added in a query for the CtaBlock and we're filtering it by the content type it's attached to. After that, we're pulling in everything from the query on our component. This is exactly what we were wanting to do, but there's another step that we need to take to actually use the data on our component.</p> <p>If you look at the JSX element for</p> <pre> <code>&lt;CtaBlock /&gt;</code></pre> <p><br /> you'll notice we aren't passing anything to it, so we've got to make sure it has data to work with or we're going to end up rendering nothing.  Edit that line to be </p> <pre> <code>&lt;CtaBlock data={data} /&gt;</code></pre> <p>In case you're not familiar, this is a React concept known as passing props, or properties, to a child component. We're passing the data object that was returned from our GraphQL query to the CtaBlock component so that it can use the included data. Since this is just a demo, we're passing the entire thing along, but it's easy enough to only pass the relevant parts of the object.</p> <p>Now back in our CtaBlock component we can use the data to render out our block's content.</p> <pre> <code class="language-javascript">import React from 'react' import { graphql } from 'gatsby' const CtaBlock = ({ data }) =&gt; { return &lt;div class='ctaBlock'&gt; &lt;h3&gt;{data.blockContentCtaBlock.field_cta_heading}&lt;/h3&gt; &lt;p dangerouslySetInnerHtml= {{ __html: data.blockContentCtaBlock.body.value}} /&gt; &lt;a href={data.blockContentCtaBlock.field_cta_link.uri}&gt;{data.blockContentCtaBlock.field_cta_link.title}&lt;/a&gt; &lt;/div&gt; } export default CtaBlock export const CtaBlockQuery = graphql` fragment CtaBlockQuery on block_content__cta_block { field_cta_heading field_cta_link { title uri } body { value } relationships { field_cta_content_type { name } } }`</code></pre> <p>Now we have our block based on content type rendering within our content type's Gatsby template. Note that I've left out a few things that should be noted for decoupled sites. </p> <ol><li>An internal link should us the Gatsby &lt;Link /&gt; component.</li> <li>Drupal needs some love to pass the correct alias over for an internal link so that it renders correctly.</li> <li>This is a very basic example. YMMV.</li> </ol><p>Anyway, I hope that this helps someone out there who ran into the same problems that I did. Please feel free to reach out to me on Twitter @jddoesdev, on Slack where I'm usually Dorf, or just leave a comment here if you have questions, concerns, comments, or just something nice to say.</p> <p>Also, please feel free to support my efforts in speaking on mental health in tech or creating blog posts and tutorials like these by checking out my gofundme and patreon campaigns in the sidebar.</p> <p>Thanks for reading!</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/11" hreflang="en">Drupal</a></div> <div class="field__item"><a href="/taxonomy/term/21" hreflang="en">GatsbyJS</a></div> <div class="field__item"><a href="/taxonomy/term/22" hreflang="en">decoupled</a></div> <div class="field__item"><a href="/taxonomy/term/23" hreflang="en">JSON:API</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=Using%20Drupal%20Blocks%20in%20a%20Decoupled%20GatsbyJS%20Application&amp;1=https%3A//www.jamesdflynn.com/development/using-drupal-blocks-decoupled-gatsbyjs-application&amp;2=node/23" token="F4UWrvkGA1S8tEBNlmh0l57oSf3DB-xuciA0qzTm1HE"></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, 03 Nov 2019 18:40:45 +0000 jflynn 23 at https://www.jamesdflynn.com Quick Hit: Using Drupal and Symfony Components Without Bootstrapping Drupal https://www.jamesdflynn.com/development/quick-hit-using-drupal-and-symfony-components-without-bootstrapping-drupal <span class="field field--name-title field--type-string field--label-hidden">Quick Hit: Using Drupal and Symfony Components Without Bootstrapping Drupal</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">Fri, 07/26/2019 - 01:47</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><h2>Sometimes we just want to see if a thing works</h2> <p>Recently I ran into a situation while building out the <a href="https://drupal.org/project/watson_form_parser">Watson/Silverpop Webform Parser</a> where I just wanted to test and see if a few things worked without having to reload and bootstrap Drupal every time I refreshed a page. I also wanted to utilize some of the classes and methods made available from Symfony and Drupal. Can you feel my dilemma?</p> <p>Let's be honest, running <code>drush cr</code> and refreshing a page takes time, and I'm an impatient person, so I wanted to see if there was a way to use some of these things without going through the pain of waiting for a Drupal bootstrap. Turns out, the solution wasn't that difficult and it made development on many methods of my module more pleasant than it could have been.</p> <h2>Here's the scenario</h2> <p>I wanted to test a few things that didn't require the database connection. Specifically, <code>Drupal\Core\Serialization\Yaml</code> to parse an array that I was building into YAML. So, what I did was stubbed out what would become my class <code>WatsonFormEntityForm</code> in a file in my docroot that I creatively named <code>test.php</code>. Now I was able to navigate to <code>local.docksal/test.php</code> on my local machine and see things working.</p> <h2>Get to the good stuff, already!</h2> <p>I got to a point in my development where I was able to convert the submitted HTML, in this case from a call to <code>file_get_contents('./test.html');</code> into an array that I could work with. Xdebug was going great, and so was the module, but I wanted to see if I could convert it into YAML using a native Drupal method. The solution came with one single line of code.</p> <pre> <code>$autoloader = require_once 'autoload.php'; </code></pre> <p>This tells PHP, "Hey, we got a file here that wants to use some of the methods and classes in the Autoloader. Let's go ahead and let it!" This variable doesn't need to be called anywhere in the file. It just needs to exist. Now I was able to update the file with a few friendly <code>use</code> statements from within the Drupal and Symfony ecosystem without having to wait for all the database connections to happen.</p> <p>The end result was:</p> <pre> <code>&lt;?php use Drupal\Core\Serialization\Yaml; $autoloader = require_once 'autoload.php'; // Do all the things here, including: $yaml = Yaml::encode($array); var_dump($yaml); </code></pre> <p>It sped up development, and it made it so I didn't have to wonder if something wasn't working because I forgot to <code>drush cr</code> all the things or if it was just because I made some mistakes.</p> <h2>Gotchas</h2> <p>Be sure that any code you're running doesn't rely on database calls or the container. For instance, if you try to run <code>$nodes = \Drupal::entityTypeManager()-&gt;getStorage('node')-&gt;loadMultiple();</code> is going to throw a painful error.</p> <p>Also, this is mainly for rapid prototyping or proving concepts. I don't recommend writing an entire module in procedural code and then trying to refactor later. Maybe take it one function at a time just to make sure it's doing what you want it to do.</p> <p>Let me know if this helped you out or if you have better suggestions for rapidly testing some Drupal stuff without having to rely on a full bootstrap. As always, feel free to reach out to me on the Drupal Slack, where I'm always Dorf and always willing to help if I can.</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> </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=Quick%20Hit%3A%20Using%20Drupal%20and%20Symfony%20Components%20Without%20Bootstrapping%20Drupal&amp;1=https%3A//www.jamesdflynn.com/development/quick-hit-using-drupal-and-symfony-components-without-bootstrapping-drupal&amp;2=node/22" token="usx_tADiTKNlt8qhwWYf50hpv8zCQyfEqo0cJMXR5ys"></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> Fri, 26 Jul 2019 06:47:48 +0000 jflynn 22 at https://www.jamesdflynn.com An Open Letter to the Drupal Community https://www.jamesdflynn.com/development/open-letter-drupal-community <span class="field field--name-title field--type-string field--label-hidden">An Open Letter to the Drupal Community</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">Tue, 07/23/2019 - 11:00</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><h2>Let's Start a Conversation</h2> <p>Dear friends,</p> <p>Over the past few weeks there have been some conversations among many of us regarding the issue credit system and how it could be used to "game the system."  This started with a conversation in a Slack team asking if a d.o user was a bot because of the number of issue credits attached to their profile.  At the time it was around 550 in the past year.  There was assurance from someone who knew this person that the person in question was not a bot, however a cursory look at the issues they had been credited for revealed that the majority of these issues were issues that could be considered Novice level and were very similar to each other.</p> <p>I didn't think much of it until I pushed a custom module into the contrib space, and within 30-60 minutes I received an email notice from d.o saying an issue had been opened on my module.  My brand new, still in alpha, less than an hour old module.  I read the issue and two things stuck out:</p> <ol><li>The name of the person creating the issue.</li> <li>The verbiage used in this issue.</li> </ol><p>These stuck out because it was the exact same person who had been brought up in Slack and the words were an exact copypasta of issues I had looked at when trying to figure out how this person had earned so many credits without being a bot.  I did a little more looking into it and noticed that the issue description was exactly the same as other issues opened by this person, and so was the comment.  I copied the text of the comment and pasted it into Google and found that there were 8 pages of results with this exact same text, almost all of them were opened by this person.  This did not help claims of being a definite human not-bot.</p> <h2>What's Wrong With This Picture?</h2> <p>Now why is this an issue, you may be asking.  Why do I care and why am I making a fuss about it?</p> <p>Let me preface this by saying that I don't care about the number of issue credits by my name.  My skills (or lack thereof) speak for themselves, as does my reputation.  I'm happy in my current position, I have a reasonable grasp on what I do, and even though I don't have a ton of credits next to my name, I consider myself a member of the community and an asset to the Drupal project.</p> <p>As for making a fuss about it, there are three main reasons that I'm bothered by this situation.</p> <p><b>Reason 1.</b></p> <p>We've all seen the CMS Learning Curve image, right?</p> <div alt="CMS learning curve meme" data-embed-button="media" data-entity-embed-display="media_image" data-entity-embed-display-settings="large" data-entity-type="media" data-entity-uuid="846c22ff-2a99-4bfb-a6b0-84572813f950" data-langcode="en" title="Learning Curve" class="embedded-entity"> <img src="/sites/default/files/styles/large/public/cms-learning-curve_0.png?itok=whbLHfR0" alt="CMS learning curve meme" title="Learning Curve" typeof="foaf:Image" class="image-style-large" /></div> <p>If not, here it is.</p> <p>This has always been a joke within the community.  Drupal has a high barrier to entry from a developer perspective.  Drupal 8, even moreso than Drupal 7 which (IIRC) is the version that earned Drupal's place on this infamous image.</p> <p>Because of this barrier, we've tried to make it possible for new contributors to get comfortable with contributing through issues that are tagged "Novice".  These issues are often documentation, coding standards, or basic code fixes that can be handled by someone who may not have as much experience contributing to the project, whether it be the core Drupal project or contributed modules.</p> <p>These novice issues are important to have.  Sure, they may take some of the core maintainers or more advanced developers a few minutes to handle, but if they're not critical then there's no reason to not utilize them to help bring in new talent and give novice contributors a chance to get into the weeds a bit.</p> <p>Looking through this person's issue credits, every single issue I found has been a novice issue.  I did not take the time to look through all 570 credits to verify this, but my sample size was large and based on that sample, I'm willing to wager that the vast majority of these issues would have made for decent entry points for new contributors.   With one person collecting all of the proverbial "low-hanging fruit" to pad their profile, this effectively gates new contributors from having the opportunity to help the project and removes upwards of 570 novice issues that would make perfect entry points for novices, not counting issues that have not been credited yet.</p> <p><b>Reason 2.</b></p> <p>In speaking with some other developers, it's demoralizing.  This person was recently mentioned in a tweet as "the most impressive <a href="http://Drupal.org">http://Drupal.org</a> profile page I've ever seen".  I'm not trying to say that these issues aren't important, and if they improve the project overall, then it's a good thing that they're being handled, but the process and the "reward" system makes it seem like the problems are larger than they really are.</p> <p>Imagine this scenario, please:  Developer A has taken the challenge to move from novice issues to something a little more advanced and spends twenty four working hours debugging an issue in core that is preventing an entity from saving under certain circumstances.  They find the problem, discuss it with the core maintainers on Slack, toss ideas back and forth on the issue node, create a patch, create tests, submit the patch, wait for RTBC, and the patch gets committed.  The project is made better because Developer A is passionate about Drupal, they spent their free time trying to improve Drupal, and when the patch is committed, they get credit for the issue.  This credit is worn as a badge of honor on their profile.  They feel pride in what they've done and what they've given back.</p> <p>Developer A is scrolling through Twitter and sees mention of Developer B as "the most impressive <a href="http://Drupal.org">http://Drupal.org</a> profile page I've ever seen" and wants to know what their secret is.  Developer A starts looking at the issues, and finds that the "impressive" profile is primarily novice issues that can be handled in a matter of minutes, yet they're the ones marked as "impressive".  Developer A feels like their credit is devalued because the only thing seen on the profile is the number, not the number of issues by difficulty or number of lines in a patch, but number of issue credits.</p> <p>Developer A decides that contributing back isn't worth their time because the only thing that seems to matter is the number of credits.  Drupal loses a talented contributor, and the project is poorer for it.</p> <p><b>Reason 3.</b></p> <p>The Drupal Marketplace page puts weight on the number of contributions over the last 90 days in its ranking system.  Outwardly, this would seem to reward companies who invest in improving the Drupal project, and it does.  However, it also incentivizes companies who hire new developers who are just thrilled to have a job into positions that exist only for the sake of getting their company to the top of the marketplace.  This is unfair to other companies who may not have the budget, but still contribute how they can, and it's unfair to the employee who is stuck searching for module releases, looking for novice issues, and copying and pasting descriptions and comments to get more and more credits.</p> <p>In a sense, it's gating the employee from being able to move on from this position because even though they have 570+ credits, they've potentially never done anything past that.  I'm not claiming that the person who sparked this conversation doesn't have skills, but churning out that many bug reports and quickfix patches doesn't really empower that person to show what they're really capable of.  I know if I spent all day doing the same thing over and over, I definitely would not want to even look at a text editor on my free time, let alone feel that it would be worth it to try and expand my skill set.</p> <h2>Possible Solutions</h2> <p>There is no one-size-fits-all solution for this.  Hell, there's probably no good solution at all.  However, here are some ideas that I would like to throw out there that are just that, ideas. They're not fully fleshed out, and they aren't ultimatums of "Do this or I'm going to walk!" They're potential starting points and hopefully help to spark a larger conversation.</p> <p><b>Solution 1: Weighted Credits</b></p> <p>Not every credit should be worth the same as others. There could be a number of ways to do this, but in my opinion the easiest way would be to weigh by difficulty of the task. A novice, non-critical issue could be weighted less than an advanced, critical issue.</p> <p>Pros:</p> <ul><li>Could incentivize contributors to move up in difficulty of tasks.</li> <li>Gives contributors who spend a lot of time on difficult issues more credit than contributors who seek out the easiest fixes.</li> <li>Rewards growth as a developer.</li> </ul><p>Cons:</p> <ul><li>Difficult to implement.</li> <li>Creating a weighting system can be difficult and fairness cannot be guaranteed.</li> <li>Valuation of an issue is subjective and could be gamed if a module maintainer decides to start tagging every issue as "Critical" or similar.</li> </ul><p><b>Solution 2: Mandatory Difficulty Tagging</b></p> <p>The word "Difficulty" is difficult here. Some other possibilities would be "severity", "level of effort", "need", or any number of words that could be placed there. The point is that for this solution, I recommend creating a specific taxonomy that relates to difficulty or [insert word here] of the issue. This would be mandatory before marking an issue as fixed by the maintainer, whether it be a contrib module maintainer or a core committer. Then, on a user's profile credits could be divided by rating. For example: User Credited on 50 "Novice" issues, 3 "Intermediate" issues, 15 "Advanced" issues, and 3 "Critical" issues.</p> <p>Pros:</p> <ul><li>Makes it easier to discern how much effort is actually being put into contributions.</li> <li>More transparent for potential employers looking at d.o for references.</li> <li>Creates a weight system without actually having to weight contributions.</li> </ul><p>Cons:</p> <ul><li>Puts the responsibility on module/core maintainers to assign difficulty, which can be very subjective.</li> <li>Does not prevent a module maintainer from marking their own issue as "Critical" when it could easily be tagged "Novice"</li> <li>Potentially devalues novice credits and gates new contributors.</li> </ul><p><b>Solution 3: Credit limits</b></p> <p>I'm of the opinion that if someone has over 100 novice credits, they should probably be at a point where they are either taking on more serious issues and leaving the novice issues as entry points for new contributors. This solution would still require tagging of issues by maintainers, but instead of creating tiers based on difficulty it would limit the number of "novice" issues a contributor would receive credit points for. The number would have to be decided and I don't know if it should be 10 or 100. I'm just spitballing here.</p> <p>Pros:</p> <ul><li>Incentivizes contributors to move on from novice issues.</li> <li>Keeps novice issues for actual novices.</li> <li>Creates more entry points for new contributors.</li> </ul><p>Cons:</p> <ul><li>May deter non-novice contributors from taking on novice issues that need to be done.</li> <li>Again, module maintainers may tag issues inappropriately to ensure credit.</li> <li>The term "novice" is very subjective and could easily be debated.</li> </ul><p><b>Solution 4: Redistribution of Credits</b></p> <p>Sorry, my politics are showing on this one, but I don't mean take credits from the top 1% and give them to the bottom 25%. Instead, give others a chance to take on the tasks and open the gates to new contributors. The issues that are being created, fixed, and credited are issues, but they're mostly novice issues. My Google-fu returned over 20 pages with identical descriptions. I don't know exactly how many because I just stopped clicking "Next Page" when I hit about page 15, but it's safe to say that this person knows how to fix this issue.</p> <p>Great. Good job. Now give someone else a chance!</p> <p>If the goal of these issues is truly to move the project forward and not just farm credits like a World of Warcraft sweatshop circa 2008 CE, then let's get some new people contributing. Create the issue, tag it as novice, and let it be. Find an upcoming camp and tag it with that camp so that if there is a space for contribution and/or a beginning contributor workshop, there are plenty of issues to work on to help get the new contributors comfortable with the steps needed for code contribution. If it's critical, go ahead and fix it. If it's non-breaking and a good chance for someone to learn some git-fu, how to branch and create patches, how to comment in the issue queue, how to push up a patch in a comment, and the lifecycle of an issue from creation to RTBC to fixed, then let it be for a bit.</p> <p>If there's no movement after a few days or after the camp it was tagged for, then go ahead and fix it. Sure, maybe the module maintainer will fix it in the meantime. Maybe it'll get marked as "Closed: Outdated" because it's already been taken care of on a dev branch that just hasn't made it up to d.o yet. Maybe, just maybe, a new contributor in the wild will be searching the issue queue for novice issues to get started and they'll take the reins on the issue and get their first credit. You may not get credit for authoring the issue, but you'll definitely get a warm fuzzy feeling for helping foster a new contributor into the world of Drupal. That's truly what building community is about.</p> <p>Pros:</p> <ul><li>Gates are open for new contribution.</li> <li>Knowledge is shared and the community grows stronger.</li> <li>You can almost hear the <i>DING</i> when they level up into the world of open source contribution.</li> </ul><p>Cons:</p> <ul><li>Won't necessarily get credit for every issue created, but we shouldn't be here just for credits.</li> <li>Issues may become stale and overlooked.</li> <li>That's all I can think of...</li> </ul><h2>Closing Thoughts</h2> <p>This post is not meant to attack any person or any company. If anything, I hope that it sparks some conversations and helps make the Drupal project richer and more diverse by helping spread the wealth of entry-level issues and credits to people who may or may not have opportunities otherwise. Since I began writing this post I've tried keeping an eye on things in the issue queues and I found that some people are taking the example from the account that got me thinking about this and running with it. I've even seen some people trying to patch core testing modules with the <code data-backticks="1">.info.yml Dependency Namespacing</code> issue. Fortunately these were caught pretty early and core maintainers made it a point to redirect the issue reporter to fixing more than just one file in core in a patch.</p> <p>The credit system is great and it rewards hard work, but the issue credit system has issues with credits and needs some work as well. I have brought this up to both the CWG and the DA and they're understandably in tough spots. Yes, fixing issues definitely makes the project better regardless of how small the issue is, but there are thousands upon thousands of developers out there just looking for a project to grab onto and start working with. We need to make it as easy as possible for these people to jump in and become part of the Drupal community. I travel for a lot of camps and, unfortunately, for the most part I see a lot of the same faces in the contrib rooms and areas. I would like to see some new people feel welcomed the same way I felt welcomed at my first contribution days when I was able to get my first patch committed to core. Sure, those maintainers could have taken care of the issue and taken credit for themselves, but instead they created the issue and passed it to me, an eager entry-level developer still trying to find my place in this crazy Drupal world.</p> <p>We shouldn't have to rely on the CWG or DA to police these things. We should be able to police ourselves. We should be able to know if what we're doing is self-serving or if it for the betterment of the Drupal project and the Drupal community. We should be able to feel good about the credits we get and the credits we help others get. We should try our best to be good people and do the right thing whenever we can.</p> <p>We shouldn't reward companies or individuals who hoard issue credits just to look better on the marketplace or in a wordcloud on a powerpoint. We can do better than that.</p> <p>If you're a new developer or an aspiring contributor looking for something to do to contribute back to the project, contact me. I'll help you find an issue to work on and run you through the process of patching, commenting, testing, and whatever else it is that you need to succeed. I want to stress that you don't need to know how to code to contribute back to Drupal. Ask me how!</p> <p>If you're a seasoned developer or contributor, I challenge you to mentor someone who's eager to learn. Give up a credit or two to someone who is looking for that first <i>DING</i>. Take the time and we'll all be better for it.</p> <p>Sincerely,</p> <p>JD Flynn, Drupal Developer and Community Member</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/11" hreflang="en">Drupal</a></div> <div class="field__item"><a href="/taxonomy/term/20" hreflang="en">Community</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=An%20Open%20Letter%20to%20the%20Drupal%20Community&amp;1=https%3A//www.jamesdflynn.com/development/open-letter-drupal-community&amp;2=node/21" token="t7Ne3ruHgSk2Db9AcBWNjQdiqWmsisLOlQio-D1JLFA"></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/Gold-Coin-680x380.jpg" width="680" height="380" alt="Gold coins" loading="lazy" typeof="foaf:Image" /> </div> </article> </div> Tue, 23 Jul 2019 16:00:00 +0000 jflynn 21 at https://www.jamesdflynn.com Damn you, DEV-MASTER! https://www.jamesdflynn.com/development/damn-you-dev-master <span class="field field--name-title field--type-string field--label-hidden">Damn you, DEV-MASTER!</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, 05/27/2019 - 16:12</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"></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> </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=Damn%20you%2C%20DEV-MASTER%21&amp;1=https%3A//www.jamesdflynn.com/development/damn-you-dev-master&amp;2=node/17" token="KOIDLwko4YOanoAR3-eK3EjMEHlB2oH-tpQE_B8L0w4"></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, 27 May 2019 21:12:51 +0000 jflynn 17 at https://www.jamesdflynn.com Drupal-Check, Site Factory, and Acquia BLT, OH MY! https://www.jamesdflynn.com/development/drupal-check-site-factory-and-acquia-blt-oh-my <span class="field field--name-title field--type-string field--label-hidden">Drupal-Check, Site Factory, and Acquia BLT, OH MY!</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">Wed, 05/08/2019 - 09:02</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>I was fortunate enough to attend DrupalCon Seattle this year, as well as give a presentation on mental health in tech, but one of the key topics of DrupalCon was Drupal 9 readiness. Dries mentioned it several times in the Driesnote and we even had some contribution efforts specific to Drupal 9 readiness at MidCamp 2019.</p> <p>One thing that stuck out to me as particularly awesome was my friend and fellow MidCamp organizer <a href="https://glamanate.com/">Matt Glaman's</a> <a href="https://github.com/mglaman/drupal-check">drupal-check tool</a> that will check your modules for deprecated code that is <em>NOT</em> ready for Drupal 9 being mentioned in the Driesnote. Additionally just as awesome was another good friend, <a href="https://www.mcdwayne.com/">Dwayne McDaniel</a> getting a shoutout for going through EVERY. SINGLE. CONTRIB. MODULE. and running this tool against it. That's a lot of modules!</p> <p>In case you didn't know, the upgrade path from Drupal 8 to Drupal 9 is supposed to be extremely easy. As someone who had to migrate from 7 to 8, this is welcome news. The TL;DR version is: Once there are backwards compatibility breaks, we're at a new version. What does this mean? It means that code that is marked as deprecated in 8 will <em>NOT</em> be in 9 and you can't upgrade from 8 to 9 if you're using this deprecated code. (I'm looking at you <code>array()</code>)</p> <h2>The Problem(s)</h2> <p>Drupal Check is AMAZING! That's not a problem. The first problem is making sure that your code is ready to be checked by Drupal Check.</p> <h3>Issue 1 (Acquia Cloud Site Factory/Multisite)</h3> <p>We use Acquia Cloud Site Factory extensively at Genuine. This leads to many multi-site installs. Why is this an issue? Well, it's not unusual to have multiple profiles available on Site Factory. Often times, these profiles will be based off another one, but different in theme, look and feel, or functionality. This means that there is a chance that there will be some duplicated functions in the <code>.theme</code> file. Specifically, helper functions that may not fall into the <code>_THEMENAME_function_name()</code> convention and may be missed when you change everything around.</p> <pre> <code>Fatal error: Cannot redeclare _my_poorly_named_function() (previously declared in /var/www/docroot/profiles/custom/profile_1/themes/profile_1_theme/profile_1_theme.theme:150) in /var/www/docroot/profiles/custom/profile_2/themes/profile_2_theme/profile_2_theme.theme on line 168 </code></pre> <p>It happens. Stop looking at me like that.</p> <h3>Solution 1</h3> <p>Well, this is a pretty easy fix, again compliments of Matt Glaman and Will Long AKA Kerasai. You can either prefix the functions OR use namespaces!</p> <pre> <code>&lt;?php namespace my_profile_name; </code></pre> <p>Your <code>.profile</code> files are autoloaded so the namespace will be valid. This prevents the error above.</p> <h3>Issue 2 (Acquia BLT)</h3> <p>This one is a doozy. Acquia BLT or Build and Launch Tool (as of 9.2) has a little command in it that checks for deprecated code, but the dependencies it uses are outdated and cause Drupal Check to fail before it gets anywhere.</p> <p><code>blt tests:php:sniff:deprecated</code><br /><br /> This command depends on the package <code>sensiolabs-de/deprecation-checker</code> which has a dependency on <code>nikic/php-parser:~3.0</code>. Drupal Check requires <code>nikic/php-parser:^4.0</code> due to the way it's built. Trying to run <code>composer require nikic/php-parser:^4.0</code> will make composer yell at you. A lot.</p> <h3>Solution 2</h3> <p>This one gets a bit tricky. There are a few things that need to happen here if you're wanting to reap the benefits of Drupal Check, and trust me, you do.</p> <p>In your base <code>composer.json</code> file there should be a couple of lines that look like this</p> <pre> <code> "merge-plugin": { "require": [ "blt/composer.required.json" ], "include": [ "blt/composer.overrides.json" ], </code></pre> <p>The <code>composer.required.json</code> file is in your <code>blt/</code> folder and is autogenerated by BLT so it's not safe to change. The workaround for this is to copy the file to <code>composer.required-modified.json</code> and update the line below <code>"require": [</code> to <code>blt/composer.required-modified.json</code>. The end result should look like this:</p> <pre> <code>"merge-plugin": { "require": [ "blt/composer.required-modified.json" ], "include": [ "blt/composer.overrides.json" ], </code></pre> <p>Next, you will need to run the command <code>composer remove sensiolabs-de/deprecation-detector</code> This <em>should</em> remove the old package and update <code>nikic/php-parser</code> but if it doesn't, check in your new <code>composer.required-modified.json</code> file, make sure that Deprecation Detector is gone, and add in <code>"nikic/php-parser": "^4.0"</code> at the end of the <code>"require-dev"</code> section. <strong>DON'T FORGET YOUR COMMA!</strong></p> <p>Now you should be able to run Drupal Check without issues, but there's still one thing we need to take care of before saying we're done. That little BLT command from above <code>blt tests:php:sniff:deprecated</code>. If we try running it without the <code>deprecation-detector</code> package then we're going to have a bad time.</p> <p>My workaround for this was to create a replacement command for BLT. This is done by using replace command annotation in a custom class. The BLT docs are <a href="https://blt.readthedocs.io/en/latest/extending-blt/#adding-a-custom-robo-command">here</a> but they are lacking and caused me some headaches, so I've got some docs for you right here.</p> <p>My custom command file lives in <code>blt/src/Hooks/NoDeprecatedCommandHook.php</code>. The name of the file and class needs two things:</p> <ol><li>It can't be the same as the class you're replacing.</li> <li>It needs the word Hook at the end.</li> </ol><p><code>DeprecatedCommandHook.php</code> will not work. <code>NoDeprecatedCommand.php</code> will not work. <code>MyReplacementCommandHook.php</code> will definitely work, so long as your class shares the same name, but let's stick with my own file here.</p> <pre> <code>&lt;?php namespace Acquia\Blt\Custom\Hooks; use Acquia\Blt\Robo\BltTasks; /** * Defines commands in the "tests:php:sniff:deprecated*" namespace. */ class NoDeprecatedCommandHook extends BltTasks { /** * Detects usage of deprecated custom code. * * @hook replace-command tests:php:sniff:deprecated * * @aliases tpsd deprecated */ public function detect() { $this-&gt;say("This command has itself been deprecated. Please use drupal-check for all of your deprecated code needs."); return 0; } } </code></pre> <p>Let's walk through the file a bit</p> <pre> <code>namespace Acquia\Blt\Custom\Hooks; </code></pre> <p>Without this line, it won't work It needs to be in this namespace or you're going to have a bad time.</p> <pre> <code>use Acquia\Blt\Robo\BltTasks; </code></pre> <p>This is optional, but I used it to get the <code>say()</code> method available.</p> <pre> <code>class NoDeprecatedCommandHook extends BltTasks { </code></pre> <p>Our class name. If you're using BltTasks, don't forget to extend it.</p> <pre> <code> /** * Detects usage of deprecated custom code. * * @hook replace-command tests:php:sniff:deprecated * * @aliases tpsd deprecated */ </code></pre> <p>This is where the magic happens. <code>@hook replace-command</code> tells the system to forget about the original command, this one is driving now.</p> <pre> <code> public function detect() { $this-&gt;say("This command has itself been deprecated. Please use drupal-check for all of your deprecated code needs."); return 0; } </code></pre> <p>This is our function that does nothing more than tell us to stop using this function. Clever, right?</p> <p>The end result is a functioning drupal-check and a function that tells its own deprecation.</p> <pre> <code>➜ project git:(master) ✗ blt tests:php:sniff:deprecated This command has itself been deprecated. Please use drupal-check for all of your deprecated code needs. </code></pre> <h2>Conclusion</h2> <p>This is a pretty specific use case and hopefully Acquia BLT will be pushing out an update in the near future that doesn't require us to have to workaround much longer, but this works for us.</p> <p>Please test the hell out of your project after making these updates. BLT should really only be used for local dev and deployment artifacts, but it's still worth noting what does and doesn't happen that might bork up your site.</p> <p><strong><em>Edit:</em></strong> BLT is going to be getting rid of the DeprecatedCommand function that is replaced in this post in the very near future from 5/8/19. That will help all of you out, but it also means that I spent way too much time typing this thing out. See: <a href="https://github.com/acquia/blt/pull/3621">https://github.com/acquia/blt/pull/3621</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/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/18" hreflang="en">drupal-check</a></div> <div class="field__item"><a href="/taxonomy/term/3" hreflang="en">php</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=Drupal-Check%2C%20Site%20Factory%2C%20and%20Acquia%20BLT%2C%20%20OH%20MY%21&amp;1=https%3A//www.jamesdflynn.com/development/drupal-check-site-factory-and-acquia-blt-oh-my&amp;2=node/15" token="iQHNvAqNw-5A29w-Ow7GChXn8z89_LsKq2zi6vWv1Wk"></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/drupal-check.jpg" width="1212" height="442" alt="Screen grab of github" loading="lazy" typeof="foaf:Image" /> </div> </article> </div> Wed, 08 May 2019 14:02:56 +0000 jflynn 15 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