Building a polls plugin in Craft

Recently a client requested some basic poll functionality for their site that we were planning to build in Craft. I was a bit surprised to see that the Craft plugin directory didn't list anything for "poll" or "survey". Of course there are a lot of third-party apps you could use for this kind of thing, but having just watched Ben Croker's Craft Plugin Development course on Mijingo, it occurred to me that it could be built out in a pretty similar fashion to the Entry Count plugin in that tutorial. I hadn't built my own Craft plugin before, and I thought this would be a relatively simple one to start with.

In this article, we're going to walk through planning and building this plugin from the ground up. I'm approaching this as a primarily front-end developer, so some of the concepts involved are a bit outside my usual wheelhouse. If you're an experienced back-end or plugin developer, this may be a little basic for you.

The Plan

For this plugin, we’re going to add a Control Panel (CP) section where admins can create polls. Each poll will have a question, a list of answers, and an active status toggle. Admins will also be able to see a breakdown of a poll’s responses.

We’re also going to create a new field type called “Poll” that can be added to our entries. When adding a poll field to an entry, we can select which poll we want to add to the entry from the ones we've created in our control panel section. A poll can be added to multiple entries.

On the front-end, we’ll add some basic templates and styling for a poll, as well as a way to display responses after the user has answered. If the user has already responded to a poll, or the poll is inactive, they shouldn’t be able to respond and should instead just see the responses.

Plugin Structure

File structure of our plugin.

The first thing to do is build out the scaffolding for the plugin. For this we’re going to use PluginFactory.io. If you're new to Craft plugin development, like I am, I highly recommend you use this service, as it comes with super helpful documentation in each file about what should go there (I've stripped it out of the code samples below to keep things short).

GtPoll_PollController (this controller is going to handle saving polls and resetting response counts in the CP, and incrementing polls on the public site)

Field Types

GtPoll_PollFieldType (this field type is going to allow our content editors to add polls to an entry)

Models

GtPoll_PollModel (polls are going to have question text and a status of either active or inactive)

GtPoll_AnswerModel (answers are going to have answer text, a poll that they belong to, and a position in the answer list)

Records

GtPoll_PollRecord (our record specifies how our polls are stored in the database - here we define our table name, columns, and relationships - in this case, polls have many answers)

GtPoll_AnswerRecord (answers belong to a poll)

Services

GtPollService (all of our business logic goes here - this is the part of our plugin that does the real work)

Variables

GtPollVariable (here we're going to create some methods for our templates to use, things like getting lists of our polls and their answers)

In addition to the classes above (which actually represent the majority of the file structure of the plugin), we're also going to have folders for Resources (a JavaScript file) and Templates (for our CP pages).

By itself, the plugin won't really do anything except show some screens in the CP. So we're also going to create some sample template code, JavaScript, and CSS that a developer could use when implementing polls on their website.

Main Plugin Class

The main plugin class holds the basic details about our plugin - name, description, author, etc. I should note here that I've lifted this plugin name override option straight from FeedMe, which is another plugin I looked to as an example during development. This allows a developer to rename the plugin as it appears to editors in the CP (which I am always very appreciative of).

In this class, we're also going to tell Craft what settings our plugin has (just the plugin name override), what HTML to render for that settings page (in this case, a very simple Twig template found at /gtpoll/templates/settings/index.twig), and whether or not our plugin has it's own CP section. We're also going to register a dynamic CP route so that if an editor navigates to /gtpoll/edit/15, they'll be able to edit the poll with an id of 15.

Records & Models

After finishing our main plugin class, we need to define our data. We're going to build our records, which tell Craft how our data should be stored in the database, and our models, which we'll use to pass around data within our plugin. We have two types of data – polls and answers. This is the structure we’re going to use:

Polls have a questionText (the string to display to the user) and active (a boolean for the status of the poll). Craft will automatically add a primary key of id, as well as dateCreated and dateUpdated fields, so we don’t need to worry about adding those to our records.

Answers will have a pollId as a foreign key (so we know which poll the answer applies to), answerText (the string to display to the user), responses (the number of times users have responded to the poll with this answer selected), and position (the order in which it appears in the list of answers). Again, Craft will add its default fields to the table.

Our answer record is going to be structured similarly, but notice that in the defineRelations() method, we're telling Craft that the answer belongs to a poll. This will automatically create a column in the database called pollId which will be a foreign key. We're also defining an index on the pollId column, because we'll usually be searching for all the answers that belong to a specific poll.

Our models are containers we'll use to pass around this data, and their classes essentially reiterate the same information as the records, although we're also including the default fields I mentioned. These models will be separate files, but I'll combine them here since they're relatively short:

Templates

After creating the models and records, we're going to move directly into the CP templates. I find that laying out the page helps me visualize the functionality I need to create. For our CP section, we’re going to have an area with 3 tabs - a list of polls, a place to edit polls, and a link off to our settings page. The main list is going to use a template called index.twig. We’re going to extend the control panel layout included in Craft, add the tab list, and set our content to display a table showing all of our polls. There’s some documentation on the Craft site that helps explain this. However, when it comes to figuring out your layout (for example, building the table and deciding what classes to use), the official documentation is pretty light. Your best bet is to poke around Craft’s core templates (craft/app/templates/), another plugin’s templates, or check out the Inspector on another CP page.

You can see that these are just simple get methods that are taking a pollId as an input and returning some data from a service. Returning to our template, we can now set our variables we'd previously marked as TODO. Note that Craft is smart enough to figure out the get part of the method name, so we’re able to shorten our code a little bit. We're going to update those set tags to the following:

{% set polls = craft.gtPoll.polls %}

{% set answers = craft.gtPoll.answers(poll.id) %}

{% set responses = craft.gtPoll.responses(poll.id) %}

These are going to call our getPolls(), getAnswers(), and getResponses() methods in our GtPollVariable class, respectively. Of course this won’t actually do anything yet, because we haven’t built the methods in our service which the variable methods are calling. So let’s do that next!

Services

In our GtPollService class, we're going to create a method called getPolls(), which returns an array of GtPoll_PollModels, optionally only if they are active. Our main list will show all polls, but later on when we create our field type, we'll only want to get a list of active polls. We’re also going to create a method called getPoll() that takes a pollId and returns a single GtPoll_PollModel, populated with the data from the record matching that pollId.

While setting this up I was reading through the Yii ActiveRecord docs and was a little confused until I realized that Craft has an alias for findByPk() called findById(), which I wasn't finding there but is more commonly used in Craft. The more you know!

Next we're going to build out similar service methods for getAnswers() and getResponses().

That should do it for showing polls and answers, but now we need to be able to create and update them.

Edit Screen

Next we’re going to create a new CP template for the edit screen. This page is going to be a form where we can either create or edit a poll. There’s going to be a text field for the question text, a lightswitch field for the active status, and then an editable table for the list of answers, which will also allow us to reposition the answers.

To create a form in the CP, we import _includes/forms at the top of our template.

Here I want to pause for a moment and discuss my biggest source of confusion when building out my first plugin: It wasn't immediately obvious to me that including these forms was also going to include the JavaScript to make them interactive (for example, the Add Row and drag-to-reorder functionality of an editable table). From looking at some other plugins I assumed I was going to have to write this myself, and was totally lost as to where to start. You might have read that Craft internally uses a jQuery extension called Garnish for things like sorting tables and enabling Ctrl-S to save your entry in the browser, and there's virtually no documentation for it. You'll see a lot of plugins using it for things, but the good news is, if you're working with built-in Craft form fields, you probably don't have to.

If you look in /craft/app/templates/_includes/forms you’ll see templates for each field type. Looking at these, you can figure out what data the field type expects, and most of the rest will be automatically taken care of for you.

Before we look at the code, remember that dynamic route we added to the main plugin class? If we get to this template using that route, we're going to have a pollId defined, which is how our template will know if we're editing an existing poll or creating a new one.

While the text and lightswitch fields are fairly straightforward, the editable table one is a bit tricky. The cols and rows arguments are expecting arrays. This is the part I had the most trouble with:

To add a new value to an array in Twig, we need to use the merge filter, which actually runs PHP’s array_merge function. I haven’t often needed to use that particular function in PHP, so I didn’t initially realize that PHP will reset numeric keys from 0, which is why I’ve prefixed mine with ‘answer_’. Without this prefix, this would cause us to overwrite the same first few answerIds in our database over and over (even if those answers were for other polls!).

It's also worth pointing out that in this case we’re passing an array containing one value (the answer text) but if our table had more than one column, we’d have multiple values in that array.

For a form submission in the CP, instead of putting an action attribute on the form, we add a hidden input with the name action that has our action URL as a value. There are some details on why this is the case here. Initially I had tried to use the actionURL() function to output the value here, but:

value="actionUrl('gtPoll/poll/savePoll')" doesn't work.

value="gtPoll/poll/savePoll" works.

I’m honestly not really sure why the first one doesn’t work, if you can clear up this mystery for me, send me a note!

Controller

Submitting the form with the action above is going to look for a method called actionSavePoll() in our GtPoll_PollController. This method is going to grab the info from our POST request, create a new poll model and an array of answer models and send it all to our service method (which we’ll create in a moment). The controller method will then set a session message telling the user if the poll has saved successfully, and redirect them back to the plugin index page.

While we're in the controller, we're also going to create two other methods we'll need later. The first will increment an answer's response count (which will be triggered from the front-end template by a user responding to the poll) and the second will reset all of the response counts for a poll's answers (which is triggered by clicking the (-) button in our list view in the CP).

For the public-facing action, we need to specify that users who aren't logged in can trigger this action, so we're going to do that by setting an array with that function name to $allowAnonymous, We also want to make sure that this method ends by either redirecting somewhere else or, in this case, returning JSON, otherwise we're going to get 404s when calling our controller via AJAX because Craft will try to continue routing the request (this one was a bit of a gotcha for me).

Back in our service, we’re going to create the savePoll() method which is actually going to insert or update the poll and answers. The logic here is fairly straightfoward. We’re going to validate the poll model we’ve been given by the controller, either grab the existing record or create a new one, set it’s values from the model, and then save. Once we’ve saved the poll, we have a pollId to relate the answers to, so we’ll loop through the answer models and save them in the same fashion.

Field Type

The last thing we need to do on the admin side is create a field type so that our editors can add polls to entries. First we’re going to create the Twig template for the field type, which is fairly simple. We’re going to create an array of poll options in a similar fashion to our answers on the edit screen, prefixing their numeric keys. Then we’re going to output a dropdown with the poll question texts as the options:

Once that’s done we’re going to create the field type class. The getInputHtml() method is going to render our Twig template with some values, including the current field value from the database. The prepValueFromPost() method is going to strip our prefix from the field value before saving it to the database.

Front-End

Now that we’ve finished the back end for our plugin, we need to create the Twig template, CSS, and JavaScript that’s going to make all this come together on the front-end of the site. This isn’t part of the plugin per se, but it’s helpful to provide some documentation for developers using the plugin as to how they can implement it.

We're going to build a front-end module which displays the poll as a form, does an AJAX submission to increment the chosen response, and then does a card flip animation to show the results (including the response we've just put in). We're also going to set a cookie so that users who have already completed the poll won't be able to do so again. If users with this cookie go to an entry with this poll, or the poll has been set to inactive, we'll just see the results immediately.

This module consists of three parts: a CSS file, a JavaScript file, and some Twig code. Let's look at the template first. A developer using our plugin would want to put this anywhere a Poll field is used (in my case, I've got a Poll block in my main matrix field, which includes this code).

So we've essentially got the typical card-flipper pattern there. The front side of the card has our poll question and submission form, and the back side has our responses. We're going to use the class .gtPoll--inactive on the main container to indicate our "flipped" status. You'll notice in our CSS we're going to omit heights for the cards. We'll add those in with JavaScript afterwards to ensure the card is always the height of the taller of our two card faces.

A function to animate the poll bars from zero to their new total, including the option the user just voted for.

A submit event for the form, that will submit the form data to our controller via AJAX, then flip our card, call the above animate function (you'll notice I'm using a setTimeout with a delay that matches our transition, but the more correct way to do this would be to listen for a transition end event on the flip), and then set our cookie.

When the page loads:

If the user has a cookie indicating they've completed the form, we need to make it inactive.

If the page contains any inactive polls, we want to animate their response bars right away.

We need to set the height of the card sides to the taller of the two sides (it might also be a good idea to re-trigger this function when the window is resized, but we're not going to worry about that).

There are some ways I've already thought of to improve the plugin. Most importantly, instead of representing our polls in a simple table, we could create them as ElementTypes, which would also allow us to use Craft's built-in translations. It would also be helpful to have the option to delete old polls, which wouldn't be difficult to implement.

Writing this first plugin for Craft was a big learning experience for me, and I'm sure the above article missed something or got some conventions wrong. If you have any comments or feedback, please drop me a line via email or Twitter, or create issues on the repo.