DEV Community

Cover image for In-place editing with Dancer2 and jQuery
Dave Cross
Dave Cross

Posted on

In-place editing with Dancer2 and jQuery

I really enjoyed reading Mark Gardner's blog post about web frameworks this week, but in one way it made me feel slightly sad.

It made me sad because it got me thinking about how a lot of Perl web development seems to be stuck in the past. Oh, don't get me wrong - I know that modern Perl web frameworks allow you to do anything that you could do with technologies like Node.js. But I don't know how widely-known that information is. If you spend much time reading the Perl questions on Stack Overflow, you'll soon realise just how much Perl web development still consists of CGI programs running in a LAMP stack. For many of the people asking for help there, it seems that even using a templating engine to separate your HTML from your code is cutting-edge technology that they would rather avoid. And that means that other modern web technologies completely pass them by.

But there's no need for that at all. A lot of the coolest modern web technologies happen at the front-end, in the browser. And the browser doesn't care at all what technologies you're using at the back end. As long as the browser is given valid HTML, CSS and Javascript to work with, it doesn't matter if that was generated using Perl, Node.js or whatever other technology is trending this week.

So I thought it was worth writing a short series of articles demonstrating how you might do some clever-looking modern web development tricks using Perl at the back end. We'll start with in-place editing of data.

Start with Dancer2

I'll start with a basic Dancer2 application. Let's pretend we're a freelance developer of some kind and we have many projects for different clients in progress at the same time. At the basic level, you'd like to see what projects you are currently working on. A useful web page might look like this.

Basic client/project information

Here we have two clients and there are two projects currently live for each of them.

This is a very basic Dancer2 application. There are two tables, called client and project. I've used Dancer2::Plugin::DBIC to make it easy to access my database from my application. There's a single route that displays this page and the page has been made to look reasonably well-designed by using Bootstrap.

The Dancer code is trivial:

package APIDemo;
use Dancer2;
use Dancer2::Plugin::DBIC;

get '/' => sub {
  template 'index' => {
    title => 'APIDemo',
    clients => [ schema->resultset('Client')->all ],

Enter fullscreen mode Exit fullscreen mode

And the relevant part of the template is just a couple of simple loops:

<table class="table">
<% FOR client IN clients -%>
    <td><% client.code %></td>
    <td><% %></td>
    <td><% %></td>
<% FOR project IN client.projects -%>
        <li><% project.code %> / <% %></li>
<% END -%>
<% END -%>
Enter fullscreen mode Exit fullscreen mode

In-place editing

So far, so good (I hope). But let's get a bit cleverer. I'm sure you've all seen web pages where you can edit text just by clicking on the page, changing the text and hitting a tick button to save your changes. Let's add that feature to this page.

A system like this needs two components:

  • Some Javascript that gives the user an edit box when they click the text and then makes an AJAX call to a back-end API to save the changes
  • Some code at the back end that handles that AJAX call and saves the new data to the database

Let's look at the back end first, because that's the Perl bit.

Perl at the back end

There are a couple of approaches to this. If you're a REST API purist, then you'll want to write PATCH endpoints for all of the resources that can be updated in this way. That's a good way to go, but you can end up writing a lot of very similar code. I took a different approach.

I wrote an API endpoint that expects to be given the type of object that is being updated - because that way the same code can be used for all of your updates. The code looks like this:

patch '/api/update_attribute' => sub {

  my $input = decode_json(request->content);

  my $rs = schema->resultset($input->{resultset});

  my $object = $rs->find($input->{id});

  if ($object) {


  return 200;
Enter fullscreen mode Exit fullscreen mode

Note that in a production system, you would probably want to keep your API in a separate application to your web site. As this is a simple example, they're both in the same application and I've just used the prefix /api for the API call.

The code isn't complicated though. We expect a JSON payload which we can decode to get access to our parameters. One parameter is the name of the resultset that we need to query and another is the ID of the actual object we're updating. Taking those together, we can use the resultset's find() method to get our object from the database. We expect the update parameter to be a hash mapping keys to new values that we can pass directly to the object's update() method. Our current example will only be updating one attribute at a time, but we've written future-proof code that can handle multiple attributes at the same time.

The code could be cleverer. Note that if we fail to find the object, we just skip the update. We should probably return 404 at that point. We also need to check that we're passed meaningful sets of parameters and take appropriate action if, for example, the resultset is missing. But this is enough for a proof of concept.

jQuery at the front end

Now it's time to look at the front-end. I mentioned before that the web page was designed using Bootstrap. And one of the nice things about Bootstrap is that it uses jQuery for all of its front-end cleverness. So it's likely that a page that uses Bootstrap already loads the jQuery core library. We just need to look for an add-on that will support the feature that we need to add. A couple of minutes with Google led me to jQuery-editable which does everything we need.

Basically, jQuery-editable can be attached to any HTML element on your page. The approach recommended in the documentation is to add the class "editable" to any elements that you want to make editable. You can then use the Javascript document ready event to set up the plugin. Doing this in a document ready event is a pretty standard approach when using jQuery. I created a file called apidemo.js which is loaded in my main layout template and which contains this:

$( document ).ready(function() {

  $('.editable').editable(function(value, settings) {
    var data = {
      resultset: this.dataset.resultset,

    var update = {};
    update[this.dataset.attribute] = value;

    data.update = update;

    var url = '/api/update_attribute';

      url: url,
      type: 'PATCH',
      data: JSON.stringify(data),
      success: function(data) {

  }, {
    submit: '&check;',
    cancel: 'X',
    showfn : function(elem) { elem.fadeIn('slow') },
    cancelcssclass : 'btn btn-danger',
    submitcssclass : 'btn btn-success'
Enter fullscreen mode Exit fullscreen mode

Most of this is concerned with setting up the editable() function which gets called whenever the user edits the value in the HTML element.

We start by creating a data object that will be passed as the payload in our AJAX request. The first two attributes we set up in the object are resultset and id which (as we saw in the Perl code) are used to find the correct database row to update. We get these values from the dataset attributes attached to the current HTML element. These are the data-* attributes that you might have seen in modern HTML. You can add any attributes you like to an HTML element and the value is readable from Javascript. This means that we need to alter our templates slightly so this information is added. For example, the <td> element around the client name would look like this:

<td class="editable" data-resultset="Client"
    data-attribute="name" data-id="<% %>">
  <% %>
Enter fullscreen mode Exit fullscreen mode

We've added the class="editable" so we can identify this as an editable element. We've also added the data-resultset, data-attribute and data-id attributes. Some of these values are hard-coded, but the data-id value comes from the client object that we're displaying on this row of the table.

So, we've put the resultset and id in the data object. We then create another object that maps the attribute name to the value that is passed to the function (which will be the new value of the attribute). By creating a Javascript object like this, we end up in the Perl code with a hash that can be passed directly to the update() (as we saw above). We then take this update object and store it in data.

The next step is to make the AJAX call that actually updates the database. We use JSON.stringify() to encode our data object into JSON (so that the Perl code can decode the JSON to a Perl data structure). We basically ignore the value returned from the AJAX call (just writing it to the console) but a more rigorous version of this code would want to look for and deal with any error responses.

The rest of the setup is cosmetic. We set the labels for the submit and cancel buttons and use the CSS options to give the buttons classes that Bootstrap will recognise. Finally, we've written a function that means the edit box will fade in slowly.

And that's it. Save all of the files, restart your Dancer app and reload it. Nothing will look different, but if you click on a client name you'll see this.

Editing a client's name

You can then edit the name and when you submit the changes, they get saved to your database.

The nice thing about this approach is that, having set it all up, it's really easy to make it work for any other data item displayed on the page. You just need to add the editable class and the relevant data-* attributes.

The code for this demo app is available on GitHub and in that version you can edit the client name and email address, together with the project name.

I hope this starts to show you how you Perl can be used to build web apps that have all the bells and whistles that uses expect. If there's any particular modern web feature that you'd like to see me explore in this series, then let me know in the comments and I'll see what I can do.

Discussion (5)

bbrtj profile image

Sadlly jQuery is also perceived as something very outdated. But I guess you wanted to show how to do this without setting up all the stuff needed for a framework (npm, dependencies, webpack, necessary loaders to name some of them). That's a nice middle-ground

davorg profile image
Dave Cross Author

Honestly I did it this way because I wasn't aware that jQuery was considered outdated. I'm not much of a Javascript developer and I still thought that my meagre jQuery skills were pretty neat.

But I've just been doing some reading and it seems you're right. Apparently most of the things that jQuery was useful are now included in standard JS.

So now I'm wondering if my next post in this series should just be updating the front-end code to use standard JS instead. And then, maybe, I could do another version using a front-end framework like React (or whatever's cool these days).

Oh. I've just remembered another reason why I use jQuery. As you'll see from this example, I often use Bootstrap to make my web apps look nice. And Bootstrap already brings in jQuery.

bbrtj profile image

I'm not a js expert in the slightest, but if you want to use the newest JS features then you're probably going to webpack it too to make sure it will work on all browsers, and that requires some setup. JQuery is low effort, you can just add a jquery script to your page and use it right away. Other options require much more setup and I always found it rather tedious.

When it comes to frameworks, vue.js is pretty cool. You will probably like it if you liked jquery, since it does not necessarily mean writing single page applications and can just make it easier to write some regular JS elements on a page.

mjgardner profile image
Mark Gardner

Thanks for the shout-out! This is a great example of modern web features with a Perl back-end. We need more of these.

davorg profile image
Dave Cross Author

We need more of these.

That's the plan :-)