I’ve just released my third game to IndieDB called Airline Reservations Agent. This is a light, web-based simulation about reserving flights for callers in the 1960s. The main components of the game are a console with lots of buttons and a wooden box full of route cards. A tutorial is included in the game to get you up to speed with the console.
Author: randy
Railroad Dispatcher on Indie DB
I’ve just added my second game, Railroad Dispatcher, to Indie DB. This is an enjoyably-paced, web-based game based on real-life dispatchers working Centralized Traffic Control (CTC) panels for their railroad.
For those of you unfamiliar with CTC, here is an image of the real thing:
I’ve played around with this idea for about a year now and finally put some serious work into it the last few weeks. Although based on real life, I’ve been throwing my own flavor into it, based on both American and British railroads, just to get the game to a certain “feel” I’m driving toward.
So if you are interested, check it out at Indie DB. I’d appreciate any feedback you have. I’ll be chronicling the development over there and am aiming to release the first demo there shortly.
To Framework or Not To Framework
So this post is really just a mess of thoughts and questions that have been erupting out of my cranium the last few months or so, as I’ve dealt with decisions on using different platforms and technologies for different projects. Unfortunately, I don’t think I’ve come up with any answers yet, but getting this out in print is relieving some of the built up pressure.
I’ve been involved in several website projects recently where the question keeps arising as to whether to use a framework, and if so, which one? The “which one” question can be complicated enough, but I’ve been taking a step back and really trying to decide if I want to use a framework at all!
Now, I’ve been involved in this battle for some time now, and it has intensified with my recent experiences with Drupal projects. I’ve built several sites now in Drupal with varying degrees of success. It’s been a roller coaster. It usually starts with the siren call that sounds like “Hey! You could do just about everything you want in Drupal. Why not plunge all the way in and make this your framework for life!” But in the valleys, I’ve wanted to throw everything away and just write pure PHP or make a living somehow in the deep wilderness. It’s making me second guess myself on choosing the correct tools for the job.
Take for instance one of my recent projects. It’s an ecommerce site of sorts, and as you would expect, the design calls for a listing of products, with filters on the left and sorting on the top — like Amazon.com and most other sites of that nature. It’s being built in Drupal. Easy enough, I say. The versatile Views module can handle that. But guess what? Far into the process I realize that you can’t separate the filter controls from the sort controls. There’s a contrib module out there that is addressing this, but it’s still in development and it’s not working on my site. So I start digging through the Views code, trying to figure out what’s gone wrong and in my mind I’m thinking, “If I was building this site in straight PHP or .NET I could write this in five minutes!”
And there’s the problem rearing its ugly head again. What makes more sense? Building a site from scratch, where you have a handle on all of the mechanics, or using a framework where a lot of the standard site code is already written and feature-rich? The lure of the framework, where a lot of things are already taken care of (user administration, roles and security, page editing, etc.) is very enticing. But what I’ve found is that there are always several features the client wants that the framework doesn’t readily handle. And some of them are shockers (like the filter/sort mentioned above).
Now, I’m a coder, so you say, “Randy, if you can code, then just get in that open source goodness and change the code to do what you want.” I can do that, but if you are working with a large framework (e.g. Drupal) it usually isn’t a matter of just hopping into a file and changing a couple lines of code. The framework is massive, it’s extended with a thousand modules written by a thousand different developers and a lot of it, by necessity and good practice is abstracted quite a bit to handle an unknown number of use cases. So it takes time, sometimes a lot of time, and I’m always facing a deadline.
So it sounds like I’m really trashing Drupal. That is not my intent. I’ve worked on several projects where it was the right tool for the job and for the most part, did most of what the client wanted. If it doesn’t, however, that custom coding can get out of hand. The Drupal API is monstrous and I’ve coded quite a few custom modules with it, as the need arises. Sometimes it’s straightforward, many times it’s not.
Lately, I’ve been leaning more towards using PHP supplemented with code libraries. That way I can write PHP without worrying how to integrate it into some massive code base. Using something like Cake or OpenAvanti gives you MVC and ORM out of the box and you can just start coding and feel the cool breeze on your face. Starting a project this way, you can still hear those sirens beckoning, “We’ve already got user admin and page editing ready to go over here.” But I have to resist. Once over there I get sucked in, and before I know it I wish I was back where I had my hands on the wheel.
Like a Glove – A Coder’s Tools
You’ve felt it before. You’re a coder, and you are in the zone. Not just mentally, but physically as well. Your fingers fly across the keyboard, producing lines of logic as fast as you can think them. Key combinations and shortcuts are activated without a second thought. You feel at one with your machine.
I was thinking about this the other day as I sat at one of my computers and was not feeling at all in the zone. My mind was willing and ready, but I couldn’t mesh its energy with my machine effectively. The location: my desktop at home. Something just didn’t feel right.
Most of my coding hours at the moment are spent at my day job where I sit at a nice size desk with my laptop and a good-size second monitor. The desk is a plain, flat surface, and it’s pretty much just me, my laptop, mouse, second monitor and usually a can of Coke. It is this setup where I can usually get into the zone. I feel like a Formula One driver, able to feel every subtle variation of the road beneath me. In fact, as long as I’m in a pleasant environment, on a nice surface, with at least my laptop and a mouse, I’m good to go.
You cannot ignore the fact of how a mouse feels in your hand, how the keys click beneath your fingers and how the overall layout of the keyboard is conducive to your programming activities. Just giving a developer any keyboard and mouse and expecting them to be effective is not realistic. You have to love the feel of your hardware.
Now, software also plays a big part in this, naturally. If you don’t like your development tools then it can be near impossible to feel that good energy while you program. I’ve been using Vim for a while now and it’s becoming an extension of myself. I love it and I feel totally charged coding in it. But right now, sitting at my desktop, I knew the software wasn’t the issue.
My desktop at home currently sits on a computer desk that I purchased several years ago that has built in shelving and a decent amount of surface to use. The problem I think is the rolling keyboard tray. You know, that flat shelf thing that rolls out and holds your keyboard and mouse? I’m sitting there with the tray rolled out, which puts me a few inches further back from my monitor than I would like. The tray is also a few inches lower than the desk so it’s not at an ideal height for me.
These few inches here and there may not seem like a whole lot, but it’s what’s preventing me from being immersed in that wonderful zone. I feel disconnected, like I’m operating my machine through some kind of remote presence device. So I know what I have to do. I have to get a new desk. This simple step is going to give me a big boost in productivity. I know because I’ve already felt what it’s like to feel at one with my machine. And it feels incredible.
Taming Minecraft Installations and Mods
After playing Minecraft for a bit I started venturing into the world of mods for more variety and new adventures. The one thing that immediately started annoying me though was that I wasn’t yet sure how the mod system worked, how the files were organized and how I could keep separate versions of Minecraft installed on the same machine. The result was my single installation of Minecraft getting all mucked up with different mods and generally watching everything go haywire.
What if I just wanted to play the latest Minecraft version with no mods? What if I just wanted to play a single-mod version or a certain combination of mods only (e.g. Computercraft and OptiFine)? I needed to get a handle on how this all worked so I could keep my sanity. And the solution is really quite simple.
First, create a directory anywhere on your machine. For this example, we’ll just call it c:\minecraft. Within this folder create a bin folder and any other folders you wish to create that will hold your separate versions (or mod combos, or instances, or whatever you want to call them) of Minecraft. For example:
Take your Minecraft.exe file (the one you downloaded from Minecraft.net) and copy it to your c:\minecraft\bin folder.
Now let’s work on our Computercraft version of Minecraft. Within the c:\minecraft\computercraft folder, create a folder named data and a file named minecraft.bat.
Edit your minecraft.bat file to look like the following (Thanks to this post at stack exchange which helped me a lot and started me on the path to seeing the light: http://gaming.stackexchange.com/questions/29272/how-do-i-keep-two-different-versions-of-minecraft-installed):
set LAUNCHER=c:\minecraft\bin\minecraft.exe
set APPDATA=c:\minecraft\computercraft\data
%LAUNCHER%
As you can see, this is just telling Minecraft where the launcher is and the game directory we want to use. This is key. Without this bat file, Minecraft just assumes you will be playing inside the default folder, usually located at something like AppData/Roaming/.minecraft.
Double-click your minecraft.bat file to launch Minecraft. We need to do this first so Minecraft will setup the appropriate files and folders. The Minecraft launcher will recognize this as a new instance, so log in as normal. You will also need to click the Play button as this will finish the default Minecraft setup. You can then close Minecraft. Your folder structure now looks like this:
As you might expect, if you play this now, you will be playing a stock version of Minecraft. But we want to play Computercraft, so let’s get that setup. Computercraft needs the Minecraft Forge utility to run so we will set that up first. Get Forge and run it’s installer. We want to select “Install client” and then it asks you to specify the .minecraft folder to install Forge against. In this case, we want to use our c:\minecraft\computercraft\data\.minecraft folder.
Click OK. Forge will then download some libraries and let you know when it is complete. A couple of things to note here. First, Forge may tell you that you need to run a certain version of Minecraft before it can install. In my case, Forge wanted me to run version 1.6.2 before it could install. I was confused until I realized that my profile was set to “Run latest version”, which as of this writing is 1.6.4. I simply set my profile to Run 1.6.2, clicked Play, then Quit then Forge installed normally. Secondly, I’ve noticed that sometimes the “downloading libraries” step hangs. If after a minute or so you think it is hanging, just abort the installation and try it again. Every time this has happened to me, a reinstall fixes the problem.
Running your minecraft.bat file now gives you a Forge profile in your launcher. Selecting the Forge profile then confirming your existing user or logging in as a different one, then clicking Play will complete the Forge installation. The Minecraft start screen should look something like this:
And your folder structure should look something like this (notice the new mods folder):
OK, great! The hard part is done. Quit the game once more. Get yourself a copy of computercraft and simply drop the zip file into your mods folder.
Run your minecraft.bat. You are now playing Minecraft with the Computercraft mod installed. You can continue to add other mods as well that are compatible with Forge. For example, I dropped the OptiFine mod into the mods folder so I’m running Computercraft and OptiFine at the same time.
So as you can see, this is a great way to keep your Minecraft installations and mods organized. You can play around with new mods and resource packs and worlds or whatever without worrying that your other setups are getting messed up.
My First Minecraft Model
I’ve recently been sucked into the world of Minecraft and am really enjoying the open-ended nature of the experience. Being a Minecraft newbie I’ve been absorbing all kinds of information from survival tips to creating complex redstone circuits. Another thing that’s really grabbed my attention is the ability to create really cool architecture and scenes out of the simple blocks provided.
So here is my first attempt at a focused building effort within Minecraft. It is based on the Whitmore Lake McDonald’s in Michigan that I’ve stopped at a couple of times and really think the layout is aesthetically pleasing.
This is the view as you enter the front door. The drink station is on the left, counter straight ahead and playland to the right. The drink station contains three dispensers that dispense empty buckets, buckets of water and buckets of milk.
Looking toward the playland you can see some blocks to climb on and there’s also a short minecart route to ride around on. There are also tables a step down into the playland area as well as a couple of tables inside the playland.
Looking back toward the drink station you can also see the McCafe station (full of buckets of lava) and the two drive-thru windows.
Into the kitchen we see several furnaces. Coal is stocked in the chests on the floor. The chests in the prep area contain raw meat and bread. The chests in the front contain cooked meat, bread, baked potatoes and apples. Raw potatoes are stocked at the fry station over by the drive-thru.
This is just a shot from behind the counter looking toward the front of the restaurant.
Here is the back of the restaurant. An employee entrance to the kitchen is on our left, another employee door out the back of the building to the right and the restrooms are in the back corner. I’m not sure what to do yet with that little brick area.
And finally, I’ll leave you with a view of the restaurant at sunset.
Charity Tree – Interface First – Part 2
So I’m working on the Charity Tree project again, which I began chronicling in this post, and am picking up where I left off, with user interface design.
I’ve recently come across a fantastic UI prototyping tool called Indigo Studio and am absolutely loving it. I encourage you to try it out. The screenshots in this post were created with this tool, and there are actually interactions wired up as well that you would see if you were viewing them from within the software.
If you’ve read my previous articles in this series, you will remember that one of my main goals is to keep the interface as simple as possible. Matching donors with clients has always been by sticking point, and I think I’ve finally nailed it. As you will see below, there are no more concepts of combination client/donor screens. You simply pick the entity you want to work with, then go search for a match. I think this approach has really simplified things.
So let’s walk through the current state of the UI prototype and I’ll commentate a little on some of the functionality.
Program Manager’s Dashboard
This is the screen that the program manager, my term for the overall person in charge of running the charity program, would see after logging in. My goal here is provide an at-a-glance view of the current state of the program.
Client Listing
The listing screens are probably the most uninteresting of the lot, but again, I created a screen that allows you to quickly get where you need to go without a billion options. Let’s click one of the “Gorton” rows.
Client Detail
Here we are looking at a client application that has been submitted, including multiple family members. The lower portion of the screen is divided with the general client application data on the left and the family member data on the right. Both sections are scrollable.
The client application can be reviewed here, and it can be approved or rejected by clicking the status next to Application.
Finding a Donor
Let’s find a donor for this client. You can see on the right-side of the screen that we can find a donor for all of the family members at one time, by clicking “Find a donor for all waiting members of this family”, or find a donor for just one of the family members by clicking “Find a donor” beneath the desired member. Let’s click the “all waiting members” link.
We are brought to the donor listing screen and notice at the top how the app is now expecting you to locate a donor for the Gorton family. Let’s select the Sycamore Gathering organization. You can see that they would like to donate to 40 families, but only 4 have been assigned so far. Clicking them brings us to the donor detail screen.
You can see we still have the message at the top of the screen telling us that we are still in “match” mode. Also, on the right side of the screen you can see we have the option of matching the Gorton family to this donor. If we forgot some of the details of the Gorton family, we can click the “view details” link at the top of the page.
So let’s say we are satisfied with our selections and make the match by clicking on the “Match to Gorton” link.
Notice the notification of the successful match at the top of the screen and the increase in the match total at the right of the screen.
Conclusion
I’ve detailed out several other button clicks and a few other screen state changes but we’ll stop for now as I think you’ve probably got the idea of how this app is going to work. If you’d like to view all of the screens, you can download the zip file here.
If you have Indigo Studio, you can download my project file and check out all of the screens so far and some of the screen interactions.
So I’m pretty excited about the direction this is taking and I’m feeling good about starting to cut some code. More on that later!
Articles in this series:
Quick ScrollTo Code for jQuery
The Problem
I had been using a jQuery plugin to automatically scroll an element to the top of a container with success for quite a while. Then I changed my markup and the .ScrollTo() function began having strangely; specifically, scrolling my entire page to the top of the window then refusing to respond to further .ScrollTo() calls. After an hour of trying to figure out why and then trying an alternative plugin with the same results, I decided to try and see if I could just quick write what I needed.
It turned out to be surprisingly easy — actually only two lines of code — one if you really want to compact it further.
The Layout
The simple markup we are looking at is a container full of elements that we want to scroll to by clicking various buttons. So let’s say we have a container full of header-paragraph pairs representing short descriptions of planets in the solar system (content taken from wikipedia). We have a toolbar of buttons with the planet labels, and by clicking them, we want to scroll to the appropriate header. The markup would look something like this:
<div id="planets">
<div id="mercury" class="button">Mercury</div>
<div id="venus" class="button">Venus</div>
<div id="earth" class="button">Earth</div>
</div>
<div id="content">
<div id="planetWrapper"</div>
<h2 id="content_mercury">Mercury</h2>
<p>
Mercury is the smallest and closest to the Sun of the eight planets in the Solar System, with an orbital period of about 88 Earth days.
</p>
<h2 id="content_venus">Venus</h2>
<p>
Venus is the second planet from the Sun, orbiting it every 224.7 Earth days.
</p>
<h2 id="content_earth">Earth</h2>
<p>
Earth is the third planet from the Sun, and the densest and fifth-largest of the eight planets in the Solar System.
</p>
</div>
</div>
The CSS
There’s only a couple of key elements that need to be in place:
- The content element, being the “outer container” needs its position set to relative and its overflow-y property set to hidden
- The planetWrapper element needs its position property set to absolute
The Scroll
I won’t go into all the details here of reacting to the click events and such in jQuery. What I want to show you is the code that scrolls the planetWrapper to the correct element. Let’s call our function scrollToPlanet and it will accept one parameter, which is the planet’s H2 to scroll to. Therefore:
function scrollToPlanet(planetHeader) {
currentPos = $(planetHeader).position().top;
$(planetWrapper).animate({
top: -(currentPos)
}, 1000);
}
All we are doing here is noting the current position of the planet header we want to scroll to, then animating the entire planetWrapper element such that its top position moves up a negative amount equal to the header’s position. This effectively dispalys the header at the top of the container.
A Faceted Search Solution for Drupal
I recently had to add a search filter to a Drupal site where the user can filter down criteria to find a reduced listing of products. This is called Faceted Search and is a common navigation technique on ecommerce sites. Think of the left side of Amazon’s site where you can pick Toys then filter down to a specific department and age group.
After searching the Drupal contrib library for something that would work, and wanting to stay away from any extra server-side configurations, I decided to write my own. This was an intense battle with the Drupal API and I am publishing the solution here to help others out and hopefully get some feedback on things I could have done better.
The Test Site
OK, so let’s lay some groundwork here for the test site. It is built on Drupal 7.15 and includes the following contrib modules worth making note of:
I’ll make note of when these modules are used and for what purpose throughout the article.
We will be mimicking a small portion of Amazon’s website by building a Drupal site where you can shop for toys. To start, I have created a couple of Taxonomy Vocabularies — Department and Age Range — and populated them with a few terms (e.g. Action Figures for Department and 2 to 4 Years for Age Range). I then created a Content Type called Toy which includes the following fields:
- Title
- Body
- Department – term reference
- Age Range – term reference
I have then created several toys from a couple of different departments and age ranges. We are now all set to create some views to show us the toys and work on the tools that will help us filter down the list.
Initial Setup of “Shop Toys By” Views
We are going to create a page that lists the different filter terms available for the user to shop by. For example, we’d like the user to see a heading called Shop By Department with the various departments listed underneath. We will do this by creating two views — one for shopping by department and the other for shopping by age range. Make sure you have the Views module installed with Views and Views UI enabled.
Create a view called “Shop Toys By” which is a listing of Taxonomy Terms from the Department vocabulary. We want this created as a block.
Click “Continue & Edit”. Change the Display Name to “Department” and the Title to “Shop By Department”. If you look in the preview area at the bottom of the page, you should see a listing of Departments.
We also want to be able to shop toys by age range, so clone the Department block and change the new block’s Display Name to “Age Range” and the Title to “Shop By Age”.
The Shop Toys Page
We would now like a page to display both of these views to give the user all of these options for starting to shop. There are a couple of ways to do this but I have decided to use Panels to create a page then drop the two views onto the page. Make sure the Panels module is installed as well as the CTools-Page Manager module so we can create a Panel Page. Also make sure that CTools-Views content panes module is enabled so we can drop views output into panels as you will see in a moment.
Navigate to Structure|Pages and click “Add custom page”. I created a page with the path “shop-toys”. In the Content section of the Panel Page, drop the two “Shop By” views.
If you navigate to /shop-toys now you should see something like this:
OK, that’s a start. We have the first step toward shopping for toys via filters.
The Search Results Page
Let’s now create the Search Results page, which will show a listing of all toys that match the incoming filters. Again, there’s a couple of ways of creating this page. This time, I created a view as a page with the path /toy-search-results. To keep the output simple for now, we will simply list all toys that have been published and show their teaser content.
If you preview this view now, you will of course see a listing of all the toys you have entered into the site. Being that this is a search results page though, we will want some way to tell the listing to limit its results based on certain criteria. We want the user to arrive at this page from somewhere else via a url formatted such as /toy-search-results/department/age-range (e.g. /toy-search-results/action-figures/2-to-4-years). To accomplish this, we need to add Contextual Filters to this view.
We add the first Contextual Filter for the Department Field. We set it up as follows:
- When the filter value is NOT in the URL: Provide a default value, Raw value from URL, Path component – 1
- When the filter value IS in the URL or a default is provided: Specify validation criteria, Validator – taxonomy term, Vocabularies – department, Filter value type – term name converted to term id, Transform dashes in URL to spaces in term name filter values, Action to take if filter value does not validate – display contents of “no results found”
What all of this means is the view is expecting the first argument in the URL to represent a toy department. The first argument is coming in as a taxonomy term (e.g. action-figures), it’s from the Department Taxonomy Vocabulary and we want it converted to the actual Term ID.
We then add the second Contextual Filter for the Age Range Field, using similar settings as above.
Looking at the preview for the view, you will see nothing until you enter some arguments into the Preview With Contextual Filters textbox. For example, I entered “action-figures/2-to-4-years” and received a result set.
So at this point we are able to enter a URL in the format of /toy-search-results/department/age-range and get a filtered listing of toys. So let’s make sure we can get there from our Shop Toys page.
Modifying the “Shop Toys By” Views
If you now navigate back to the Shop Toys page and hover over a link, say, Action Figures, you will see that the link points to something like “/taxonomy/term/15”. This is not the behavior we want of course. We’d rather have it go to “/toy-search-results/action-figures”. To do this, we need to modify how our “Shop Toys By” views are writing their links.
Within the view, under the Fields section, you will see “Taxonomy term: Name”. Click it to adjust its settings. Uncheck “Link this field to its taxonomy term page” which is causing it to point to the wrong location. Under the “Rewrite Results” section, check “Output this field as a link”. Now we can define where we want the link to point to. In the “Link path” textbox we can specify the target URL. Notice under the “Link path” textbox the message “You may enter data from this view as per the ‘Replacement patterns’ below.” We have one option which is “[name]”. That’s exactly what we want because we want to send the taxonomy term name to the results page. Note: the Token module must be installed to get replacement patterns.
Enter the following in the “Link path” textbox: toy-search-results/[name]/all. OK, why the “all” term at the end? The view will not like it if it is missing any of its contextual filters, so “all” will satisfy it. This is what we want at this point anyway. If we were to click on the “Action Figures” link, we are looking for all action figure toys regardless of age range. Also make sure the “Replace spaces with dashes” checkbox is checked since we know our search results view is expecting dashes in the URL.
Do the same thing for your Age Range block except your “Link path” should read “toy-search-results/all/[name]”. Save the view. Now if you navigate to the Shop Toys page, you will see that the terms are linked correctly. Click one of them and you will be taken to the Toy Search Results page with a filtered listing of toys.
Filtering by More Than One Value
OK, this is great so far, but what if you want to get a listing of action figures for 5 to 7 year olds? Let’s give the user a place on the search results page to add more filters. Navigate to your Toy Search Results view. What we want to do here is add filter criteria to allow the user to select a department AND an age range if they desire. Add a new filter criteria for “Content: Department”. Select “Dropdown” in the next window but we will override this in a minute. In the next window, check the box for “Expose this filter to visitors, to allow them to change it”. That is all we need here. In the preview area, you should see a droplist allowing the user to select a department.
Being that this is an ecommerce-like filter, we would really rather have radio buttons for the selections instead of dropdown lists. For this, you need to install the Better Exposed Filters module. You may then go over to the “Exposed form style” in the Advanced section of the view and select “Better Exposed Filters”. Select “checkboxes/radio buttons” for the field controls. The view preview will now look something like this:
Save the view and navigate to the Shop Toys page. Click a link and the Search Results page will show the results and include filters for further refinement. Unfortunately, at this point, both Department and Age Range radio button sets have -Any- selected, even though we already selected our first filter criteria. This does not make for a good user experience. Also, that Apply button is not doing what we want either, so we’ll have to do something about that.
Updating the Filters
OK, so let’s tackle those radio buttons first. When we arrive at this page with filters in the URL, we would like the radio buttons to reflect that. In order to do this, we are going to need to write a custom module and modify the behavior of this view. Note that I am not going to go into the basics of creating a Drupal module here. You can refer to this tutorial if you need some basic instruction.
I created a custom module called “toysearch”. In the module file I implement the form_alter hook so I can modify the exposed filter form.
function toysearch_form_alter(&$form, &$form_state, $form_id) { }
In this next code snippet you will see that I am simply parsing the incoming request URL, finding the available options in the $form parameter to match terms against, then altering the form via the $form_state parameter to select the correct radio buttons.
function toysearch_form_alter(&$form, &$form_state, $form_id) { //make sure this is the toy search filter form if (strpos($_SERVER['REQUEST_URI'], 'toy-search-results') != 1 || $form_id != 'views_exposed_form') { return; } //get the page arguments $argStartPos = strpos($_SERVER['REQUEST_URI'], 'toy-search-results') + 19; $argString = substr($_SERVER['REQUEST_URI'], $argStartPos); $args = explode('/', $argString); //select the appropriate filter value for each argument $argCounter = 0; foreach ($args as $a) { //replace the hyphens with spaces so we can match the arg $a = str_replace('-', ' ', $a); switch ($argCounter++) { //department case 0: $matchFound = false; foreach ($form['field_department_tid']['#options'] as $optionId => $optionValue) { if ($optionValue == $a) { $form_state['input']['field_department_tid'] = $optionId; $matchFound = true; break; } } if (!$matchFound) { $form_state['input']['field_department_tid'] = 'All'; } break; //age range case 1: $matchFound = false; foreach ($form['field_age_range_tid']['#options'] as $optionId => $optionValue) { if (strtolower($optionValue) == strtolower($a)) { $form_state['input']['field_age_range_tid'] = $optionId; $matchFound = true; break; } } if (!$matchFound) { $form_state['input']['field_age_range_tid'] = 'All'; } break; default: break; } } //if trailing args are not specified, set the radio group to All if (count($args) < 1) { $form_state['input']['field_department_tid'] = 'All'; $form_state['input']['field_age_range_tid'] = 'All'; } else if (count($args) < 2) { $form_state['input']['field_age_range_tid'] = 'All'; } }
Note that during the process I insert the “All” term if a certain term is not found, then at the end of the function I append the “All” term if a URL comes in that doesn’t specify all of the search criteria. For example, if the URL is “toy-search-results/action-figures” we append “/all” to cover the age range requirement.
If you now select a link from the Shop Toys page, you will see the correct radio buttons automatically selected. This is because we reacted to the form_alter hook and modified the form state before it rendered.
You still can’t select further filters and click the Apply button and get the expected behavior, so we need to do something about that.
Modifying the Apply Button
The supplied Apply button does not act like we want, so we are going to add some code in our form_alter function to add our own button and behavior.
The first thing to note is that because we wanted exposed filters in the view, we were required to enable AJAX for the view. The default behavior for the Apply button is to send an AJAX request with the new filter. We do not want this to happen. We want to request the toy-search-results page again with the new search parameters instead. So first, we want to disable the default behavior of the form.
//set the form action to search-results page instead of the ajax views handler $form['#action'] = 'toy-search-results';
Then we will hide the default button
//hide the default filter button $form['submit']['#attributes'] = array( 'style' => 'display:none;' );
and add our own button
//add our own filter button that calls a custom function to set //up the appropriate search url $form['buttons']['apply_filter'] = array( '#type' => 'submit', '#value' => 'Apply filter', '#submit' => array( 'toysearch_apply_filter' ) );
Notice the “#submit” index in the $form[‘buttons’][‘apply_filter’] array. This specifies a function that we want called when the user clicks our custom button. This is where we are going to redirect the default action to the URL we want.
In the following function we look at the radio buttons that have been selected and construct a URL consisting of “toy-search-results” plus the additional search terms based on the currently selected radio buttons. We then call Drupal’s goto function to redirect the user to the results page.
function toysearch_apply_filter(&$form, &$form_state) { //get the filter value ids $ids = array(); $ids['field_department_tid'] = $form_state['values']['field_department_tid']; $ids['field_age_range_tid'] = $form_state['values']['field_age_range_tid']; //get the actual filter values $urlTerms = array(); foreach ($ids as $fieldName => $fieldId) { foreach ($form[$fieldName]['#options'] as $optionId => $optionValue) { if ($optionId == $fieldId) { if ($optionId == 'All') { $urlTerms[] = 'all'; } else { $term = $optionValue; $term = str_replace(array(' '), '-', $term); $urlTerms[] = $term; } break; } } } $urlString = implode('/', $urlTerms); drupal_goto('toy-search-results/' . $urlString); }
One problem remains. When that Apply Filter button is clicked, a query string is passed in to set the form state. All we need to do now though is test for the presence of a query string and parse the search arguments that way in addition to parsing a clean URL. Here is the entire form_alter function.
function toysearch_form_alter(&$form, &$form_state, $form_id) { //make sure this is the toy search filter form if (strpos($_SERVER['REQUEST_URI'], 'toy-search-results') != 1 || $form_id != 'views_exposed_form') { return; } //set the form action to search-results page instead of the ajax views handler $form['#action'] = 'toy-search-results'; //hide the default filter button $form['submit']['#attributes'] = array( 'style' => 'display:none;' ); //add our own filter button that calls a custom function to set //up the appropriate search url $form['buttons']['apply_filter'] = array( '#type' => 'submit', '#value' => 'Apply filter', '#submit' => array( 'toysearch_apply_filter' ) ); //check if this page has a query string // //this would signify that the user has used the apply filter button //on this filter instead of arriving at the search results directly, using //a clean url $queryString = $_SERVER['QUERY_STRING']; if (!empty($queryString)) { $args = explode('&', $queryString); //select the appropriate filter value for each argument foreach ($args as $a) { $kvp = explode('=', $a); switch ($kvp[0]) { case 'field_department_tid': $form_state['input']['field_department_tid'] = $kvp[1]; break; case 'field_age_range_tid': $form_state['input']['field_age_range_tid'] = $kvp[1]; break; default: break; } } } else { //get the page arguments $argStartPos = strpos($_SERVER['REQUEST_URI'], 'toy-search-results') + 19; $argString = substr($_SERVER['REQUEST_URI'], $argStartPos); $args = explode('/', $argString); //select the appropriate filter value for each argument $argCounter = 0; foreach ($args as $a) { //replace the hyphens with spaces so we can match the arg $a = str_replace('-', ' ', $a); switch ($argCounter++) { //department case 0: $matchFound = false; foreach ($form['field_department_tid']['#options'] as $optionId => $optionValue) { if ($optionValue == $a) { $form_state['input']['field_department_tid'] = $optionId; $matchFound = true; break; } } if (!$matchFound) { $form_state['input']['field_department_tid'] = 'All'; } break; //age range case 1: $matchFound = false; foreach ($form['field_age_range_tid']['#options'] as $optionId => $optionValue) { if (strtolower($optionValue) == strtolower($a)) { $form_state['input']['field_age_range_tid'] = $optionId; $matchFound = true; break; } } if (!$matchFound) { $form_state['input']['field_age_range_tid'] = 'All'; } break; default: break; } } //if trailing args are not specified, set the radio group to All if (count($args) < 1) { $form_state['input']['field_department_tid'] = 'All'; $form_state['input']['field_age_range_tid'] = 'All'; } else if (count($args) < 2) { $form_state['input']['field_age_range_tid'] = 'All'; } } //no query string }
Conclusion
Everything is now functioning properly. The user can come to this page via the Shop Toys page which constructs a clean URL. The filter radio buttons are automatically set. The user can then add filters and the radio buttons will persist with the new choices and a new, clean URL will be constructed which can easily be shared.
I’ve got a couple of hacks in here and some hardcoded values (yuck) which I would ideally like to fix, but I welcome your feedback on any improvements I could make.
Happy Birthday Ludwig Mies van der Rohe
Seeing Google’s doodle today, honoring Ludwig Mies van der Rohe’s birthday, reminded me that several years ago I was playing around with a desktop package called Visual Reality. I’ve always admired the clean lines and simplicity of his work, so I used the application to loosely recreate the basics of the Seagram Building found in New York City.