<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
   <channel>
      <title>Latest blog posts</title>
      <link>http://www.smgaweb.com/blog/rss/</link>
      <description />
      <atom:link href="http://www.smgaweb.com/blog/rss/" rel="self" />
      <language>en-us</language>
      <lastBuildDate>Sat, 25 Apr 2020 19:28:00 +0000</lastBuildDate>
      
      <item>
         <title>Dynamically Importing Vuex Store Modules From Directory Structure</title>
         <link>http://www.smgaweb.com/blog/dynamically-importing-vuex-store-modules-directory-structure</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Sat, 25 Apr 2020 19:28:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/dynamically-importing-vuex-store-modules-directory-structure</guid>
         <description>How to dynamically import Vuex store modules into the base store from throughout a Vue application directory structure.</description>
         <content:encoded>
        <![CDATA[<p>This is a continuation of my <a href="https://www.smgaweb.com/blog/dynamically-generating-vue-router-routes-from-directory-structure" target="_blank" rel="follow noopener">previous post</a>, where I went through the details of how to define Vue router route definitions in files throughout the app directory structure, and then import them into the main router index.js file. In this post, I'm going to be showing the same process for Vuex store modules. There are a couple small variations from the router import code, but nothing major.</p>
<p>First, here is the structure of a store module:</p>
<pre>import * as types from '@/store/types';<br /><br />const state = {<br />  events: [],<br />};<br /><br />// getters<br />const getters = {<br />  getEvents: state =&gt; state.events,<br />};<br /><br />// actions<br />const actions = {<br /><br />};<br /><br />// mutations<br />const mutations = {<br />  [types.SET_EVENTS](state, shifts) {<br />    state.events = shifts;<br />  }<br />};<br /><br />export default {<br />  namespaced: true,<br />  name: 'event',<br />  state,<br />  getters,<br />  actions, <br />  mutations<br />}</pre>
<p>There is one small addition to this file for import purposes, and that is the <code>name</code> key in the exported object. This is because during the import process, since stores are separated by namespaces, a name property is needed. However, in looking through all of the available information returned from <code>require.context()</code>, I didn't see the value of the parent directory name, so I just added that value to the exported object.</p>
<p>With those defined, here is the store <code>index.js</code> file:</p>
<pre><span>import </span>Vue <span>from </span><span>'vue'</span><span>;<br /></span><span>import </span><span>Vuex </span><span>from </span><span>'vuex'</span><span>;<br /></span><span>import </span>Auth <span>from </span><span>'@/store/modules/auth'<br /></span><span><br /></span>Vue<span>.</span><span>use</span><span>(</span><span>Vuex</span><span>);<br /></span><span><br /></span><span>const </span><span>debug </span><span>= </span><span>process</span><span>.</span><span>env</span><span>.</span><span>NODE_ENV </span><span>!== </span><span>'production'</span><span>;<br /></span><span><br /></span>Vue<span>.</span><span>config</span><span>.</span><span>devtools </span><span>= </span><span>true</span><span>;<br /></span><span><br /></span><span>// Import all of the resource store files.<br /></span><span>function </span><span>loadStores</span><span>() {<br /></span><span>  </span><span>const </span><span>context </span><span>= </span><span>require</span><span>.</span>context<span>(</span><span>'@/resources'</span><span>, </span><span>true</span><span>, </span><span>/store.js$/i</span><span>);<br /></span><span>  </span><span>return </span><span>context</span><span>.</span><span>keys</span><span>()<br /></span><span>    .</span><span>map</span><span>(</span><span>context</span><span>)         </span><span>// import module<br /></span><span>    </span><span>.</span><span>map</span><span>(</span><span>m </span><span>=&gt; </span><span>m</span><span>.</span><span>default</span><span>)  </span><span>// get `default` export from each resolved module<br /></span><span>}<br /></span><span><br /></span><span>const </span><span>resourceModules </span><span>= </span><span>{};<br /></span><span>loadStores</span><span>().</span><span>forEach</span><span>((</span><span>resource</span><span>) </span><span>=&gt; </span><span>{<br /></span><span>  </span><span>resourceModules</span><span>[</span><span>resource</span><span>.</span><span>name</span><span>] </span><span>= </span><span>resource</span><span>;<br /></span><span>});<br /></span><span><br /></span><span>resourceModules</span><span>.</span><span>auth </span><span>= </span>Auth<span>;<br /></span><span><br /></span><span>export default new </span><span>Vuex</span><span>.</span><span>Store</span><span>({<br /></span><span>  </span><span>modules</span><span>: </span><span>resourceModules</span><span>,<br /></span><span>  </span><span>strict</span><span>: </span><span>debug</span><span>,<br /></span><span>});</span></pre>
<p>You can see where I use the exported name property in the call to <code>loadStores()</code>. Then, once all of the store modules under the <code>/resources</code> directory are adde to the&nbsp;<code>resourceModules</code> object, I manually add the store for the <code>Auth</code> module (since it's outside of the <code>/resources</code> directory), and then pass it all to the modules key in the new <code>Vuex.Store()</code> constructor. After verifying in the Vue Dev tools that the store exist, I'm off and running.&nbsp;</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Dynamically Generating Vue Router Routes From Directory Structure</title>
         <link>http://www.smgaweb.com/blog/dynamically-generating-vue-router-routes-from-directory-structure</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Sat, 18 Apr 2020 06:10:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/dynamically-generating-vue-router-routes-from-directory-structure</guid>
         <description>How do build a Vue router definition by importing router configurations from throughout a Vue application directory structure.</description>
         <content:encoded>
        <![CDATA[<p>I'm working on a Vue starter kit for a Javascript platform that I've used for a few years, and I have the need - well, the desire to make it as easy for the developer as possible - to automatically import route definitions from multiple locations and add them to the Router() definition, instead of loading them all from the router index file.</p>
<p>In applications such as Vue apps that have multiple types of files that are used together to create the app, there are primarily two ways to organize the files. For instance, in Vue, let's say that we have multiple resources, and each resource has three components:</p>
<ol>
<li>The component (.vue file)</li>
<li>The router definition file</li>
<li>The Vuex module file</li>
</ol>
<p>You could choose to organize your files in two different ways:</p>
<ol>
<li>By feature - each directory has a .vue file, a router file, and a Vuex file</li>
<li>By functionality - one directory each for components, one for router files, and one for Vuex files.</li>
</ol>
<p>In my case, I'm going to group by feature, which I will refer to as a resource (to match the platform naming convention). Using the example resources, of user, event, and job, this will be my directory structure:</p>
<blockquote>
<pre class="lang-js prettyprint prettyprinted" style="margin: 0px 0px 1em; padding: 12px 8px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; line-height: inherit; font-family: Consolas, Menlo, Monaco, 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace, sans-serif; font-size: 13px; vertical-align: baseline; box-sizing: inherit; width: auto; max-height: 600px; overflow: auto; background-color: var(--black-050); border-radius: 3px; display: block; color: #242729; overflow-wrap: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><code style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: Consolas, Menlo, Monaco, 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace, sans-serif; font-size: 13px; vertical-align: baseline; box-sizing: inherit; background-color: transparent; white-space: inherit;"><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">/</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">src
  </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">|</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">
  </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---/</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">resources
         </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">|</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">
         </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---/</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">user
              </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">|</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">
               </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---</span><span class="typ" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: #2b91af;">User</span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">.</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">vue
               </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">routes</span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">.</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">js
               </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">store</span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">.</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">js
            /event
              </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">|</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">
               </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---</span><span class="typ" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: #2b91af;">Event</span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">.</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">vue
               </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">routes</span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">.</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">js
               </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">store</span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">.</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">js
            /job
              </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">|</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">
               </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---</span><span class="typ" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: #2b91af;">Job</span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">.</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">vue
               </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">routes</span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">.</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">js
               </span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">---</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">store</span><span class="pun" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">.</span><span class="pln" style="margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-size: 13px; vertical-align: baseline; box-sizing: inherit; color: var(--black-750);">js<br /><br /></span></code></pre>
</blockquote>
<p>The contents of a <code>routes.js</code> file will look like this:</p>
<pre>import Event from '@/resources/event/Event'<br /><br />export default [<br />  {<br />    path: '/events',<br />    name: 'event',<br />    component: Event<br />  },<br />];</pre>
<p>Including an additional <code>Auth.vue</code> component (which is outside the <code>/resources</code> directory), our base router index file will look like this:</p>
<pre>import Vue from 'vue'<br />import Router from 'vue-router'<br />import Home from '@/components/Home'<br />import Auth from '@/components/Auth';<br /><br />Vue.use(Router);<br /><br />let routes = [<br />&nbsp; {<br />&nbsp;&nbsp;&nbsp; path: '/',<br />&nbsp;&nbsp;&nbsp; name: 'home',<br />&nbsp;&nbsp;&nbsp; component: Home<br />&nbsp; },<br />&nbsp; {<br />&nbsp;&nbsp;&nbsp; path: '/login',<br />&nbsp;&nbsp;&nbsp; name: 'auth',<br />&nbsp;&nbsp; component: Auth<br />&nbsp; },<br />];<br /><br />export default new Router({<br />&nbsp; mode: 'history',<br />&nbsp; routes,<br />})</pre>
<p>If I were to manually add the router objects for my three resources, I could do it like this:</p>
<pre>import Vue from 'vue'<br />import Router from 'vue-router'<br />import Home from '@/components/Home'<br />import Auth from '@/components/Auth';<br />import Event from '@/resources/event/Event';<br />import Job from '@/resources/job/Job';<br />import User from '@/resources/user/User'; <br /><br />Vue.use(Router);<br /><br />let routes = [<br />&nbsp; {<br />&nbsp;&nbsp;&nbsp; path: '/',<br />&nbsp;&nbsp;&nbsp; name: 'home',<br />&nbsp;&nbsp;&nbsp; component: Home<br />&nbsp; },<br />&nbsp; {<br />&nbsp;&nbsp;&nbsp; path: '/login',<br />&nbsp;&nbsp;&nbsp; name: 'auth',<br />&nbsp;&nbsp;  component: Auth<br />&nbsp; },<br />  {<br />&nbsp;&nbsp;&nbsp; path: '/events,<br />&nbsp;&nbsp;&nbsp; name: 'event',<br />&nbsp;&nbsp;  component: Event<br />&nbsp; },<br />  {<br />&nbsp;&nbsp;&nbsp; path: '/Job',<br />&nbsp;&nbsp;&nbsp; name: 'job',<br /> &nbsp;&nbsp; component: Job<br />&nbsp; },<br />  {<br />&nbsp;&nbsp;&nbsp; path: '/user',<br />&nbsp;&nbsp;&nbsp; name: 'user',<br />&nbsp;&nbsp;  component: User<br />&nbsp; },<br />];<br /><br />export default new Router({<br />&nbsp; mode: 'history',<br />&nbsp; routes,<br />})</pre>
<p>However, I want to avoid having to manually update this file every time I add a resource, so instead I do it dynamically:</p>
<pre>import Vue from 'vue'<br />import Router from 'vue-router'<br />import Home from '@/components/Home'<br />import Auth from '@/components/Auth';<br /><br />Vue.use(Router);<br /><br />let baseRoutes = [<br />  {<br />    path: '/',<br />    name: 'home',<br />    component: Home<br />  },<br />  {<br />    path: '/login',<br />    name: 'auth',<br />    component: Auth<br />  },<br />];<br /><br />// Import all of the resource routes files.<br />function loadRoutes() {<br />  const context = require.context('@/resources', true, /routes.js$/i)<br />  return context.keys()<br />    .map(context) // import module<br />    .map(m =&gt; m.default) // get `default` export from each resolved module<br />}<br /><br />const resourceRoutes = loadRoutes();<br />resourceRoutes.forEach((route) =&gt; {<br />  routes.push(route[0]);<br />});<br /><br />export default new Router({<br />  mode: 'history',<br />  routes,<br />})</pre>
<p>The magic happens in the <code>loadRoutes()</code> function. As described to me in the <a href="https://stackoverflow.com/a/61262665/391054" target="_blank" rel="nofollow noopener">answer to my Stackoverflow post,</a> <code>require.context()</code> returns a <code>context</code> module, which exports a <code>keys</code> function that returns an array of all possible requests (i.e., the matching paths), each of which could be passed to the context itself (which invokes its <code>resolve</code> function) to import the corresponding module. The returned array from loadRoutes() function looks like this:</p>
<pre>resourceRoutes = [<br />&nbsp; {<br />&nbsp;&nbsp;&nbsp; 0: [<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; path: "/event",<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; name: "event",<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; component: {<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; name: "Event",<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br />&nbsp; },<br />&nbsp; {<br />&nbsp;&nbsp;&nbsp; 1: [<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; path: "/jobs",<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; name: "job",<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; component: {<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; name: "Job",<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br />&nbsp; },<br />...<br />]</pre>
<p>so I just loop through each returned item, get the first array item, and add it to the <code>routes</code> array. And voila!, it's done.</p>
<p>There are a couple benefits to this approach. First, it keeps my code modularized and keeps my main router file from getting more and more cluttered. Second, this looks toward the future; down the road, I will be creating a Vue CLI plugin that will allow a user to add all of the necessary files for a new resource that has been added to the platform, and it is easier to create a new file with a pre-determined content than it is to modify the contents of an existing file.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Render Functions, Icons, and Badges With Vuetify</title>
         <link>http://www.smgaweb.com/blog/render-functions-icons-badges-vuetify</link>
         <media:content medium="image" url="https://cdn.buttercms.com/BtHJsU6QcOtkQvwn3SEG"/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Wed, 15 Apr 2020 14:45:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/render-functions-icons-badges-vuetify</guid>
         <description>My steps to generate a Vuetify icon and badge using a render function in VueJS.</description>
         <content:encoded>
        <![CDATA[<p>In the Vue app that I'm currently working on, I ran into a case where I had to use two tools that are, shall we say, not my strongest: render functions and slots. Fortunately, I was able to figure out how to do what I needed to do, and I'm here to share about it.</p>
<p>My use case is that I need to display an <a href="https://v15.vuetifyjs.com/en/components/icons" target="_blank" rel="follow noopener">icon</a> with a <a href="https://v15.vuetifyjs.com/en/components/badges" target="_blank" rel="follow noopener">badge</a>&nbsp;(yes, cue the classic movie line about how <a href="https://www.youtube.com/watch?v=VqomZQMZQCQ" target="_blank" rel="follow noopener">we don't need no stinking badges</a>) that contains a number. If I could just do it with the default HTML tags, it wold be easy. Here is the HTML from the Vuetify docs:</p>
<pre>&lt;v-badge left&gt;<br />  &lt;template v-slot:badge&gt;<br />    &lt;span&gt;6&lt;/span&gt;<br />  &lt;/template&gt;<br />  &lt;v-icon<br />    large<br />    color="grey lighten-1"<br />  &gt;<br />  shopping_cart<br />  &lt;/v-icon&gt;<br />&lt;/v-badge&gt;</pre>
<p>However, in this app, the bottom half of every page is a data table that uses a Vuetify <a href="https://v15.vuetifyjs.com/en/components/data-tables" target="_blank" rel="follow noopener">v-data-table</a> component to list data, and each column uses a functional component to render the content.</p>
<pre><span>&lt;</span><span>script</span><span>&gt;<br /></span><span>export default </span><span>{<br /></span><span>  </span><span>name</span><span>: </span><span>'BaseTableColumn'</span><span>,<br /></span><span>  </span><span>props</span><span>: </span><span>{<br /></span><span>    </span><span>format</span><span>: </span><span>Function</span><span>,<br /></span><span>    </span><span>item</span><span>: </span><span>Object</span><span>,<br /></span><span>    </span><span>index</span><span>: </span><span>String</span><span>,<br /></span><span>  </span><span>}</span><span>,<br /></span><span>  </span><span>methods</span><span>: </span><span>{<br /></span><span>    </span><span>getText</span><span>() {<br /></span><span>      </span><span>return this</span><span>.</span><span>item</span><span>[</span><span>this</span><span>.</span><span>index</span><span>]</span><span>;<br /></span><span>    </span><span>}</span><span>,<br /></span><span>  </span><span>}</span><span>,<br /></span><span>  </span><span>render</span><span>(</span><span>createElement</span><span>) {<br /></span><span>    </span><span>if </span><span>(</span><span>this</span><span>.</span><span>$props</span><span>.</span><span>format</span><span>) {<br /></span><span>      </span><span>return this</span><span>.</span><span>$props</span><span>.</span><span>format</span><span>(</span><span>this</span><span>.</span><span>item</span><span>, </span><span>this</span><span>.</span><span>index</span><span>, </span><span>createElement</span><span>)</span><span>;<br /></span><span>    </span><span>}<br /></span><span>    </span><span>return </span><span>createElement</span><span>(</span><span>'div'</span><span>, </span><span>this</span><span>.</span><span>getText</span><span>())</span><span>;<br /></span><span>  </span><span>}</span><span>,<br /></span><span>}</span><span>;<br /></span><span>&lt;/</span><span>script</span><span>&gt;</span></pre>
<p>So in this case, where I need to generate the markup is in <code>this.$props.format,</code> where <code>format</code> is an object key.. As you can see from the arguments passed above, the signature is</p>
<pre><span>format</span><span>: </span><span>(</span><span>item</span><span>, </span><span>index</span><span>, </span><span>createElement</span><span>) </span><span>=&gt; </span><span>{<br />  // rendering code goes here.<br /></span>});</pre>
<p>As you can see at my initial <a href="https://stackoverflow.com/questions/61149713/how-to-create-vuetity-badge-and-icon-using-createelement-in-render-function" target="_blank" rel="follow noopener">Stackoverflow post,</a> this got me pretty close:</p>
<pre class="lang-js prettyprint prettyprinted"><code><span class="pln">format</span><span class="pun">:</span><span class="pln"> </span><span class="pun">(</span><span class="pln">item</span><span class="pun">,</span><span class="pln"> index</span><span class="pun">,</span><span class="pln"> createElement</span><span class="pun">)</span><span class="pln"> </span><span class="pun">=&gt;</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
  </span><span class="kwd">const</span><span class="pln"> propsObj </span><span class="pun">=</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
    attrs</span><span class="pun">:</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
      color</span><span class="pun">:</span><span class="pln"> </span><span class="str">'blue'</span><span class="pun">,</span><span class="pln">
    </span><span class="pun">},</span><span class="pln">
    props</span><span class="pun">:</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
      overlap</span><span class="pun">:</span><span class="pln"> </span><span class="kwd">true</span><span class="pun">,</span><span class="pln">
      left</span><span class="pun">:</span><span class="pln"> </span><span class="kwd">true</span><span class="pun">,</span><span class="pln">
    </span><span class="pun">},</span><span class="pln">
    slots</span><span class="pun">:</span><span class="pln"> </span><span class="pun">{</span><span class="pln">
      badge</span><span class="pun">:</span><span class="pln"> </span><span class="str">'dummy'</span><span class="pun">,</span><span class="pln">
    </span><span class="pun">},</span><span class="pln">
  </span><span class="pun">};</span><span class="pln">
  </span><span class="kwd">return</span><span class="pln"> createElement</span><span class="pun">(</span><span class="str">'v-badge'</span><span class="pun">,</span><span class="pln"> propsObj</span><span class="pun">,</span><span class="pln"> </span><span class="pun">[</span><span class="pln">createElement</span><span class="pun">(</span><span class="str">'v-icon'</span><span class="pun">,</span><span class="pln"> </span><span class="pun">{</span><span class="pln"> attrs</span><span class="pun">:</span><span class="pln"> </span><span class="pun">{</span><span class="pln"> color</span><span class="pun">:</span><span class="pln"> </span><span class="str">'success'</span><span class="pun">,</span><span class="pln"> large</span><span class="pun">:</span><span class="pln"> </span><span class="kwd">true</span><span class="pln"> </span><span class="pun">}</span><span class="pln"> </span><span class="pun">},</span><span class="pln"> </span><span class="str">'account_circle'</span><span class="pun">)]);</span><span class="pln">
</span><span class="pun">}</span></code></pre>
<p>but I still wasn't getting the badge or badge content, even though I could see in my markup that the outer <code>&lt;span&gt;</code> for the badge was there around the icon elements. Thanks to my buddy&nbsp; - and fellow <a href="https://devchat.tv/views-on-vue/" target="_blank" rel="follow noopener">Views on Vue</a> panelist - Austin Gil (aka <a href="https://twitter.com/stegosource" target="_blank" rel="follow noopener">Stegosource</a>), I was able to get it working. Here's the final working code:</p>
<pre><span>format</span><span>: </span><span>(</span><span>item</span><span>, </span><span>index</span><span>, </span><span>createElement</span><span>) </span><span>=&gt; </span><span>{<br /></span><span>  </span><span>const </span><span>propsObj </span><span>= </span><span>{<br /></span><span>    </span><span>props</span><span>: </span><span>{<br /></span><span>      </span><span>overlap</span><span>: </span><span>true</span><span>,<br /></span><span>      </span><span>left</span><span>: </span><span>true</span><span>,<br /></span><span>      </span><span>color</span><span>: </span><span>'success'</span><span>,<br /></span><span>    </span><span>}</span><span>,<br /></span><span>  </span><span>}</span><span>;<br /></span><span>  const span = createElement('span', { slot: 'badge' }, '5');  <br /></span><span>  const </span><span>icon </span><span>= </span><span>createElement</span><span>(</span><span>'v-icon'</span><span>, </span><span>{ </span><span>props</span><span>: </span><span>{ </span><span>color</span><span>: </span><span>'warning'</span><span>, </span><span>large</span><span>: </span><span>true </span><span>} }</span><span>, </span><span>'account_circle'</span><span>)</span><span>;<br /></span><span>  </span><span>return </span><span>createElement</span><span>(</span><span>'v-badge'</span><span>, </span><span>propsObj</span><span>, </span><span>[</span><span>span</span><span>, </span><span>icon</span><span>])</span><span>;<br /></span><span>}</span></pre>
<p>What we did was break the render functions in parts to match the desired markup. Looking at the markup, there are three parts:</p>
<ol>
<li>The outer <code>v-badge</code></li>
<li>The inner <code>badge</code> slot</li>
<li>The inner <code>v-icon</code> element</li>
</ol>
<p>So first, we use <code>createElement</code> to generate the badge slot:</p>
<pre><span>const span = createElement('span', { slot: 'badge' }, '5');</span></pre>
<p><span>Next, we generate the markup for the icon:</span></p>
<pre><span>const icon = createElement('v-icon', { props: { color: 'warning', large: true } }, 'account_circle');</span></pre>
<p><span>And finally we put it together, passing the <code>span</code> and <code>icon</code> values created above, also passing in <code>propsObj</code> with attributes for the<code> v-badge</code> element.</span></p>
<pre><span>return createElement('v-badge', propsObj, [span, icon]);</span></pre>
<p><span>Here is the end result:</span></p>
<p><span><img src="https://cdn.buttercms.com/BtHJsU6QcOtkQvwn3SEG" alt="icon_with_badge.png" /></span></p>
<p><span>From a coding standpoint, now I just need to control the value that is displayed in the badge, but that's trivial (and outside the scope of this post).</span></p>
<p><span>Thanks for reading, and happy coding.</span></p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Creating a Fast Site with Nuxt and CSS</title>
         <link>http://www.smgaweb.com/blog/creating-fast-site-nuxt-css</link>
         <media:content medium="image" url="https://cdn.buttercms.com/haYutbiiT7qJjvU4Kq1u"/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Sat, 14 Mar 2020 04:52:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/creating-fast-site-nuxt-css</guid>
         <description>How I used Nuxt and pure CSS to build a very fast website with good SEO.</description>
         <content:encoded>
        <![CDATA[<p>As a developer who has spent the majority of his years working on the back end of website projects, my front end skills have not always been very developed, to put it mildly. At those times when I did need to do theming work, I would rely on tools like Bootstrap, Bulma, or Vuetify to handle the bulk of it for me. However, over the last year, after hearing so much about the features that have been added to HMTL and CSS over the past few years, I decided to start learning about CSS.</p>
<p>In particular, CSS Grid and Flexbox intrigued me, because layout has always been fairly difficult, so I started out by taking the free courses from Wes Bos on <a href="https://cssgrid.io" rel="follow">Grid</a>&nbsp;and <a href="https://flexbox.io" rel="follow">Flexbox</a>. I also want to be able to learn to design my own layouts, so I got Adam Wathan&rsquo;s <a href="https://refactoringui.com" rel="follow">Refactoring UI</a>&nbsp;(although I haven&rsquo;t been able to get started on it yet).</p>
<p>I was starting to look around for a project to work on, when I was presented with an opportunity to do a <a href="https://cesf.us" rel="follow">website for a local non-profit</a>. Still not feeling comfortable with designing my own theme, I went browsing on the internets and came across the <a href="https://preview.themeforest.net/item/icare-nonprofit-fundraising-html-template/full_screen_preview/7974340" rel="follow">iCare theme</a> on Theme Forest. After getting approval from the organization, I purchased the theme.</p>
<p>&nbsp;<strong>Selecting the Site Toolset</strong></p>
<p>In this particular case, SEO was very important to the organization, and being familiar with the Vue ecosystem (I work with Vue in my day job), I decided to go with Nuxt, because of its server side rendering capabilities. For the back end, I looked at a couple of different options. I knew I wanted to use a headless CMS. I worked with Drupal for a number of years, so I&rsquo;m very familiar with content modeling and structure, but I wanted something that I didn&rsquo;t have to set up myself. I considered a GitHub pages-type structure where I write in markdown files and commit directly to a repository, but there were a couple problems with that. First, that would work great for me, but I am the only technical person in the organization, and if I were to get hit by the proverbial bus (or fire engine, <a href="https://www.facebook.com/clackamasfire/photos/a.3985692881444509/3985697871444010/?type=3&amp;theater" rel="follow">in my case</a>) I wanted a non-technical person to be able to manage site content in my absence. I really like <a href="/" rel="follow">ButterCMS</a> (the CMS I use for this site), because it has a very flexible data modeling and content management user interface, and API is drop dead easy to use. However, in order to get the full data modeling capabilities, the cost is $99/month, and that was cost prohibitive for the organization. I started looking at the many hosted headless CMS options, and narrowed it down to Prismic, Sanity, Contentful, Zesty, and Strapi. After considering all the different pros and cons (cost, API ease of use, data modeling capabilities, etc) of each one, I decided to go with <a href="https://prismic.io/" rel="follow">Prismic</a>. The Content Query API takes a little getting used to, and the API docs can be a little confusing, but the UI is fairly intuitive and flexible, and it has good code documentation for various frameworks and languages (including VueJs and Nuxt). For just one user it is free, and adding two more users only costs $7/month, so that fit right in the organization&rsquo;s budget.</p>
<p>&nbsp;From a hosting standpoint, my own site is hosted in <a href="https://www.netlify.com/" rel="follow">Netlify</a>, and it is a popular and flexible platform that I am already familiar with, so choosing it was a no-brainer.</p>
<p>&nbsp;<strong>Implementing the Theme</strong></p>
<p>My initial plan with the iCare theme was to just incorporate it into Nuxt, add the data, and be off and running. However, the theme contains multiple other dependencies (much custom JS, jQuery, PopperJS, FontAwesome, etc.) and although I tried and tried, I could never get everything to work without a bunch of re-work. After banging my head against that for a while, I decided to build my theme from scratch with CSS Grid. So I started from the front page, and built it out, doing my best to make it look exactly like the iCare theme, but only in appearance, not underlying structure. After a while, I got the header, the footer, and the central content area complete in a full page format. However, it wasn&rsquo;t responsive at all (you couldn&rsquo;t even scroll to the side on a smaller screen), and although I had read about media queries and flexbox, I wasn&rsquo;t sure exactly how to implement them in my theme.</p>
<p>Instead of doing more head banging, I called on my friend <a href="https://www.linkedin.com/in/illepic/" rel="follow">Chris Bloom</a>. Chris is the Director of Frontend Engineering at <a href="https://www.phase2technology.com/" rel="follow">Phase2 Technology</a>, and a real CSS guru. &nbsp;He and I go back to working in Drupal for Sony Music a few years ago, so I&rsquo;ve known him for a while. He&rsquo;s also a big proponent of <a href="https://tailwindcss.com/" rel="follow">Tailwind CSS</a>, and I&rsquo;d recently seen him present on using it in <a href="https://www.meetup.com/Portland-Vue-js-Meetup/events/267781079/)" rel="follow">VueJS</a>. I gave him access to my theme repo and said &ldquo;can you make this responsive?&rdquo;, and he said &ldquo;sure!&rdquo;, and a couple days later, sent me a merge request.</p>
<p>I studied it for a while, made some changes, and got stuck, so Chris took an hour and a half and on a video call, and walked me through everything he had done (if you want to someone who is passionate about CSS, get Chris talking about CSS. Just sayin&rsquo;.). The two things I learned from what he showed me were 1) using media queries and 1) Tailwind-type utility classes.</p>
<p>&nbsp;<strong>Media Queries</strong></p>
<p>According to <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries" rel="follow">MDN</a>, media queries &ldquo;are useful when you want to modify your site or app depending on a device's general type (such as print vs. screen) or specific characteristics and parameters (such as screen resolution or browser&nbsp;<a href="https://developer.mozilla.org/en-US/docs/Glossary/viewport">viewport</a>&nbsp;width).&rdquo; In this particular case, we just defined four queries that were minimum width sizes (borrowed from Tailwind&rsquo;s breakpoints):</p>
<pre>@media (min-width: 640px) {<br />&nbsp; .container {<br />&nbsp; &nbsp; max-width: 640px;<br />&nbsp; }<br />}<br /><br />@media (min-width: 768px) {<br />&nbsp; .container {<br />&nbsp; &nbsp; max-width: 768px;<br />&nbsp; }<br />}<br /><br />@media (min-width: 1024px) {<br />&nbsp; .container {<br />&nbsp; &nbsp; max-width: 1024px;<br />&nbsp; }<br />}<br /><br />@media (min-width: 1280px) {<br />&nbsp; .container {<br />&nbsp; &nbsp; max-width: 1200px;<br />&nbsp; }<br />}</pre>
<p>The thing that it took me a while to get my head around was that for any of these queries, because we were only using <strong>min-width</strong>, the styles used in that media query would apply for any size up to full screen size, unless overridden in another query of a higher size. For instance, in the example above, if there had been no <strong>.container</strong> selected in the query for <strong>min-width: 1024px</strong> or <strong>1280p</strong>x, the <strong>max-width</strong> for the .container class would be 768px. What this also means is that there are actually 5 sizes; the four sizes listed above, and anything below 640px wide; namely, phones.</p>
<p>A good example of where this comes into play is with the main nav bar. Typically, in order to get a row of menu items, you would create a list of items using <code>&lt;ul&gt;</code> and <code>&lt;li&gt;</code> tags, and then set <strong>display: inline-block</strong> (or even <strong>display:flex</strong>) on the <code>&lt;li&gt;</code> elements. However, in a mobile view, we want them stacked vertically, and since that is the default list structure, we don&rsquo;t have to apply CSS to make that happen. Then, when the screen is wide enough that we do want them to be horizontal, we can use a media query to make them inline.</p>
<pre>@media (min-width: 768px) {<br />&nbsp; .flex {<br />&nbsp; &nbsp; display: flex;<br />&nbsp; }<br />}</pre>
<p>I finally understood that this is what is meant by <em>mobile first design</em>; designing for the smaller screen, and then adding things as your screen gets wider.</p>
<p><strong>Utility Classes</strong></p>
<p>As I get it, the idea behind utility classes is that each class does only one thing, and the name reflects what it does. For instance, a simple example would be aligning text:</p>
<pre><code>.text-left {</code><br /><code>&nbsp; text-align: left;</code><br /><code>}&nbsp;</code></pre>
<p>Then, you just apply the classes to a given element as needed. The downside is that you get a longer list of classes for a given element, but at the same time, you can also tell what that class is doing, as compared to a class with many attributes.</p>
<p><strong>Combining Media Queries and Utility Classes</strong></p>
<p>Now we can combine media queries with utility classes to easily modify section widths depending on screen sizes. As an example, on the events details page, we have this element:</p>
<pre>&lt;div class="container"&gt;<br />&nbsp; &lt;div class="content-area sm:flex justify-between"&gt;<br />&nbsp; &nbsp; &lt;div class="event-content-left sm:w-2/3 md:w-770"&gt;</pre>
<p>&nbsp;And here is the CSS to go along with them</p>
<pre>&nbsp;@media (min-width: 640px) {<br />&nbsp; &nbsp; .container {<br />&nbsp; &nbsp; &nbsp; max-width: 640px<br />&nbsp; &nbsp; }<br /><br />&nbsp; .sm\:flex {<br />&nbsp; &nbsp; display: flex;<br />&nbsp; }<br /><br />&nbsp; .sm\:w-1\/3 {<br />&nbsp; &nbsp; width: 33%;<br />&nbsp; }<br /><br />&nbsp; .sm\:w-2\/3 {<br />&nbsp; &nbsp; width: 66%;<br />&nbsp; }<br />}<br /><br />@media (min-width: 768px) {<br />&nbsp; .container {<br />&nbsp; &nbsp; max-width: 768px;<br />&nbsp; }<br /><br />&nbsp; .md\:w-1\/3 {<br />&nbsp; &nbsp; width: 33%;<br />&nbsp; }<br /><br />&nbsp; .md\:w-2\/3 {<br />&nbsp; &nbsp; width: 66%;<br />&nbsp; }<br /><br />&nbsp; .md\:w-770 {<br />&nbsp; &nbsp; width: 770px;<br />&nbsp; }<br />}</pre>
<p>And here is what&rsquo;s happening:</p>
<p>At a screen width of 640-767px:</p>
<ul>
<li>The width of <strong>.container</strong> has a <strong>max-width</strong> value of 640px.</li>
<li>The <strong>display: flex</strong> property is added to the <strong>.content-area</strong> element.</li>
<li>The width of the .<strong>content-area</strong> element is set to 33%.</li>
<li>The width of the <strong>.event-content-left</strong> element is set to 66%.</li>
</ul>
<p>At a screen width of 768-1023px:</p>
<ul>
<li>The width of the <strong>.event-content-left</strong> element is set to 770px.</li>
<li>.<strong>display:flex</strong> is still set.</li>
<li>The width of the <strong>.content-area</strong> element is still set to 33%.</li>
</ul>
<p>One interesting thing to note is the difference between the class declarations in CSS and applications in HTML for the <strong>sm:</strong> and <strong>md:</strong> classes; the : and<strong> /</strong> characters have to be escaped in the CSS (sm\:w-1\/3), but is not in the HTML (sm:w-1/3).</p>
<p><strong>Forms</strong></p>
<p>The one last feature I needed to implement was forms. Fortunately, Netlify has a <a href="https://docs.netlify.com/forms/setup/" rel="follow">solution for forms</a>. The basic gist is that you create your form as you normally would (in this case, in a .vue single file component), and then create a .html file that has only the HTML form content (in my case, I created them under the <strong>/static/form_dummy</strong> directory), Netlify finds that file and automagically handles it, <a href="https://docs.netlify.com/forms/submissions/" rel="follow">saving the submissions in your site dashboard</a>. You can also set up <a href="https://docs.netlify.com/forms/notifications/#email-notifications" rel="follow">email notifications</a> when a form is submitted. On top of that, they even provide spam protection using Akismet, along with additional protection using a <a href="https://docs.netlify.com/forms/spam-filters/" rel="follow">honeypot field</a>.</p>
<p><strong>Conclusion</strong></p>
<p>The end result is a very fast, lightweight, static <a href="https://cesf.us" rel="follow">site</a> that is cheap to host, and with an easy to manage content CMS. The primary benefit for me was that I was able to use the process of building the site to do a lot of learning about CSS Grid and Flexbox, media queries, utility classes, and how to put them all together. What added more satisfaction was that it wasn't just a learning project that I came up with; it was a very real website for a very real organization that does a lot of great things.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>I&#x27;m Now A Panelist On The Javascript Jabber Podcast</title>
         <link>http://www.smgaweb.com/blog/im-now-a-panelist-on-the-javascript-jabber-podcast</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Sun, 06 Oct 2019 04:49:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/im-now-a-panelist-on-the-javascript-jabber-podcast</guid>
         <description>After being a fan of the Javascript Jabber podcast, I get the privilege of now being a regular panelist.</description>
         <content:encoded>
        <![CDATA[<p>As I mentioned in a <a href="https://www.smgaweb.com/blog/on-the-my-javascript-story-podcast" target="_blank" rel="noopener">previous post</a>, last year I was a guest on the My Javascript Story podcast, one of the multiple podcasts produced by <a href="https://devchat.tv/" title="DevChat.tv">devchat.tv.</a> Well, being an avid fan of the podcast, I heard when Chuck put out a call for new panelists, I responded, and now I am a regular panelist. So far I've recorded two episodes (not yet released as of this writing), including episode #400, but both have been a ton of fun. Part of the fun is getting to meet people that I've listened to for years and then working with them. The other part is getting to meet people that are doing great stuff in the Javascript world, and then getting to discuss it with them.</p>
<p></p>
<p>It's been a fun ride so far, and I'm looking forward to many more episodes.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>On the My JavaScript Story Podcast</title>
         <link>http://www.smgaweb.com/blog/on-the-my-javascript-story-podcast</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Sat, 29 Sep 2018 02:56:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/on-the-my-javascript-story-podcast</guid>
         <description>I was recently a guest on the My JavaScript Story podcast with Charles Max Wood of devchat.tv to talk about my history as a developer and how I got into javascript.</description>
         <content:encoded>
        <![CDATA[<p>Recently I had the pleasure of being on the <a href="https://devchat.tv/my-javascript-story/mjs-078-steve-edwards/">My JavaScript Story</a> podcast with <a href="https://twitter.com/cmaxw" target="_blank" rel="noopener">Charles Max Wood</a> of <a href="https://devchat.tv/" target="_blank" rel="noopener">devchat.tv</a>. I've listened to multiple devchat.tv podcasts over the past few years (<a href="https://devchat.tv/adv-in-angular/">Adventures in Angular</a>, <a href="https://devchat.tv/js-jabber/" target="_blank" rel="noopener">JavaScript Jabber</a>, and now <a href="https://devchat.tv/views-on-vue/" target="_blank" rel="noopener">Views on Vue</a>), and when Chuck started the <em>My JavaScript Story</em> podcast, I really wanted to be on it, just because my story of how I got into web development is different than most of the stories I hear (e.g. I started in middle/high school, or I started with gaming), so when he sent out the announcement that he would start doing episodes with people that hadn't previously been guests on JavaScript Jabber, I jumped at the chance.</p>
<p>As expected, I had a great time recording the episode. I got to talk about getting hired to do tech support while having almost zero knowledge of computers back in the mid-90s, using MS FrontPage, learning PHP/MySQL, and eventually getting into JavaScript with Angularjs, and now Vuejs. Many thanks to Chuck for having me as a guest.</p>
<p>You can listen to the episode <a href="https://devchat.tv/my-javascript-story/mjs-078-steve-edwards/" target="_blank" rel="noopener">here</a>.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Custom Video Export/Import Process With Views and Feeds</title>
         <link>http://www.smgaweb.com/blog/custom-video-exportimport-process-views-and-feeds</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Tue, 02 May 2017 20:49:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/custom-video-exportimport-process-views-and-feeds</guid>
         <description>Creating a custom video node export/import process using custom forms, RSS feeds with Views, and a custom Feeds importer using the SimplePie parser.</description>
         <content:encoded>
        <![CDATA[<p>In the <a href="http://www.mrc.org">Media Research Center</a>'s set of three main Drupal sites, <a href="http://www.mrctv.org" target="_blank" rel="noopener">MRCTV</a> serves as our video platform where all videos are created and stored as nodes, and then using <a href="https://drupal.org/project/feeds" target="_blank" rel="noopener">Feeds</a>, specific videos are imported into the other two sites (<a href="http://www.newsbusters.org" target="_blank" rel="noopener">Newsbusters</a> and <a href="http://www.cnsnews.com" target="_blank" rel="noopener">CNS News</a>) as video nodes. Then, on NB and CNS, we use the <a href="https://www.drupal.org/project/video_embed_field" target="_blank" rel="noopener">Video Embed Field</a> module with a custom VEF provider for MRCTV to play the embedded videos.</p>
<p>There are only specific videos that need to be imported into the destination sites, so a way to map channels between the two sites is needed. All three sites have a Channels vocabulary, and a mapping is created between the appropriate channels. This mapping has two parts:</p>
<ol>
<li>A feed of channels terms on NB and CNS.</li>
<li>A custom admin form that links source channels on MRCTV with target channels on the destination site.</li>
</ol>
<p>On the receiving site side, in addition to the standard feed content, the following custom elements are needed for the Feeds import:</p>
<ol>
<li>The nid of the video node on MRCTV. This is used to create the URL that is put into the VEF field.</li>
<li>The taxonomy terms for the Channels vocabulary terms in the destination sites (NB and CNS).</li>
</ol>
<p>Since these are outside of the standard feed components, they will need to be added custom to the feed items.</p>
<p>I documented my custom Feeds importer <a href="https://drupal.stackexchange.com/questions/89026/handling-custom-parsing-and-importing-with-feeds/89179#89179" target="_blank" rel="noopener">on drupal.stackexchange</a>, so you can see the code there.</p>
<p>MRCTV is finally in the process of being updated to D8 from D6 (insert derision here), so both the mapping form and the feed needed to be re-created. The first part of the structure is the channel mapping form. The following file <em>VideoExportForm.php</em> is placed in <em>/modules/custom/video_export/src/Form</em>:</p>
<pre>/**
 * @file
 * Contains \Drupal\video_export\Form\VideoExportForm.
 */

namespace Drupal\video_export\Form;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use GuzzleHttp\Exception\RequestException;

class VideoExportForm extends ConfigFormBase {
  
  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'video_export_settings';
  }
    
  /**
   * {@inheritdoc}
   */
  public function buildform(array $form, FormStateInterface $form_state) {
    $form = array();
    $channels = array();
    
    // Get list of channels.
    $terms =\Drupal::entityTypeManager()-&gt;getStorage('taxonomy_term')-&gt;loadTree('channels');
    foreach ($terms as $term) {
      $channels[$term-&gt;tid] = $term-&gt;name;
    }
    
    // Get config data from video_export.settings.yml.
    $config = \Drupal::config('video_export.settings');
    $mapping_config = \Drupal::config('video_export.mappings');
    $sites = $config-&gt;get('sites');
    
    foreach($sites as $site =&gt; $site_data) {
      // Get channels list.
      try {
        $response = \Drupal::httpClient()-&gt;get($site_data['channel_url'], array('headers' =&gt; array('Accept' =&gt; 'text/plain')));
        $data = $response-&gt;getBody();
        if (empty($data)) {
          return FALSE;
        }
      }
      catch (RequestException $e) {
        return FALSE;
      }
  
      $channel_data = new \SimpleXMLElement($data);
      foreach ($channel_data-&gt;channel as $channel) {
        $channel_name = $channel-&gt;name-&gt;__toString();
        $channel_tid = $channel-&gt;tid-&gt;__toString();
        $target_channels[$channel_tid] = $channel_name;
      }
      // Sort array alphabetically by element.
      asort($target_channels, SORT_STRING);
  
      $target_channel_options = array();
      $target_channel_options[0] = "No Channel";
      foreach ($target_channels as $target_tid =&gt; $target_name) {
        $target_channel_options[$target_tid] = $target_name;
      }
  
      //Get mappings from mappings conifg.
      $mappings = $mapping_config-&gt;get('sites');
      foreach ($mappings[$site]['mappings'] as $mrctv_channel =&gt; $target_channel) {
        $mapping_defaults[$mrctv_channel] = $target_channel;
      }
  
      $form[$site] = array(
        '#type' =&gt; 'details',
        '#title' =&gt; t($site . ' Channel Mappings'),
        '#description' =&gt; t('Map MRCTV channels to ' . $site . ' channels'),
        '#collapsible' =&gt; TRUE,
        '#collapsed' =&gt; TRUE,
        '#tree' =&gt; TRUE,
      );
  
      // Loop through all of the categories and create a fieldset for each one.
      foreach ($channels as $id =&gt; $title) {
        $form[$site]['channels'][$id] = array(
          '#type' =&gt; 'select',
          '#title' =&gt; $title,
          '#options' =&gt; $target_channel_options,
          '#tree' =&gt; TRUE,
        );
        if (in_array($id, array_keys($mapping_defaults))) {
          $form[$site]['channels'][$id]['#default_value'] = intval($mapping_defaults[$id]);
        }
      }
    }
    
    // Get mapping configs.
    $xml = array();
    $mapping_config = \Drupal::config('video_export.mappings');
    $sites = $mapping_config-&gt;get('sites');
    $channel_mappings = $sites[$site]['mappings'];
    // Get video nodes that belong to one of the selected channels.
    $query = \Drupal::entityQuery('node')
      -&gt;condition('status', 1)
      -&gt;condition('type', 'video')
      -&gt;condition('changed', REQUEST_TIME - 59200, '&gt;=')
      -&gt;condition('field_channels.entity.tid', array_keys($channel_mappings), 'IN');
    $nids = $query-&gt;execute();
    // Load the entities using the nid values. The array keys are the associated vids.
    $video_nodes = \Drupal::entityTypeManager()-&gt;getStorage('node')-&gt;loadMultiple($nids);

    foreach ($video_nodes as $nid =&gt; $node) {
      $host = \Drupal::request()-&gt;getSchemeAndHttpHost();
      $url_alias = \Drupal::service('path.alias_manager')-&gt;getAliasByPath('/node/' . $nid);
      // Get channels values.
      $channel_tids = array_column($node-&gt;field_channels-&gt;getValue(), 'target_id');
      $create_date = \Drupal::service('date.formatter')-&gt;format($node-&gt;getCreatedTime(), 'custom', 'j M Y h:i:s O');
      $item = array(
        'title' =&gt; $node-&gt;getTitle(),
        'link' =&gt; $host . $url_alias,
        'description' =&gt; $node-&gt;get('body')-&gt;value,
        'mrctv-nid' =&gt; $nid,
        'guid' =&gt; $nid . ' at ' . $host,
        'pubDate' =&gt; $create_date
      );
      // Check for short title and add it if it's there.
      if ($node-&gt;get('field_short_title')-&gt;value) {
        $item['short-title'] = $node-&gt;get('field_short_title')-&gt;value;
      }
      foreach ($channel_tids as $ctid) {
        $item[$site . '-channel-map'][] = $ctid;
      }
      $xml[] = $item;
    }

    return parent::buildForm($form, $form_state);
  }
  
  /**
   * {@inheritdoc}.
   */
  public function validateForm(array &amp;$form, FormStateInterface $form_state) {
  
  }
  
  protected function getEditableConfigNames() {
    return ['video_export.mappings'];
  }
  
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &amp;$form, FormStateInterface $form_state) {
    $values = $form_state-&gt;getValues();
    $config = $this-&gt;config('video_export.mappings');
    $sites = array();

    foreach($values as $site =&gt; $mappings) {
      if (is_array($mappings)) {
        foreach ($mappings['channels'] as $mrctv_channel =&gt; $target_channel) {
          if ($target_channel != 0) {
            $sites[$site]['mappings'][$mrctv_channel] = $target_channel;
          }

        }
        $config-&gt;set('sites', $sites);
      }
    }
    $config-&gt;save();
  
    parent::submitForm($form, $form_state);
  }
}
</pre>
<p>The setting for the channels feeds on NB and CNS are stored in <em>/modules/custom/video_export/config/install/video_export.settings.yml</em>:</p>
<pre>sites:
  newsbusters:
    channel_url: 'http://www.newsbusters.org/path/to/channels'
  cnsnews:
    channel_url: 'http://www.cnsnews.com/path/to/channels'
list_time: 24
	</pre>
<p>Since this is an admin settings form, I extend the <a href="https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Form!ConfigFormBase.php/class/ConfigFormBase/8.2.x" target="_blank" rel="noopener">ConfigFormBase</a> class. This adds some additional functionality over the standard <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Form%21FormBase.php/class/FormBase/8.2.x" target="_blank" rel="noopener">FormBase</a> class, similar to the way the <a href="https://api.drupal.org/api/drupal/modules%21system%21system.module/function/system_settings_form/7.x">system_settings_form(</a>) function does in D7 and older (see the <a href="https://www.drupal.org/node/1910694">change record</a>&nbsp;for details).</p>
<p>As mentioned above the form does the following things:</p>
<ol>
<li>Reads the channels feed from the destination sites</li>
<li>Creates a fieldset for each site with a select list for each MRCTV channel where the user can select the destination channel.</li>
<li>Saves the mappings in config.</li>
</ol>
<p>The next thing that is needed is the feed of video nodes that are available to be imported. After trying unsuccessfully to <a href="https://drupal.stackexchange.com/questions/234897/adding-rss-feed-elements-with-custom-rest-api-endpoint" target="_blank" rel="noopener">create a custom REST API endpoint</a>, I ended up going with a Feeds display in Views. Out of the box I can create my feed, but I still need to add my custom elements. In D6, I used <a href="https://api.drupal.org/api/drupal/developer%21hooks%21core.php/function/hook_nodeapi/6.x" target="_blank" rel="noopener">hook_nodeapi($op = 'rss item')</a> to add my custom elements. In other feeds on D7 sites I've been able to use the <a href="https://drupal.org/project/views_rss" target="_blank" rel="noopener">Views RSS module</a> with its provided hooks to add custom RSS elements, but as of now it is currently unusable for D8 due to <a href="https://www.drupal.org/node/2425699" target="_blank" rel="noopener">one major issue</a>.</p>
<p>Finally, since everything in D8 is based on OOP, I knew there had to be a way to override a Views class at some level, so after some searching, I decided to override the display plugin. I poked around in the Views code and found the <a href="https://api.drupal.org/api/drupal/core%21modules%21views%21src%21Plugin%21views%21row%21RssFields.php/class/RssFields/8.2.x">RssFields</a> class that is used for the field level display for a Feeds display, so I overrode that.</p>
<pre>namespace Drupal\video_export\Plugin\views\row;

use Drupal\views\Plugin\views\row\RssFields;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

/**
 * Renders an RSS item based on fields.
 *
 * @ViewsRow(
 *   id = "mrctv_rss_fields",
 *   title = @Translation("MRCTV Fields"),
 *   help = @Translation("Display fields as RSS items."),
 *   theme = "views_view_row_rss",
 *   display_types = {"feed"}
 * )
 */
class MRCTVRssFields extends RssFields {
  
  /**
   * Override of RssFields::render() with additional fields.
   *
   * @param object $row
   *
   * @return array
   */
  public function render($row) {
    $build = parent:: render();
    $item = $build['#row'];
    
    // Add MRCTV nid
    $item-&gt;elements[] = array(
      'key' =&gt; 'mrctv-nid',
      'value' =&gt; $row-&gt;nid,
    );
  
    // Add channels and their target nids. We can get them from $row-&gt;_entity.
    $site = $this-&gt;view-&gt;args[0];
    // Get MRCTV nids from view.
    $channel_tids = array_column($row-&gt;_entity-&gt;field_channels-&gt;getValue(), 'target_id');
    // Now, get destination tids from config.
    $mapping_config = \Drupal::config('video_export.mappings');
    $all_mappings = $mapping_config-&gt;get('sites');
  
    foreach($channel_tids as $mrctv_channel) {
      if(in_array($mrctv_channel, array_keys($all_mappings[$site]['mappings']))) {
        $item-&gt;elements[] = array(
          'key' =&gt; $site . '-channel-map',
          'value' =&gt; $all_mappings[$site]['mappings'][$mrctv_channel],
        );
      }
    }
    
    // Re-populate the $build array with the updated row.
    $build['#row'] = $item;
    
    return $build;
  }
}
</pre>
<p>As you can see, the override is fairly simple; all I needed to do was override the <em>render() </em>method. This method returns a render array, so all I do is get the built array from the parent class, add my custom elements to the #row element in the array, and return it.</p>
<p>One thing that I couldn't do simply in the views UI was select the nodes that should be in the feed based on the associate Channels vocabulary terms. These are dynamic, based on the mappings selected in the admin form, so I can't pre-select them in the view settings. This is where <a href="http://www.drupalcontrib.org/api/drupal/drupal%21core%21modules%21views%21views.api.php/function/hook_views_query_alter/8">hook_views_query_alter()</a> comes to the rescue.</p>
<pre>/**
 * Implements hook_views_query_alter().
 */
function video_export_views_query_alter(Drupal\views\ViewExecutable $view, Drupal\views\Plugin\views\query\Sql $query) {
  if ($view-&gt;id() == 'video_export' &amp;&amp; $view-&gt;getDisplay()-&gt;display['id'] == 'feed_1') {
    // First, we need to get the site parameter from the view.
    $site = $view-&gt;args[0];
    
    // Next, we need to get the saved config for the channel mapping.
    $mapping_config = \Drupal::config('video_export.mappings');
    $all_mappings = $mapping_config-&gt;get('sites');
    $tids = array_keys($all_mappings[$site]['mappings']);
   
    // Modify query to get nodes that have the selected nids, which are the array keys.
    $query-&gt;addWhere(NULL, 'node__field_channels.field_channels_target_id', $tids, 'IN');
  }
}
</pre>
<p>All I do here is get the saved mappings from config and add them to the views query as a WHERE condition to limit the feed items to the appropriate nodes.</p>
<p>One issue I ran into with the results was duplicate records. Since field_channels (the entity reference field for the Channels vocabulary) is multiselect, the query returns multiple records for each node if there are multiple Channels terms selected. There are display settings to show multiple items in one row, but they don't take effect here. I didn't dig far enough into the code to know for sure, but my guess is that the grouping happens at a higher layer in the views rendering process, so they don't take effect in this situation.</p>
<p>To get around this, I implemented <a href="https://api.drupal.org/api/drupal/core!modules!views!views.api.php/function/hook_views_pre_render/8.2.x" target="_blank" rel="noopener">hook_views_pre_render()</a>. At this point in the process, the results have been built, so I just loop through them and remove duplicates.</p>
<pre>/**
 * Implements hook_views_query_pre_render().
 */
function video_export_views_pre_render(Drupal\views\ViewExecutable $view) {
  $unique_nids = $new_results = array();
  
  // Loop through results and filter out duplicate results.
  foreach($view-&gt;result as $index =&gt; $result) {
    if(!in_array($result-&gt;nid, $unique_nids)) {
      $unique_nids[] = $result-&gt;nid;
    }
    else {
      $new_results[] = $result;
    }
  }
  // Replace $view-&gt;result with new array. Apparently views requires sequentially keyed
  // array of results instead of skipping keys (e.g. 0, 2, 4, etc), so we can't just
  // unset the duplicates.
  $view-&gt;result = $new_results;
}
</pre>
<p>As noted in the code comment, views seems to require a sequentially numbered array, so you can't just unset the duplicate keys and leave it as is, so I chose to just add each item to a new array. In retrospect, I could have just used PHP functions like <a href="http://php.net/manual/en/function.array-slice.php">array_splice()</a> and <a href="http://php.net/manual/en/function.array-filter.php">array_filter()</a>, but this method works just as well.</p>
<p>It should also be noted that the views hooks need to go in a <em>*.views_execution.inc</em> file, so this one is in <em>/modules/custom/video_export/video_export.views_execution.inc</em>.</p>
<p>All I do at this point is use the <a href="https://www.drupal.org/project/job_scheduler">Job Scheduler</a> module with Feeds in the destination sites to schedule the import at the desired interval, and the process runs by itself.&nbsp;</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Getting URL Alias For Referenced Entity With RESTful API in Drupal</title>
         <link>http://www.smgaweb.com/blog/getting-url-alias-referenced-entity-restful-api-drupal</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Sun, 27 Dec 2015 07:02:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/getting-url-alias-referenced-entity-restful-api-drupal</guid>
         <description>How to  create URL alias lookup endpoints for referenced entities using  RESTFul module in Drupal 7.</description>
         <content:encoded>
        <![CDATA[<p>As documented in a <a href="http://www.smgaweb.com/blog/creating-restful-api-url-alias-lookup-drupal">previous post</a>, with RESTful, you can create a separate API endpoint to look up the base node ID from a URL alias for a specific entity. However, there are also URL aliases for other entities that are referenced from within your base entity, such as users and taxonomy terms. We don't want to have to do a separate lookup for each of these entities outside of our base entity, so the ideal situation would be to just have a field returned within our base entity lookup that contains these URL aliases.</p>
<p>As an example, let's say that in our Blog entity, instead of using the built in node creator for the author, we have an entity reference field called Author (field_author) that references users (this gives us flexibility in specifying the content author in allowing us to easily select other names, as well as allowing us to have multiple authors). And, as is typical in Drupal 7, we use term reference fields for taxonomy terms, and since in a decoupled front end we want to display those terms as links, we need that path. We want to be able to get these aliases as fields when we query the Blog node.</p>
<p>The solution to this problem in RESTful is process callbacks (see <a href="https://www.youtube.com/watch?v=Mpv5OmscTLY&amp;index=5&amp;list=PLZOQ_ZMpYrZv8_c7jd_CkO_93-DnyVFY5" target="_blank" rel="noopener">this video</a> for examples). These work very similar to <a href="https://api.drupal.org/api/drupal/developer%21topics%21forms_api_reference.html/6#process">process callbacks</a> in the Form APi in Drupal; they are simply custom callbacks that can be used to do custom manipulating of field data once it has been retrieved and before it is returned.</p>
<p>The callbacks are added in the <a href="https://github.com/RESTful-Drupal/restful/wiki/2.-Defining-the-public-fields-(2.x)" target="_blank" rel="noopener">field definition in the publicFields function</a>&nbsp;using the <em>process_callbacks</em> key. Here is the definition for a user alias within the Users class:</p>
<pre>class Users__1_1 extends Users__1_0 {

  /**
   * {@inheritdoc}
   */
  protected function publicFields() {
    $public_fields = parent::publicFields();
...
    $public_fields['alias'] = array(
      'property' =&gt; 'uid',
      'process_callbacks' =&gt; array(
        array($this, 'getAlias')
      )
    );

    return $public_fields;
  }

  public function getAlias($value) {
    return drupal_get_path_alias('user/' . $value);
  }
}</pre>
<p>Things to note in this code:</p>
<ul>
<li>The <em>property</em> is the data that you need to have to get the alias, and in this case it is the user id (uid).</li>
<li>We define the callback name as an array with the $this reference as the first item because we are making the callback a member function in this class. If the function were placed elsewhere (such as a .module or .inc file) or is a regular PHP function, we would just pass the function name.</li>
<li>The $value argument that is passed to the callback is the value of the field defined in the property key; in this case, the uid.</li>
</ul>
<p>We just call <a href="https://api.drupal.org/api/drupal/includes%21path.inc/function/drupal_get_path_alias/7">drupal_get_path_alias()</a> and pass it the uid to get the URL alias. This alias is then returned as part of the author member in the JSON object:</p>
<pre>{
  "type": "users",
  "id": "123",
  "attributes": {
    "id": "123",
    "label": "Steve Edwards",
    "self": "http://mysite.com/api/v1.1/users/123",
    "mail": "sedwards@mysite.com",
    "name": "Steve Edwards",
<strong>    "alias": "author/steve-edwards"</strong>
}</pre>
<p>A similar case is a taxonomy term from a term reference field. Here is an example for a Journalists vocabulary:</p>
<pre>class Journalists__1_0 extends ResourceEntity implements ResourceInterface {

  /**
   * {@inheritdoc}
   */
  protected function publicFields() {
    $public_fields = parent::publicFields();

    $public_fields['alias'] = array(
      'property' =&gt; 'tid',
      'process_callbacks' =&gt; array(
        array($this, 'getAlias')
      )
    );

    return $public_fields;
  }

  public function getAlias($value) {
    return drupal_get_path_alias('taxonomy/term/' . $value);
  }
}</pre>
<p>This is very similar to the Author example, with the only difference being that we are passing a term id (tid) instead of a user id. The resulting output is the same:</p>
<pre>{	
  "type": "journalists",
  "id": "1938",
  "attributes": {
    "id": "1938",
    "label": "Jake Tapper",
    "self": "http://mysite.com/api/v1.0/journalists/1938",
    <strong>"alias": "journalists/jake-tapper"</strong>
},<span _fck_bookmark="1" id="cke_bm_458E" style="display: none;"> </span></pre>
<p>As mentioned above, if you are familiar with process callbacks in Form API, then this will look very familiar.</p>
<p>Also, see this <a href="https://github.com/RESTful-Drupal/restful/wiki/Using-process-callbacks-to-modify-field-data" target="_blank" rel="noopener">wiki page</a> I created in the GitHub repo for RESTful for another example of using process callbacks to add the path for an image style derivative.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Creating a RESTful API URL Alias Lookup in Drupal</title>
         <link>http://www.smgaweb.com/blog/creating-restful-api-url-alias-lookup-drupal</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Sun, 06 Dec 2015 01:49:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/creating-restful-api-url-alias-lookup-drupal</guid>
         <description>How to create a URL alias lookup API endpoint in Drupal when using it in a decoupled setup</description>
         <content:encoded>
        <![CDATA[<p>Over the past few months (since Drupalcon LA), I have been actively diving in to the world of decoupled Drupal, in particular using <a href="https://angularjs.org/" target="_blank" rel="noopener">AngularJS</a> as a front end. After going through many tutorials and looking at the <a href="https://www.drupal.org/project/services" target="_blank" rel="noopener">multiple</a> <a href="https://www.drupal.org/project/restws" target="_blank" rel="noopener">options</a> for providing an API from Drupal to be consumed by Angular, I settled on using the <a href="https://drupal.org/project/restful" target="_blank" rel="noopener">RESTful module</a>. One thing I like in particular about it is that you have to explicitly define what you want available in your API, as compared to everything being automatically being made available when the module is enabled.</p>
<p>In addition to the documention in the GitHub wiki and on drupal.org, there is also a really great <a href="https://www.youtube.com/playlist?list=PLZOQ_ZMpYrZv8_c7jd_CkO_93-DnyVFY5" target="_blank" rel="noopener">YouTube video series</a> on how to write your custom classes and perform the tasks you need to do to comsume the API, such as sorting, filtering, and displaying related content.</p>
<h3>Clean URL Mapping</h3>
<p>In Drupal, the most common method of creating URLs is to use <a href="https://www.drupal.org/getting-started/clean-urls" target="_blank" rel="noopener">clean URLs</a> to access data, instead of using the base&nbsp;<em>/node/$nid</em> URL. But, since Drupal uses the base URL for loading the node data, there has to be mapping and lookup functionality for linking the clean URL with the base URL. This is handled with the core Path module, with the mappings being stored in the <em>url_alias</em> table. When a page is accessed with a clean URL, the url_alias table is queried to get the base URL, the node is loaded, and the beat goes on.</p>
<p>However, in looking through all of the different API options and tutorials, I didn't see this clean URL lookup functionality (although I didn't look incredibly hard, so I might have missed something, too). Everything I saw was based on passing an entity ID in a GET URL. Even if I'm using a decoupled front end, I still want to use a clean URL, so I need to be able to take that clean URL, query my API to get the entity id using the clean URL as the search criteria, and then use that value to load the whole entity.</p>
<p>In the process of my searching, I discovered a <a href="https://github.com/RESTful-Drupal/restful/wiki/Creating-a-resource-that-takes-data-from-a-secondary-database" target="_top">wiki page</a> that showed that RESTful has a built in <a href="https://github.com/RESTful-Drupal/restful/blob/7.x-2.x/src/Plugin/resource/DataProvider/DataProviderDbQuery.php" target="_blank" rel="noopener">class</a> (a data provider in RESTful lingo) that allows you to query a database table directly. This was exactly what I needed.</p>
<h3>The Code</h3>
<p>Creating a RESTful plugin for querying a table is very similar to creating a plugin for getting entity information; you have to first <a href="https://github.com/RESTful-Drupal/restful/wiki/1.-Defining-your-own-RESTful-plugin-(2.x)" target="_blank" rel="noopener">define your plugin</a>, and then <a href="https://github.com/RESTful-Drupal/restful/wiki/2.-Defining-the-public-fields-(2.x)" target="_blank" rel="noopener">define your public fields</a> (the fields you want available as part of the API).</p>
<p>Here's the final code for my plugin:</p>
<pre>/**
 * Contains \Drupal\restful_tutorial\Plugin\resource\url_alias
 */

namespace Drupal\restful_tutorial\Plugin\resource\url_alias;

use Drupal\restful\Plugin\resource\ResourceDbQuery;
use Drupal\restful\Plugin\resource\ResourceInterface;

/**
 * Class URLAlias__1_0
 * @package Drupal\restful_tutorial\Plugin\resource\url_alias
 *
 * @Resource(
 *   name = "urlalias:1.0",
 *   resource = "urlalias",
 *   label = "URL Alias",
 *   description = "Gets the entity id from a URL alias.",
 *   authenticationTypes = TRUE,
 *   authenticationOptional = TRUE,
 *   dataProvider = {
 *     "tableName": "url_alias",
 *     "idColumn": "alias",
 *     "primary": "pid",
 *     "idField": "alias",
 *   },
 *   majorVersion = 1,
 *   minorVersion = 0,
 *   class = "URLAlias__1_0"
 * )
 */

class URLAlias__1_0 extends ResourceDbQuery implements ResourceInterface {

  /**
   * {@inheritdoc}
   */
  protected function publicFields() {
    $public_fields['pid'] = array(
      'property' =&gt; 'pid'
    );

    $public_fields['source'] = array(
      'property' =&gt; 'source'
    );

    $public_fields['alias'] = array(
      'property' =&gt; 'alias'
    );

    return $public_fields;
  }
}
</pre>
<p>Here are the key points from the code:</p>
<ul>
<li>The <a href="https://www.drupal.org/node/1882526" target="_blank" rel="noopener">annotations</a> are what provides the details about the table source.</li>
<li>The idColumn value is the database field to which the value passed in the URL will be compared.</li>
</ul>
<p>I then query the api endpoint with the following URL:</p>
<pre>http://mysite/api/urlalias/steve-edwards/2015/12/05/my-blog-title</pre>
<p>and the resulting JSON output looks like this:</p>
<pre>{
  "data": {
    "type": "urlalias",
    "id": "154177",
    "attributes": {
      "pid": "154177",
      "source": "node/97783",
      "alias": "steve-edwards/2015/12/05/my-blog-title"
    },
    "links": {
      "self": "http://mysite/api/v1.0/urlalias/154177"
    }
  },
  "links": {
    "self": "http://mysite/api/v1.0/urlalias/steve-edwards/2015/12/05/my-blog-title"
  }
}
</pre>
<p>From there, you can get the id value, and then call your appropriate endpoint to get the full entity.</p>
<p>This is a pretty basic use of the DataProviderDbQuery class, but it's a very powerful tool that allows you to create and expose custom queries as a RESTful API endpoint as needed for a decoupled setup.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Custom Image Search with Solr, Filefield Sources, and Ctools - Part 3</title>
         <link>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-3</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Mon, 21 Sep 2015 21:47:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-3</guid>
         <description>In our quest to build a custom image search functionality, we are at the last two steps. We have the ability to display a modal window, search for images, and page through the results; now we just need to be able to write the file name and file id back to the source text field so it can be added to the node. Also, in the last post, we just glossed over the code that actually gathers the search parameters and searches Solr, so we will cover that code in detail here.</description>
         <content:encoded>
        <![CDATA[<p>In our quest to build a custom image search functionality (see parts <a href="http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-1" target="_blank" rel="noopener">I</a> and <a href="http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-2" target="_blank" rel="noopener">II</a>), we are at the last two steps. We have the ability to display a modal window, search for images, and page through the results; now we just need to be able to write the file name and file id back to the source text field so it can be added to the node. Also, in the last post, we just glossed over the code that actually gathers the search parameters and searches Solr, so we will cover that code in detail here.</p>
<h3>Writing the selected image to the field</h3>
<p>First, let's look at the Select link that is displayed for each image back in our page callback:</p>
<pre>$rows[] = array(
  'image' =&gt; $styled_image,
  'name' =&gt; $image['filename'],
  'add' =&gt; ctools_ajax_text_button("select", "imgsearch/nojs/imgadd/" . $fid . '/' . $next_field_id, t('Select')),
);
</pre>
<p>The <a href="http://cgit.drupalcode.org/ctools/tree/includes/ajax.inc#n24" target="_blank" rel="noopener">ctools_ajax_text_button()</a> wrapper function just includes the core ajax.inc file and adds the <em>use-ajax</em> class to the link. $fid is the file ID for the image (returned from Solr), and $next_field_id is the number of the element in the field collection.</p>
<p>The menu item for this function is:</p>
<pre>$items['imgsearch/%ctools_js/imgadd/%/%'] = array(
  'title' =&gt; 'Add Image',
  'page callback' =&gt; 'nb_image_search_add_image',
  'page arguments' =&gt; array(1, 3, 4),
  'access callback' =&gt; TRUE,
  'type' =&gt; MENU_CALLBACK,
);
</pre>
<p>And the corresponding function to add the image:</p>
<pre>/**
 *  Add the selected image to field_images
 */
function nb_image_search_add_image($js, $fid, $id) {
  if (!$js) {
    // We don't support degrading this from js because we're not
    // using the server to remember the state of the table.
    return MENU_ACCESS_DENIED;
  }
  ctools_include('ajax');
  ctools_include('modal');
  ctools_include('object-cache');

  //Get file name and selector from ctools object cache.
  $cache = ctools_object_cache_get('imgsearch', 'imgsearch_' . $id);

  $filename = $cache-&gt;fids[$fid];
  $imageurl = $filename .' [fid:' . $fid . ']';
  $url_selector = $cache-&gt;fieldname['url'];
  $person = $cache-&gt;meta[$fid]['person'];
  $person_selector = $cache-&gt;fieldname['person'];
  $organization = $cache-&gt;meta[$fid]['organization'];
  $organization_selector = $cache-&gt;fieldname['organization'];
  $year = $cache-&gt;meta[$fid]['year'];
  $year_selector = $cache-&gt;fieldname['year'];

  $ajax_commands = array();
  // Tell the browser to close the modal.
  $ajax_commands[] = ctools_modal_command_dismiss();
  // Add our custom insertImagePath command to the commands array.
  $ajax_commands[] = array
  (
    // The command will be used in our JavaScript file (see next section)
    'command' =&gt; 'insertImagePath',
    // We pass the field name and the image URL returned from the modal window.
    'url_selector' =&gt; $url_selector,
    'imageurl' =&gt; $imageurl,
    'person_selector' =&gt; $person_selector,
    'person' =&gt; $person,
    'organization_selector' =&gt; $organization_selector,
    'organization' =&gt; $organization,
    'year_selector' =&gt; $year_selector,
    'year' =&gt; $year,
  );

  // Clear ctools cache to avoid any possible conflicts on the same node.
  ctools_object_cache_clear('imgsearch', 'imgsearch_' . $id);

  print ajax_render($ajax_commands);
}
</pre>
<p>This is where the image data cached in the ctools_object_cache comes into play. Based on the $next_field_id passed by the modal window link, we get the image data and build an array to pass to our ajax command that will write the image name and file ID to the source field. Once the $ajax_commands array has been created and filled, we make sure to clear out the ctools object cache to avoid conflicts.</p>
<p>The sharp-eyed person will notice in the code above that the Drupal ajax command - insertImagePath - is a custom command. This is because for some reason, the core Drupal ajax code does not include a function that uses the jQuery <a href="http://api.jquery.com/val/" target="_blank" rel="noopener">val()</a> method to write a value to a text field. The command looks like this:</p>
<pre>/**
 * Add custom behaviors to Drupal.ajax.prototype.commands
 */
(function($, Drupal)
{
  // Our function name is prototyped as part of the Drupal.ajax namespace, adding to the commands:
  Drupal.ajax.prototype.commands.insertImagePath = function(ajax, response, status)
  {
    // The values we passed in our Ajax callback function will be available inside the
    // response object.
    $(response.url_selector).val(response.imageurl);
    $(response.person_selector).val(response.person);
    $(response.organization_selector).val(response.organization);
    $(response.year_selector).val(response.year);
    // Set focus on field.
    $(response.url_selector).focus();
  };
}(jQuery, Drupal));
</pre>
<p>So the result is that when the Select link in the modal window is clicked, the modal is closed, and the image name and fid are written to the filefield source text field, like so.</p>
<pre>my_image_file.jpg [fid:12345]
</pre>
<h3>Indexing image data into Solr</h3>
<p>Finally, we are to the Solr functionality. Before we can search Solr for the image data, we need to index it, and we need to add some custom indexing specifically for images. To do that, we use <a href="http://cgit.drupalcode.org/apachesolr/tree/apachesolr.api.php#n425" target="_blank" rel="noopener">hook_apachesolr_index_document_build_ENTITY_TYPE()</a>.</p>
<pre>/**
 * Implementation of hook_apachesolr_index_document_build_ENTITY_TYPE
 *
 * @param ApacheSolrDocument $document
 * @param $entity
 * @param $env_id
 */
function nb_image_search_apachesolr_index_document_build_node(ApacheSolrDocument $document, $entity, $env_id) {
  if ($entity-&gt;type == 'blog') {
    if (isset($entity-&gt;field_images['und'][0])) {
      $imagefield = entity_load('field_collection_item', ($entity-&gt;field_images['und'][0]));
      foreach($imagefield as $key =&gt; $image)  {
        if(isset($image-&gt;field_image[LANGUAGE_NONE])) {
           // Since they will be search on the image name, we don't want the file extension indexed, so
          // we remove it here.
          $uri  = $image-&gt;field_image[LANGUAGE_NONE][0]['uri'];
          // Index the entire image URL
          $document-&gt;addField('sm_field_image', $uri);
          $fid = $image-&gt;field_image[LANGUAGE_NONE][0]['fid'];
          $length = strrpos($uri, '.') - strrpos($uri, '/') - 1;
          $document-&gt;addfield('tom_image_name', substr($uri, strrpos($uri, '/') + 1, $length));
          // But we still need the file name with the extension, so we use a separate field for that.
          $document-&gt;addfield('sm_image_name_ext', substr($uri, strrpos($uri, '/') + 1));
          $document-&gt;addField('sm_image_path', substr($uri, 0, strrpos($uri, '/') + 1));
          $document-&gt;addfield('im_image_fid', $fid);
          // Also index person, organization, and year fields.
          foreach (array('person', 'organization') as $field) {
            $field_name = 'field_' . $field;
            if (isset($image-&gt;{$field_name}[LANGUAGE_NONE][0])) {
              $document-&gt;addField('tom_image_' . $field, $image-&gt;{$field_name}[LANGUAGE_NONE][0]['value']);
            }
            if (!empty($image-&gt;field_year)) {
              $document-&gt;addField('im_image_year', $image-&gt;field_year[LANGUAGE_NONE][0]['value']);
            }
          }
        }
      }
    }
    // There is no image for this node, so get the default image for field_image and index it.
    else{
      $default_file = nb_image_search_get_image_default();
      $default_image_uri = $default_file-&gt;uri;
      $document-&gt;addField('sm_field_image', $default_image_uri);
    }
  }
}
</pre>
<p>And here is the referenced helper function to get the default image.</p>
<pre>/**
 * Helper function to get the path for the default image.
 *
 * @return default file uri
 */
function nb_image_search_get_image_default() {
  // First, see if the path is already cached.s
  $default_file = &amp;drupal_static(__FUNCTION__);
  if (!isset($default_image_uri)) {
    if ($cache = cache_get('field_image_default')) {
      $default_file = $cache-&gt;data;
    }
    // Not cached, so we need to generate it this time through.
    else {
      $field = field_info_field('field_images');
      $instance = field_info_instance('field_collection_item', 'field_image', 'field_images');
      // Load the file object.
      $default_file = file_load($instance['settings']['default_image']);
      // Cache the file object.
      cache_set('field_image_default', $default_file, 'cache');
    }
  }

  return $default_file;
}
</pre>
<p>All we are doing here is splitting out the image information into smaller parts and indexing them into separate fields in the Solr index, and then also indexing data from the Person, Organization, and Year fields if present.</p>
<h3>Searching Solr for images</h3>
<p>And finally, we are to the code to actually search Solr. The Apache Solr Search module is currently a requirement, but only a very small portion of it is needed. The D7 version of the module includes a forked version of the <a href="https://github.com/PTCInc/solr-php-client" target="_blank" rel="noopener">Solr PHP Client library</a>, and that's most of what we will be using. The majority of the Apache Solr Search module code handles the Drupal side of things (pages, blocks, facets, etc), and since we are just doing a quick search behind the scenes, we don't need all of that code; we just need the PHP client to directly access Solr (except for one function).</p>
<pre>/**
 * Worker function to search for images.
 */
function nb_image_search_search($values, $page_num = 0) {
  // Call solr directly, since we don't need all of the other Drupal stuff
  // to be done once we get the results.
  $query = apachesolr_drupal_query('apachesolr', array(), '', '');

  if (isset($values['search_terms'])) {
    $keys = str_replace("%20", " ", $values['search_terms']);
  }
  else {
    $keys = '';
  }

  // Limit results to only records that have a value for tom_field_images.
  $query-&gt;addParam('fq', 'tom_image_name:[* TO *]');

  //Need to add fields to be searched to qf param.
  $query-&gt;addParam('qf', 'tom_image_name');

  foreach (array('person', 'organization') as $field) {
    $field_name = 'search_' . $field;
    if($values[$field_name] !== '') {
      $query-&gt;addFilter('sm_image_' . $field, $values['search_' . $field]);
    }
  }
  if ($values['search_year'] != '') {
    $query-&gt;addFilter('im_image_year', $values['search_year']);
  }

  // Get current params
  $rows = $query-&gt;getParam('rows');
  // Set $start param from pager
  $start = $rows * $page_num;
  $query-&gt;replaceParam('start', $start);

  // Specify the fields to be returned in the search results.
  $query-&gt;addParam('fl', 'sm_image_path');
  $query-&gt;addParam('fl', 'sm_image_name_ext');
  $query-&gt;addParam('fl', 'im_image_fid');
  $query-&gt;addParam('fl', 'sm_image_person');
  $query-&gt;addParam('fl', 'sm_image_organization');
  $query-&gt;addParam('fl', 'im_image_year');

  $response = $query-&gt;search($keys);
  $results = json_decode($response-&gt;data);

  // Loop through results and put them in an array for the modal form building function.
  if (count($results-&gt;response-&gt;docs)) {
    foreach ($results-&gt;response-&gt;docs as $result) {
      $filepath = $result-&gt;sm_image_path[0];
      $filename = $result-&gt;sm_image_name_ext[0];
      $fid = $result-&gt;im_image_fid[0];
      $images[] = array(
        'filename' =&gt; $filename,
        'filepath' =&gt; $filepath,
        'fid' =&gt; $fid,
        'person' =&gt; $result-&gt;sm_image_person[0],
        'organization' =&gt; $result-&gt;sm_image_organization[0],
        'year' =&gt; $result-&gt;im_image_year[0],
      );
    }
  }
  else {
    $images = 'No matching results found.';
  }
  $params = $query-&gt;getParams();
  $return['rows'] = $params['rows'];
  $return['images'] = $images;
  $return['total_found'] = $results-&gt;response-&gt;numFound;

  return $return;
}
</pre>
<p>The code above goes through the following steps:</p>
<ol>
<li>Build the default Solr query object.</li>
<li>Get the search terms.</li>
<li>Specify fields to be searched and values to search with.</li>
<li>Specify the offset to be returned as determined by the pager value.</li>
<li>Run the query.</li>
<li>Put the results into an array, and return them to the page callback.</li>
</ol>
<p>And that's it. You now have a functional custom image search.</p>
<p>A few notes about this module:</p>
<ul>
<li>As it currently stands, it is fairly specific to my specific setup of four fields in a field collection. However, it should be fairly simple to modify it to fit your needs, including just using it with a simple image field by itself.</li>
<li>Since a <a href="https://drupal.org/project/multifield" target="_blank" rel="noopener">Multifield</a> uses an identical field structure as Field Collection, this also works with multifields.</li>
<li>It is pretty easy to see extending this to include admin settings where you can specify the fields(s) where you want to add this. Creating it as a Filefield Source already allows you to do that, but you also have to know the field structure to pass to the ajax code.</li>
</ul>
<p>This post will be updated when I have put it on drupal.org as a project.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Custom Image Search with Solr, Filefield Sources, and Ctools - Part 2</title>
         <link>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-2</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Mon, 21 Sep 2015 16:35:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-2</guid>
         <description>Continuing on from my previous post , we are working on building a custom image search in Drupal using Apache Solr, Filefield Sources, and Ctools. So far, we have created our custom FileField Source element that will allow us to search. Now we get to the step of creating our ...</description>
         <content:encoded>
        <![CDATA[<p>Continuing on from my <a href="http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-1" target="_blank" rel="noopener">previous post</a>, we are working on building a custom image search in Drupal using Apache Solr, Filefield Sources, and Ctools. So far, we have created our custom FileField Source element that will allow us to search. Now we get to the step of creating our modal window for displaying the search form and search results. As mentioned in the first post, our filefield source link has the <em>ctools-use-modal </em>class, which will cause the link to open our page callback in a modal window. The first thing we need is a menu item for our page callback:</p>
<pre>/**
 * Implementation of hook_menu.
 */
function nb_image_search_menu() {
  $items = array();

  $items['imgsearch/%ctools_js/%/%'] = array(
    'page callback' =&gt; 'imgsearch_page',
    'page arguments' =&gt; array(1, 2, 3),
    'access callback' =&gt; TRUE,
    'type' =&gt; MENU_CALLBACK,
  );

  return $items;
}
</pre>
<p>And here is the form function for our search form:</p>
<pre>/**
 * Drupal form to be put in a modal.
 */
function imgsearch_form($form, $form_state, $id) {
  $form = array();

  $form['search_terms'] = array(
    '#type' =&gt; 'textfield',
    '#title' =&gt; t('Search text'),
    '#description' =&gt; t('Enter the search terms'),
    '#required' =&gt; TRUE,
    '#default_value' =&gt; $form_state['values']['search_terms'],
  );

  $form['filter_fields'] = array(
    '#type' =&gt; 'fieldset',
    '#title' =&gt; t('Additional Filters'),
    '#collapsible' =&gt; TRUE,
    '#collapsed' =&gt; TRUE,
  );

  $form['filter_fields']['search_person'] = array(
    '#type' =&gt; 'textfield',
    '#title' =&gt; t('Person'),
    '#description' =&gt; t('The name of the person in the image.'),
    '#default_value' =&gt; isset($form_state['values']['search_person']) ? $form_state['values']['search_person'] : '',
  );

  $form['filter_fields']['search_organization'] = array(
    '#type' =&gt; 'textfield',
    '#title' =&gt; t('Organization'),
    '#description' =&gt; t('The organization the subject belongs to.'),
    '#default_value' =&gt; isset($form_state['values']['search_organization']) ? $form_state['values']['search_organization'] : '',
  );

  $form['filter_fields']['search_year'] = array(
    '#type' =&gt; 'textfield',
    '#title' =&gt; t('Year'),
    '#description' =&gt; t('Enter the year of the image'),
    '#size' =&gt; 4,
    '#default_value' =&gt; isset($form_state['values']['search_year']) ? $form_state['values']['search_year'] : '',
  );

  $form['submit'] = array(
    '#type' =&gt; 'submit',
    '#value' =&gt; t('Submit'),
  );

  return $form;
}</pre>
<p>As you can see this is just a pretty standard form with fields for the search criteria. However, one thing that is unique in this case is there is no submit function. This is because as we will see in a bit, when using a pager, the search functionality has to be &nbsp;called in the page function. So what happens in this case is that since there is no submit (or validation function), it just goes right back to the page function.</p>
<p>Here is the page function:</p>
<pre>/**
 * Page callback for imgsearch modal popup window.
 */
function imgsearch_page($ajax, $id, $bundle) {
  if ($ajax) {
    //Load the modal library and add the modal javascript.
    ctools_include('ajax');
    ctools_include('modal');

    $form_state = array(
      'ajax' =&gt; TRUE,
      'title' =&gt; t('Image Search Form'),
      'next_field_id' =&gt; $id,
    );

    // Use ctools to generate ajax instructions for the browser to create
    // a form in a modal popup.
    $search_form = ctools_modal_form_wrapper('imgsearch_form', $form_state);

    if ($form_state['executed'] || $_GET['page'] || $_GET['pager_source']) {
      if ($form_state['values']['search_terms'] != '' || isset($_GET['search_terms'])) {
        // The search fields have no value in $form_state['values'] when this is paged, so we add them from $_GET[].
        foreach(array('terms', 'person', 'organization', 'year') as $search_field) {
          $search_field_name = 'search_' . $search_field;
          if ($_GET[$search_field_name]) {
            $form_state['values'][$search_field_name] = $_GET[$search_field_name];
          }
        }

        // $form_state['executed'] = TRUE only when the form has been submitted, which means we are on the first page.
        if ($form_state['executed']) {
          $page_num = 0;
        }
        // The form was not submitted this time, so determine what page of results we are on. $_GET['page'] is only set for pages past
        // the first page.
        else {
          $page_num = isset($_GET['page']) ? $_GET['page'] : 0;
        }


        // Search Solr for images.
        $results = nb_image_search_search($form_state['values'], $page_num);
        // Loop through returned images and create a table.
        if (is_array($results['images']) &amp;&amp; count($results['images'] &gt; 0)) {
          $next_field_id = $form_state['next_field_id'];
          // Create object to store file and target field info. To be stored in ctools cache.
          $file_info = new stdClass();
          // Generate the field name.  The bundle is the first part of the field name for a field collection, so we just need the next available option
          // in the $field_images['und'] array. The second number (for field_image) will always be 0 since it is a single value field.

          // First, get field information for the field collection.
          $instances = field_info_instances('field_collection_item', $bundle);
          // Find the image field in the collection (assuming there is only one) and get the name.
          foreach ($instances as $instance_name =&gt; $instance_settings) {
            if ($instance_settings['widget']['module'] == 'image') {
              $image_field_name = str_replace('_', '-', $instance_name);
              break;
            }
          }

          // Replace underscores with dashes in $bundle.
          $bundle = str_replace('_', '-', $bundle);

          // ctools caching requires an object, not an array.
          $file_info = new stdClass();
          $file_info-&gt;fieldname['url'] = '#edit-' . $bundle . '-und-' . $next_field_id . '-' . $image_field_name . '-und-0-imgsearch-file-url';
          $file_info-&gt;fieldname['person'] = '#edit-' . $bundle . '-und-' . $next_field_id . '-field-person-und-0-value';
          $file_info-&gt;fieldname['organization'] = '#edit-' . $bundle . '-und-' . $next_field_id . '-field-organization-und-0-value';
          $file_info-&gt;fieldname['year'] = '#edit-' . $bundle . '-und-' . $next_field_id . '-field-year-und-0-value';

          $file_info-&gt;fids = array();

          // Theme the results as a table.
          $header = array(t('Image'), t('File Name'), t('Add to field'));
          $rows = array();
          foreach ($results['images'] as $image) {
            // Create image style derivative for each image.
            $imagestyle = array(
              'style_name' =&gt; 'thumbnail',
              'path' =&gt; $image['filepath'] . $image['filename'],
              'width' =&gt; '',
              'height' =&gt; '',
              'alt' =&gt; '',
              'title' =&gt; $image['filename'],
            );
            $styled_image = theme('image_style', $imagestyle);
            $fid = $image['fid'];

            $rows[] = array(
              'image' =&gt; $styled_image,
              'name' =&gt; $image['filename'],
              'add' =&gt; ctools_ajax_text_button("select", "imgsearch/nojs/imgadd/" . $fid . '/' . $next_field_id, t('Select')),
            );

            $file_info-&gt;fids[$fid] = $image['filename'];

            // Cache values for Person, Organization, and Year if they exist.
            foreach (array('person', 'organization', 'year') as $field) {
              if (isset($image[$field])) {
                $file_info-&gt;meta[$fid][$field] = $image[$field];
              }
            }
          }
          //Cache image name in ctools object cache so it can be used later in nb_image_search_image_add().
          ctools_include('object-cache');
          ctools_object_cache_set('imgsearch', 'imgsearch_' . $next_field_id, $file_info);

          // Create a render array ($build) which will be themed as a table with a
          // pager.
          $build['search_form'] = isset($search_form[0]) ? drupal_build_form('imgsearch_form', $form_state) : $search_form;
          $build['imgsearch_table'] = array(
            '#theme' =&gt; 'table',
            '#header' =&gt; $header,
            '#rows' =&gt; $rows,
            '#empty' =&gt; t('There were no matching results found'),
          );

          // Attach the pager theme.
          $pager = pager_default_initialize($results['total_found'], $results['rows']);
          $build['imgsearch_pager'] = array(
            '#theme' =&gt; 'pager',
            '#parameters' =&gt; array(
              'search_terms' =&gt; $form_state['values']['search_terms'],
              ),
          );

          // Add values for person, organization, and year to $build['imgsearch_pager']['#parameters'] if they are set.
          foreach (array('person','organization', 'year') as $search_field) {
            $search_field_name = 'search_' . $search_field;
            if ($form_state['values'][$search_field_name] != '' &amp;&amp; !isset($_GET[$search_field_name])) {
              $build['imgsearch_pager']['#parameters'][$search_field_name] = $form_state['values'][$search_field_name];
            }
            elseif (isset($_GET[$search_field_name])) {
              $build['imgsearch_pager']['#parameters'][$search_field_name] = $_GET[$search_field_name];
            }
          }

          // Set this to check for when coming back to the first page from a later page in the
          // paged search results. We have to do this because the pager itself has no indication of
          // where it is coming from when returning to the front page.
          if ($page_num &gt; 0) {
            $build['imgsearch_pager']['#parameters']['pager_source'] = TRUE;
          }

          $form_state['values']['title'] = t('Search Results - Page !page', array('!page' =&gt; $pager + 1));
          $output = ctools_modal_form_render($form_state['values'], $build);

          print ajax_render($output);
          drupal_exit();
        }
       else {
          $build['no_results'] = array(
            'markup' =&gt; '&lt;div class="no-results&gt;No images found&lt;/div&gt;',
          );
        }
      }
    }
    elseif (!isset($output)) {
      $output = ctools_modal_form_wrapper('imgsearch_form', $form_state);
      // Return the ajax instructions to the browser via ajax_render().
      print ajax_render($output);
      drupal_exit();
    }
  }
  else {
    return drupal_get_form('imgsearch_form', $id);
  }
}</pre>
<p>There is a lot going on here, so I won't cover everything in detail, but I'll hit the high points.</p>
<p>First, we check to see if we're using ajax, and then include the required code to use ctools and ajax. We also build our search form using ctools_modal_form_wrapper().</p>
<pre>if ($ajax) {
    //Load the modal library and add the modal javascript.
    ctools_include('ajax');
    ctools_include('modal');

    $form_state = array(
      'ajax' =&gt; TRUE,
      'title' =&gt; t('Image Search Form'),
      'next_field_id' =&gt; $id,
    );

    // Use ctools to generate ajax instructions for the browser to create
    // a form in a modal popup.
    $search_form = ctools_modal_form_wrapper('imgsearch_form', $form_state);
</pre>
<p>Next we determine if we even search, or just render the search form by itself, because when the modal is first rendered, we only want to render the form, since we haven't entered any search criteria yet.</p>
<pre>if ($form_state['executed'] || $_GET['page'] || $_GET['pager_source']) {
</pre>
<p>These three conditions are used because they tell us when either a search has been done or if we are paging through results:</p>
<ul>
<li><strong>$form_state['executed'] </strong>- This is only set when the form has been submitted with the Submit button.</li>
<li><strong>$_GET['page']</strong> - This value is set by the pager, but only on pages other than the first.</li>
<li><strong>$_GET['pager_source']</strong> - Manually set later in the function to tell us when we're on the first page coming from a later page, since $_GET['page'] isn't set in that case and $form_state['executed'] isn't set since the form wasn't just submitted.</li>
</ul>
<p>We then set the $page_num &nbsp;value itself, based on the variables detailed above.</p>
<pre>// $form_state['executed'] = TRUE only when the form has been submitted, which means we are on the first page.
if ($form_state['executed']) {
  $page_num = 0;
}
// The form was not submitted this time, so determine what page of results we are on. $_GET['page'] is only set for pages past
// the first page.
else {
  $page_num = isset($_GET['page']) ? $_GET['page'] : 0;
}</pre>
<p>$page_num is used in the search function itself to determine which range of images to get from Solr.</p>
<p>The next section calls the search function and formats the results in a table format. There is a lot going on, but the one thing I want to point out is the use of Ctools object caching.</p>
<pre>// ctools caching requires an object, not an array.
$file_info = new stdClass();
$file_info-&gt;fieldname['url'] = '#edit-' . $bundle . '-und-' . $next_field_id . '-' . $image_field_name . '-und-0-imgsearch-file-url';
$file_info-&gt;fieldname['person'] = '#edit-' . $bundle . '-und-' . $next_field_id . '-field-person-und-0-value';
$file_info-&gt;fieldname['organization'] = '#edit-' . $bundle . '-und-' . $next_field_id . '-field-organization-und-0-value';
$file_info-&gt;fieldname['year'] = '#edit-' . $bundle . '-und-' . $next_field_id . '-field-year-und-0-value';

$file_info-&gt;fids = array();

// Theme the results as a table.
$header = array(t('Image'), t('File Name'), t('Add to field'));
$rows = array();
foreach ($results['images'] as $image) {
  // Create image style derivative for each image.
  $imagestyle = array(
    'style_name' =&gt; 'thumbnail',
    'path' =&gt; $image['filepath'] . $image['filename'],
    'width' =&gt; '',
    'height' =&gt; '',
    'alt' =&gt; '',
    'title' =&gt; $image['filename'],
  );
  $styled_image = theme('image_style', $imagestyle);
  $fid = $image['fid'];

  $rows[] = array(
    'image' =&gt; $styled_image,
    'name' =&gt; $image['filename'],
    'add' =&gt; ctools_ajax_text_button("select", "imgsearch/nojs/imgadd/" . $fid . '/' . $next_field_id, t('Select')),
  );

  $file_info-&gt;fids[$fid] = $image['filename'];

  // Cache values for Person, Organization, and Year if they exist.
  foreach (array('person', 'organization', 'year') as $field) {
    if (isset($image[$field])) {
      $file_info-&gt;meta[$fid][$field] = $image[$field];
    }
  }
}
//Cache image name in ctools object cache so it can be used later in nb_image_search_image_add().
ctools_include('object-cache');
ctools_object_cache_set('imgsearch', 'imgsearch_' . $next_field_id, $file_info);</pre>
<p>I need to pass a lot of data from this function to my function that writes image data back to the source field on the node form, so this is the best way to do it without creating a huge link to pass everything through the URL. Later on we'll see where I get the data from this cache and clear it out.</p>
<p>The rest of the function builds a render array and adds the pager links. A key part of this is adding parameters to the pager links so that we can access them later in our search function.</p>
<pre>// Attach the pager theme.
$pager = pager_default_initialize($results['total_found'], $results['rows']);
$build['imgsearch_pager'] = array(
  '#theme' =&gt; 'pager',
  '#parameters' =&gt; array(
    'search_terms' =&gt; $form_state['values']['search_terms'],
  ),
);

// Add values for person, organization, and year to $build['imgsearch_pager']['#parameters'] if they are set.
foreach (array('person','organization', 'year') as $search_field) {
  $search_field_name = 'search_' . $search_field;
  if ($form_state['values'][$search_field_name] != '' &amp;&amp; !isset($_GET[$search_field_name])) {
    $build['imgsearch_pager']['#parameters'][$search_field_name] = $form_state['values'][$search_field_name];
  }
  elseif (isset($_GET[$search_field_name])) {
    $build['imgsearch_pager']['#parameters'][$search_field_name] = $_GET[$search_field_name];
  }
}

// Set this to check for when coming back to the first page from a later page in the
// paged search results. We have to do this because the pager itself has no indication of
// where it is coming from when returning to the front page.
if ($page_num &gt; 0) {
  $build['imgsearch_pager']['#parameters']['pager_source'] = TRUE;
}
</pre>
<p>Note that this is also where we set $_GET['pager_source'] so that we can tell if we are coming back to the first page of results from a later page.</p>
<p>One thing to note here is that in order for the pager links to work within the modal, they need to have the ctools-use-modal class applied to them. The theme_pager_link() function accepts attributes to apply to the links, but for some strange reason, the functions that actually call theme_pager_link() (e.g. theme_pager_previous(), theme_pager_next(), and theme_pager_last()) don't bother to actually pass attributes, so there's no way to get them to the link theme function. Sigh. To get around that, I created a simple jQuery behavior that adds the class to the pager links:</p>
<pre>(function ($) {
  Drupal.behaviors.addCtoolsModalClass = {
    attach: function (context) {
        $('#modalContent .pagination ul li a').addClass('ctools-use-modal');
        $('#modalContent .pagination ul li a').addClass('ctools-modal-imgsearch-modal-style');
    }
  }
})(jQuery);</pre>
<p>Finally, we set the modal window title with the page number so the user can tell which page he is on, send the output to the page, and exit.</p>
<pre>$form_state['values']['title'] = t('Search Results - Page !page', array('!page' =&gt; $pager + 1));
  $output = ctools_modal_form_render($form_state['values'], $build);

  print ajax_render($output);  
  drupal_exit();
}
</pre>
<p>And last, but not least, our else condition for when there are no search results to display, and we just want to display the form.</p>
<pre>elseif (!isset($output)) {
  $output = ctools_modal_form_wrapper('imgsearch_form', $form_state);
  // Return the ajax instructions to the browser via ajax_render().
  print ajax_render($output);
  drupal_exit();
}
</pre>
<p>So now, all that's left to do is our search function, and code to take the selected item from the modal and write it back to the filefield source field on the node form. All that will be done in the <a href="http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-3" target="_blank" rel="noopener">final post</a> in this series.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Custom Image Search with Solr, Filefield Sources, and Ctools - Part 1</title>
         <link>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-1</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Sun, 20 Sep 2015 00:02:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-1</guid>
         <description>Part I of creating a tool for storing and browsing existing images stored in Apache Solr from a Drupal 7 site.</description>
         <content:encoded>
        <![CDATA[<p>Over the years I have been a big fan and user of Apache Solr for search on Drupal sites, in particular using the <a href="https://drupal.org/project/apachesolr" target="_blank" rel="noopener">Apache Solr Search set of modules</a>, mostly because of its speed and ability to create facets for drilling down into search results, along with the flexibilty provided by the module API to customize the functionality as needed. While re-building the <a href="http://newsbusters.org" target="_blank" rel="noopener">NewsBusters</a> site from scratch in D7 last year, one of the issues that the users wanted to address was finding existing images to re-use in blog posts. The existing functionality used a file browsing functionality in the WYSIWYG, and as the number of images added to the site grew, just loading the browser to search for an image could take a few minutes. Another change we were making in the D7 version was storing all of our images on Amazon S3 (using the <a href="https://drupal.org/project/s3fs" target="_blank" rel="noopener">S3FS module</a>), so I figured that I would take this chance to create a custom image search that used Solr. This would address speed issues, and would also allow the users to index metadata about images that could be used later to search for images.</p>
<p>One module that I have used in the past that allows some image searching functionality is <a href="https://drupal.org/project/filefield_sources" target="_blank" rel="noopener">Filefield Sources</a>. For instance, it allows you to get images from remote URLs, search the files directory with an autocomplete textfield, and attach from a server directory. However, I needed to do something custom, and fortunately, the module implements <a href="http://cgit.drupalcode.org/filefield_sources/tree/filefield_sources.api.php" target="_blank" rel="noopener">hooks</a> that allows you to create your own custom source. Fortunately the Autocomplete reference field does what I already need to do once I get the image name and fid, so all I need to do is create my search functionality and write the approriate values to the text field.</p>
<p>As I mentioned above, I also wanted to index metadata about the image to allow for better searching, so I needed to find a way to store that data. I initially tried extending the image field in code (with no luck), so after looking at the avaialable options, I settled on the <a href="https://drupal.org/project/field_collection" target="_blank" rel="noopener">Field Collection</a> module. This allowed me to create a group of four fields for each image:</p>
<ul>
<li>Image</li>
<li>Person</li>
<li>Organization</li>
<li>Year</li>
</ul>
<p>So to start out, I create a custom module (nb_image_search), and I declare my custom source using <a href="http://(http//cgit.drupalcode.org/filefield_sources/tree/filefield_sources.api.php#n33" target="_blank" rel="noopener">hook_filefield_sources_info()</a>:</p>
<pre>/**
 * Implements hook_filefield_sources_info().
 */
function nb_image_search_filefield_sources_info() {
  $source = array();
  $source['imgsearch'] = array(
    'name' =&gt; t('Image search with Solr'),
    'label' =&gt; t('Image Search'),
    'description' =&gt; t('Search for an existing image using Apache Solr'),
    'process' =&gt; 'nb_image_search_image_search_process',
    'value' =&gt; 'nb_image_search_image_search_value',
    'weight' =&gt; 1,
    'file' =&gt; 'includes/image_search.inc',
  );

  return $source;
}</pre>
<p>The items in this array are:</p>
<ul>
<li><strong>name</strong> - the name of the option displayed in the image field settings for File Sources</li>
<li><strong>label</strong> - The name of the option that is displayed on the node create/edit form.</li>
<li><strong>description</strong> - The description of the source</li>
<li><strong>process</strong> - the name of the process function that does all the heavy-work of creating a form element for searching and populating a field.</li>
<li><strong>value</strong> - This callback function then takes the value of that field and saves the &nbsp;file locally.</li>
<li><strong>weight</strong> - Used for ordering the enabled sources on the node create screen.</li>
<li><strong>file</strong> - The path to the file where the process and value functions are stored.</li>
</ul>
<p>A second hook implementation that is needed is hook_theme():</p>
<pre>/**
 * Implements hook_theme().
 */
function nb_image_search_theme() {
  return array(
    'nb_image_search_image_search_element' =&gt; array(
      'render element' =&gt; 'element',
      'file' =&gt; 'includes/image_search.inc',
    ),
  );
}</pre>
<p>This specifies the theme function that will be used to theme the custom element.</p>
<p>Next up is the process function. This is basically a form function that defines the element for searching.</p>
<pre>define('FILEFIELD_SOURCE_IMGSEARCH_HINT_TEXT', 'example.png [fid:123]');

/**
 * A #process callback to extend the filefield_widget element type.
 */
function nb_image_search_image_search_process($element, &amp;$form_state, $form) {
  $element['imgsearch'] = array(
    '#weight' =&gt; 100.5,
    '#theme' =&gt; 'nb_image_search_image_search_element',
    '#filefield_source' =&gt; TRUE, // Required for proper theming.
    '#filefield_sources_hint_text' =&gt; FILEFIELD_SOURCE_IMGSEARCH_HINT_TEXT,
  );

  $element['imgsearch']['file_url'] = array(
    '#type' =&gt; 'textfield',
    '#maxlength' =&gt; NULL,
  );

  // Handle this being a Field Collection entity.
  if (isset($element['#entity']-&gt;is_new) &amp;&amp; $element['#entity']-&gt;is_new == TRUE) {
    $nid = 0;
  }
  else {
    if (isset($element['#entity']-&gt;nid)) {
      $nid = $element['#entity']-&gt;nid;
    }
    elseif(isset($form_state['node']-&gt;nid)) {
      $nid = $form_state['node']-&gt;nid;
    }
  }

  // Get id for field within the field collection. Use the position of the languge value since it's right before the number.
  $language = $element['#language'];
  $lang_pos = strpos($element['#id'], $language);
  $id = !is_object($element['#file']) ? substr($element['#id'], $lang_pos + strlen($language) + 1, 1) : 0;

  $element['imgsearch']['search'] = array(
    '#type' =&gt; 'markup',
    '#markup' =&gt; '&lt;div id="imgsearch"&gt;' . l("Search for Image", 'imgsearch/nojs/' . $id . '/' . $element['#bundle'], array('attributes' =&gt; array('class' =&gt; 'ctools-use-modal ctools-modal-imgsearch-modal-style'))) . '&lt;/div&gt;'
  );

  $element['imgsearch']['select'] = array(
    '#name' =&gt; implode('_', $element['#array_parents']) . '_imgsearch_select',
    '#type' =&gt; 'submit',
    '#value' =&gt; t('Select'),
    '#validate' =&gt; array(),
    '#submit' =&gt; array('filefield_sources_field_submit'),
    '#name' =&gt; $element['#name'] . '[imgsearch][button]',
    '#limit_validation_errors' =&gt; array($element['#parents']),
    '#ajax' =&gt; array(
      'path' =&gt; 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'],
      'wrapper' =&gt; $element['upload_button']['#ajax']['wrapper'],
      'effect' =&gt; 'fade',
    ),
  );

  return $element;
}</pre>
<p>This creates the links and fields for the source as shown below.<br /><br /><br /><img alt="" height="197" src="http://www.smgaweb.com/sites/default/files/images/nb_image_search_1.gif" title="" width="488" /><br /><br />Most of this is copied from the <a href="http://cgit.drupalcode.org/filefield_sources/tree/sources/reference.inc" target="_blank" rel="noopener">reference source</a>, but there are a couple custom things going on here.</p>
<p>First, a Field Collection is a completely separate entity attached to the node, so it requires some custom code to get the node id.</p>
<p>Second, the $element['imgsearch']['search'] markup is a link to a custom page function (detailed in a later post) that generates the ctools modal window. There are two ctools classes applied to the link:</p>
<ul>
<li><strong>ctools-use-modal</strong> - opens the page callback being called in a modal window</li>
<li><strong>ctools-modal-imgsearch-modal-style</strong> - a class to match <a href="http://cgit.drupalcode.org/ctools/tree/help/modal.html#n61" target="_blank" rel="noopener">custom settings</a>&nbsp;for the modal that &nbsp;defined in hook_node_prepare():</li>
</ul>
<pre>/**
 * Implementation of hook_node_prepare
 */
function nb_image_search_node_prepare($node) {
  if ($node-&gt;type == 'blog') {
    ctools_include('modal');
    ctools_modal_add_js();

    // Add custom settings for form size.
    drupal_add_js(array(
      'imgsearch-modal-style' =&gt; array(
        'modalSize' =&gt; array(
          'type' =&gt; 'fixed',
          'width' =&gt; 1000,
          'height' =&gt; 1200,
        ),
        'animation' =&gt; 'fadeIn',
        'closeText' =&gt; t('Close Search Window'),
        'loadingText' =&gt; t('Loading the Image Search window'),
      ),
    ), 'setting');
  }
}</pre>
<p>When clicked on, this link will open up your modal window. Details of the window content will be covered in the <a href="http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-2" target="_blank" rel="noopener">next post</a>.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Connecting to a remote MS SQL Server database from Mac OS X</title>
         <link>http://www.smgaweb.com/blog/connecting-remote-ms-sql-server-database-mac-os-x</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Sat, 04 Sep 2010 23:16:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/connecting-remote-ms-sql-server-database-mac-os-x</guid>
         <description>Connecting to a remote MS SQL Server database from Mac OS X using FreeTDS and unixODBC</description>
         <content:encoded>
        <![CDATA[<p>As part of an e-commerce site I'm working on, I had a need to update my product data with data for available inventory from a supplier. In this case, the supplier didn't provide a feed, but simply provided access to its MS SQL Server database, which meant that I had to connect to the database, query the table for new and changed records, and determine which records had been deleted. The trick in all this is connecting from a *nix system to a Microsoft database. On a Windows PC, I would just define an ODBC connection, but since that capability does not exist natively in *nix, it needs to be added. After doing some research, I decided to go with unixODBC and FreeTDS. During development I'm working on my MacBook Pro, and since I'm already using MacPorts for my AMP stack, I was just able to install the <a href="http://trac.macports.org/browser/trunk/dports/databases/unixODBC/Portfile" target="_blank" rel="noopener">unixODBC</a> and <a href="http://trac.macports.org/browser/trunk/dports/databases/freetds/Portfile" target="_blank" rel="noopener">FreeTDS</a> ports. Once those are installed, there are three different config files that need to be configured:</p>
<ul>
<li>freetds.conf</li>
<li>odbc.ini</li>
<li>odbcinst.ini</li>
</ul>
<p>The first one that needs to be configured is <span style="font-family: courier new,courier,monospace;">freetds.conf</span>, located in <span style="font-family: courier new,courier,monospace;">/opt/local/etc/freetds</span>, since data in the other two files refers to it.</p>
<pre>[bti_db]
host = 123.45.67.89 (IP address changed to protect the innocent)
port = 14333
tds version = 8.0
</pre>
<p>This contains the info that tells where the database is located. Also, the name of the connection ([bti_db] in this case) is important, because it will be referenced in the odbc ini files.<br /><br />Next is <span style="font-family: courier new,courier,monospace;">odbcinst.ini</span>, located in <span style="font-family: courier new,courier,monospace;">/opt/local/etc</span>:</p>
<pre>[FreeTDS]
Description = TDS Driver (Sybase/MSSQL)
Driver = /opt/local/lib/libtdsodbc.so
Setup = /opt/local/lib/libtdsS.so
FileUsage = 1

[ODBC Drivers]
FreeTDS = Installed
</pre>
<p>As you can see, the two primary files referenced here are <span style="font-family: courier new,courier,monospace;">libtdsodbc.so</span> and <span style="font-family: courier new,courier,monospace;">libtdsS.so</span>. Finally, we need to configure <span style="font-family: courier new,courier,monospace;">odbc.ini</span>, located in <span style="font-family: courier new,courier,monospace;">/opt/local/etc</span>:</p>
<pre>[bti_dsn]
Driver = /opt/local/lib/libtdsodbc.so
Description = BTI Data Database
Trace = no
Servername = bti_db
Database = btidata

[ODBC Data Sources]
bti_dsn = FreeTDS</pre>
<p>Notice how the Servername settings refers to the section of <span style="font-family: courier new,courier,monospace;">freetds.conf</span> detailed above.<br /><br />Now that the server has been configured, we need to write the script.&nbsp; As pointed out in a <a href="http://stackoverflow.com/questions/3252549/iodbc-error-trying-to-connect-to-ms-sql-server-in-php-with-unixodbc-freetds" target="_blank" rel="noopener">response to my pleas for help at Stackoverflow</a>, I had to set two variables (ODBCINSTINI and ODBCINI) to identify the locations for my <span style="font-family: courier new,courier,monospace;">odbc.ini</span> and <span style="font-family: courier new,courier,monospace;">odbcinst.ini</span> files.<br />&nbsp;</p>
<pre>putenv("ODBCINSTINI=/opt/local/etc/odbcinst.ini");

putenv("ODBCINI=/opt/local/etc/odbc.ini");
</pre>
<p>Once that is done, it's just a matter of using the odbc_* functions to connect to the database and pull the data to my local MySQL database.</p>
<pre>$user = 'my_user_id';
$pw = 'my_password';

$conn = odbc_connect("bti_dsn", $user, $pw);

if (!$conn){
    if (phpversion() &lt; '4.0'){
        exit("Connection Failed: . $php_errormsg" );
    }
    else{
        exit("Connection Failed:" . odbc_errormsg() );
    }
}


if ($conn){
  // This query generates a result set with one record in it.
  $sql="SELECT top 100 * FROM inventory";

  # Execute the statement.
  $rs=odbc_exec($conn,$sql);
 
  // Fetch and display the result set value.
  if (!$rs){
      exit("Error in SQL");
  }
  echo "Importing data from BTI...";
  while ($row = odbc_fetch_object($rs)) {

    $sql = "INSERT INTO bti_inventory_temp (item_id, vendor_item_id, upc, ean, item_description, item_text, attribute_keys, attribute_values, image_path, available, msrp,";
    $sql .= " price, current_price, is_on_sale, sale_starts_on, sale_ends_on, is_on_closeout, is_new, manufacturer_id, manufacturer_name, category_id, category_name, sub_category_id,";
    $sql .= " sub_category_name, group_id, group_description, group_text, associated_group_ids, updated_at, is_ormd) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, %d, %d, %d, '%s', '%s', %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d)";
       
    db_query($sql, trim($row-&gt;item_id), $row-&gt;vendor_item_id, $row-&gt;upc, $row-&gt;ean, $row-&gt;item_description, $row-&gt;item_text, $row-&gt;attribute_keys, $row-&gt;attribute_values, $row-&gt;image_path, $row-&gt;available, $row-&gt;msrp, $row-&gt;price, $row-&gt;current_price, $row-&gt;is_on_sale, $row-&gt;sale_starts_on, $row-&gt;sale_ends_on, $row-&gt;is_on_closeout, $row-&gt;is_new, $row-&gt;manufacturer_id, $row-&gt;manufacturer_name, $row-&gt;category_id, $row-&gt;category_name, $row-&gt;sub_category_id, $row-&gt;sub_category_name, $row-&gt;group_id, $row-&gt;group_description, $row-&gt;group_text, $row-&gt;associated_group_ids, $row-&gt;updated_at, $row-&gt;is_ormd);

  odbc_close($conn);
}

</pre>
<p>From there, I can do comparisons with previous runs to see what's been added, changed, and deleted, and even use the <a href="http://drupal.org/project/tw" target="_blank" rel="noopener">Table Wizard module</a> to expose my table to Views.<br /><br />I had initially done this in a separate script and just bootstrapped Drupal to make all the Drupal functions available to it, but since then I've written a custom drush command that I call from a cron job, and it's working well.<br /><br />If anyone has suggestions for ways to improve this, I'm open to them.&nbsp; I've shared this data with Damien Tournoud, since he has been the driving force behind the SQL Server driver for Drupal for D7, so hopefully it can be backported to D6 at some point.</p>]]>
      </content:encoded>
      </item>
      
      <item>
         <title>Theming This Site</title>
         <link>http://www.smgaweb.com/blog/theming-site</link>
         <media:content medium="image" url=""/>
         <dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Steve </dc:creator>
         <pubDate>Thu, 02 Sep 2010 23:13:00 +0000</pubDate>
         <guid>http://www.smgaweb.com/blog/theming-site</guid>
         <description>The various steps I went through and tools and resources used to originally theme this site in Drupal 6.</description>
         <content:encoded>
        <![CDATA[<p>I have worked on many other sites since I started working with Drupal four years ago, and other than my first blog site, I had never done one for myself.&nbsp; When I started SMGA a year ago, I knew I needed to get a site going for it, but between family and all the other sites I work on, I hadn't had the time to do it.&nbsp; The biggest issue for me was the theme.&nbsp; As a developer, and someone who is not gifted at creating designs, I needed to find one that I liked that I could convert to a Drupal theme.&nbsp; I had looked at literally hundreds of open source designs without seeing one I really liked, until one day, I saw a webcast by <a href="http://walkah.net/" target="_blank" rel="noopener">James Walker</a> on theming, and as his example he used a theme from <a href="http://www.styleshout.com/" target="_blank" rel="noopener">Styleshout</a>.&nbsp; After looking through the themes, I finally came across one I liked called <a href="http://www.styleshout.com/templates/preview/CoolBlue10/index.html" target="_blank" rel="noopener">Cool Blue</a>, and I decided to use it.&nbsp; It looks to be designed primarily for a blog site, but the page also provides examples of all styles used, so it's easy to adjust the front page.<br /><br />One of the first things I noticed was the main column.&nbsp; At first glance, it appeared that there were three columns for the blog, but after closer inspection (and a little help from the Drupal support mailing list), I realized that there were only two columns - the main content and the sidebar on the right - and the main content area was broken down into two parts, with the blog post meta data to the left of the body.&nbsp; This layout is used both on the page listing the blog posts and the blog post itself.&nbsp;<br /><br />Right off the bat it was fairly obvious that a view would be needed for listing the blog posts, and the blog post page itself would be a custom node template.&nbsp; However, upon a <a href="http://lists.drupal.org/pipermail/support/2010-June/015033.html" target="_blank" rel="noopener">suggestion from the mailing list</a>, I checked out Display Suite.&nbsp; DS is like Drupal in that it has a steep learning curve, but with the help of two <a href="http://mustardseedmedia.com/podcast/episode40" target="_blank" rel="noopener">excellent </a><a href="http://mustardseedmedia.com/podcast/episode41" target="_blank" rel="noopener">screencasts</a> by Mustardseed Media, I was able to grok DS and get my displays created.&nbsp; However, then came the time to theme them, and that wasn't so easy.&nbsp; On looking at the HTML, I saw a number of tags that I needed to modify with a class, but I couldn't find where to do it.&nbsp; I grepped the code, I looked at theme functions and templates, but for the life of me I could not find where to change the things I wanted to change.<br /><br />I had been doing a decent amount of Views theming with custom templates, and was getting pretty comfortable with it, so I decided to go that route.&nbsp; For the blog listing page, I created a view with all of the required fields, and then created a custom template by cloning views-view-fields.tpl.php into my theme directory and renaming it to match my specific display (views-view-fields--blog--page-1.tpl.php).&nbsp; I removed all of the default code from the template, copied in the appropriate HTML from the design, and then just added in my variables.&nbsp; In order to get things like the date variables the way I needed them I also had to create a views preprocess function to create those variables, and then just access them in the template.<br /><br />The node display was very similar.&nbsp; Since the HTML and CSS from the Styleshout template already creates the layout, I created node-blog.tpl.php, dropped in the HTML from the design template, and started adding in the template variables.&nbsp; Comments were a bit of a challenge, since the template uses unordered lists and by default Drupal doesn't, but by cloning and modifying comment.tpl.php and comment-wrapper.tpl.php, I was able to get the comments looking fairly close to the template (there's still some work to do).<br /><br />I swapped out the social network icons for the Sexy Bookmarks module, and I still have some work to do on the Search box and other little things, but overall I'm happy with the way it's turned out so far.</p>]]>
      </content:encoded>
      </item>
      
   </channel>
</rss>



    

      <?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">
   <title>Latest blog posts</title>
   <link href="http://www.smgaweb.com/blog/rss/" rel="alternate" />
   <link href="http://www.smgaweb.com/blog/atom/" rel="self" />
   <id>http://www.smgaweb.com/blog/rss/</id>
   <updated>2020-04-25T19:28:00+00:00</updated>
   
   <entry>
      <title>Dynamically Importing Vuex Store Modules From Directory Structure</title>
      <link href="http://www.smgaweb.com/blog/dynamically-importing-vuex-store-modules-directory-structure" rel="alternate" />
      <published>2020-04-25T19:28:00+00:00</published>
      <updated>2020-04-25T19:28:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/dynamically-importing-vuex-store-modules-directory-structure</id>
      <summary type="html">How to dynamically import Vuex store modules into the base store from throughout a Vue application directory structure.</summary>
   </entry>
   
   <entry>
      <title>Dynamically Generating Vue Router Routes From Directory Structure</title>
      <link href="http://www.smgaweb.com/blog/dynamically-generating-vue-router-routes-from-directory-structure" rel="alternate" />
      <published>2020-04-18T06:10:00+00:00</published>
      <updated>2020-04-18T06:10:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/dynamically-generating-vue-router-routes-from-directory-structure</id>
      <summary type="html">How do build a Vue router definition by importing router configurations from throughout a Vue application directory structure.</summary>
   </entry>
   
   <entry>
      <title>Render Functions, Icons, and Badges With Vuetify</title>
      <link href="http://www.smgaweb.com/blog/render-functions-icons-badges-vuetify" rel="alternate" />
      <published>2020-04-15T14:45:00+00:00</published>
      <updated>2020-04-15T14:45:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/render-functions-icons-badges-vuetify</id>
      <summary type="html">My steps to generate a Vuetify icon and badge using a render function in VueJS.</summary>
   </entry>
   
   <entry>
      <title>Creating a Fast Site with Nuxt and CSS</title>
      <link href="http://www.smgaweb.com/blog/creating-fast-site-nuxt-css" rel="alternate" />
      <published>2020-03-14T04:52:00+00:00</published>
      <updated>2020-03-14T04:52:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/creating-fast-site-nuxt-css</id>
      <summary type="html">How I used Nuxt and pure CSS to build a very fast website with good SEO.</summary>
   </entry>
   
   <entry>
      <title>I&#x27;m Now A Panelist On The Javascript Jabber Podcast</title>
      <link href="http://www.smgaweb.com/blog/im-now-a-panelist-on-the-javascript-jabber-podcast" rel="alternate" />
      <published>2019-10-06T04:49:00+00:00</published>
      <updated>2019-10-06T04:49:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/im-now-a-panelist-on-the-javascript-jabber-podcast</id>
      <summary type="html">After being a fan of the Javascript Jabber podcast, I get the privilege of now being a regular panelist.</summary>
   </entry>
   
   <entry>
      <title>On the My JavaScript Story Podcast</title>
      <link href="http://www.smgaweb.com/blog/on-the-my-javascript-story-podcast" rel="alternate" />
      <published>2018-09-29T02:56:00+00:00</published>
      <updated>2018-09-29T02:56:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/on-the-my-javascript-story-podcast</id>
      <summary type="html">I was recently a guest on the My JavaScript Story podcast with Charles Max Wood of devchat.tv to talk about my history as a developer and how I got into javascript.</summary>
   </entry>
   
   <entry>
      <title>Custom Video Export/Import Process With Views and Feeds</title>
      <link href="http://www.smgaweb.com/blog/custom-video-exportimport-process-views-and-feeds" rel="alternate" />
      <published>2017-05-02T20:49:00+00:00</published>
      <updated>2017-05-02T20:49:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/custom-video-exportimport-process-views-and-feeds</id>
      <summary type="html">Creating a custom video node export/import process using custom forms, RSS feeds with Views, and a custom Feeds importer using the SimplePie parser.</summary>
   </entry>
   
   <entry>
      <title>Getting URL Alias For Referenced Entity With RESTful API in Drupal</title>
      <link href="http://www.smgaweb.com/blog/getting-url-alias-referenced-entity-restful-api-drupal" rel="alternate" />
      <published>2015-12-27T07:02:00+00:00</published>
      <updated>2015-12-27T07:02:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/getting-url-alias-referenced-entity-restful-api-drupal</id>
      <summary type="html">How to  create URL alias lookup endpoints for referenced entities using  RESTFul module in Drupal 7.</summary>
   </entry>
   
   <entry>
      <title>Creating a RESTful API URL Alias Lookup in Drupal</title>
      <link href="http://www.smgaweb.com/blog/creating-restful-api-url-alias-lookup-drupal" rel="alternate" />
      <published>2015-12-06T01:49:00+00:00</published>
      <updated>2015-12-06T01:49:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/creating-restful-api-url-alias-lookup-drupal</id>
      <summary type="html">How to create a URL alias lookup API endpoint in Drupal when using it in a decoupled setup</summary>
   </entry>
   
   <entry>
      <title>Custom Image Search with Solr, Filefield Sources, and Ctools - Part 3</title>
      <link href="http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-3" rel="alternate" />
      <published>2015-09-21T21:47:00+00:00</published>
      <updated>2015-09-21T21:47:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-3</id>
      <summary type="html">In our quest to build a custom image search functionality, we are at the last two steps. We have the ability to display a modal window, search for images, and page through the results; now we just need to be able to write the file name and file id back to the source text field so it can be added to the node. Also, in the last post, we just glossed over the code that actually gathers the search parameters and searches Solr, so we will cover that code in detail here.</summary>
   </entry>
   
   <entry>
      <title>Custom Image Search with Solr, Filefield Sources, and Ctools - Part 2</title>
      <link href="http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-2" rel="alternate" />
      <published>2015-09-21T16:35:00+00:00</published>
      <updated>2015-09-21T16:35:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-2</id>
      <summary type="html">Continuing on from my previous post , we are working on building a custom image search in Drupal using Apache Solr, Filefield Sources, and Ctools. So far, we have created our custom FileField Source element that will allow us to search. Now we get to the step of creating our ...</summary>
   </entry>
   
   <entry>
      <title>Custom Image Search with Solr, Filefield Sources, and Ctools - Part 1</title>
      <link href="http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-1" rel="alternate" />
      <published>2015-09-20T00:02:00+00:00</published>
      <updated>2015-09-20T00:02:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-1</id>
      <summary type="html">Part I of creating a tool for storing and browsing existing images stored in Apache Solr from a Drupal 7 site.</summary>
   </entry>
   
   <entry>
      <title>Connecting to a remote MS SQL Server database from Mac OS X</title>
      <link href="http://www.smgaweb.com/blog/connecting-remote-ms-sql-server-database-mac-os-x" rel="alternate" />
      <published>2010-09-04T23:16:00+00:00</published>
      <updated>2010-09-04T23:16:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/connecting-remote-ms-sql-server-database-mac-os-x</id>
      <summary type="html">Connecting to a remote MS SQL Server database from Mac OS X using FreeTDS and unixODBC</summary>
   </entry>
   
   <entry>
      <title>Theming This Site</title>
      <link href="http://www.smgaweb.com/blog/theming-site" rel="alternate" />
      <published>2010-09-02T23:13:00+00:00</published>
      <updated>2010-09-02T23:13:00+00:00</updated>
      <author>
         <name>Steve </name>
      </author>
      <id>http://www.smgaweb.com/blog/theming-site</id>
      <summary type="html">The various steps I went through and tools and resources used to originally theme this site in Drupal 6.</summary>
   </entry>
   
</feed>
    

      <?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
 
  <url><loc>http://www.smgaweb.com/blog/dynamically-importing-vuex-store-modules-directory-structure</loc><lastmod>2020-04-25</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/dynamically-generating-vue-router-routes-from-directory-structure</loc><lastmod>2020-04-18</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/render-functions-icons-badges-vuetify</loc><lastmod>2020-04-15</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/creating-fast-site-nuxt-css</loc><lastmod>2023-03-16</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/im-now-a-panelist-on-the-javascript-jabber-podcast</loc><lastmod>2019-10-06</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/on-the-my-javascript-story-podcast</loc><lastmod>2018-09-29</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/custom-video-exportimport-process-views-and-feeds</loc><lastmod>2017-05-02</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/getting-url-alias-referenced-entity-restful-api-drupal</loc><lastmod>2015-12-27</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/creating-restful-api-url-alias-lookup-drupal</loc><lastmod>2015-12-06</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-3</loc><lastmod>2015-09-21</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-2</loc><lastmod>2015-09-21</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/custom-image-search-solr-filefield-sources-and-ctools-part-1</loc><lastmod>2015-09-20</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/connecting-remote-ms-sql-server-database-mac-os-x</loc><lastmod>2010-09-04</lastmod></url>
 
  <url><loc>http://www.smgaweb.com/blog/theming-site</loc><lastmod>2010-09-02</lastmod></url>
 
</urlset>