Bring Matrix Data Editing to the Front-End brought to you by META Q
Interior Ad 3 brought to you by The META Q

Bring Matrix Data Editing to the Front-End

Two of my favorite aspects of using ExpressionEngine are Matrix and Safecracker. In terms of making life easier, they're amazing.

For anyone unfamiliar with it, Safecracker is a fantastic stand-alone entry form - it enables entering entries into EE from the front-end. If you haven't had the opportunity to explore Safecraker, I highly recommend doing so.

In addition to Safecracker, I'm also a huge fan of Pixel and Tonic's Matrix.  As an add-on, Matrix is one of the most timesaving  (and headache-saving) tools to have at your disposal.

Have complex data that you would love to have all in one field? Matrix is the key. It's super easy to output all your data on the front-end as well with its handy tag pairs -- it just makes so many tasks a breeze.

But what if you want to be able to enter and edit on the front-end?

I'll note that you can use Matrix's {field:field_name} method and that will output a table Matrix like in the Control Panel, but it may not provide what you're looking for in terms of look.

There’s a more creative way to do all this, and with a little more flexibility.

Submitting rows to a Matrix: An entry perspective

Understanding how Matrix rows are added is key to understanding how to edit then.

Assuming you have a two column Matrix, you'll have three inputs to manage, and code that looks like this:

{exp:safecracker channel="your_channel" return="/URL_TITLE"}
<input type="hidden" name="title" id="title" value="{current_time format='%M %j%S %Y'}" maxlength="100" onkeyup="liveUrlTitle();">
<input type="hidden" name="url_title" id="url_title" value="">
<input type="hidden" name="entry_date" id="entry_date" value="{entry_date}">

<div class="fields" id="sortable">
 
  <div class="field"><!--THIS IS A MATRIX ROW-->
   <input class="new_row_id" type="hidden" name="field_name[row_order][]" value="row_new_0">
   <input type="text" name="field_name[row_new_0][col_id_1]" value="{col_field_name_1}">
   <input type="text" name="field_name[row_new_0][col_id_2]" value="{col_field_name_2}">
   <span class="delete_row">[x]</span>
  </div>
  
</div>
<div class="buttons">
  <input id="new" type="button" value="add row">       
  <input type="submit" value="Submit">
</div>
{/exp:safecracker}

You'll want to replace “field_name” and “col_field_name_x” with your field names, and “col_id_x” with your column IDs. If you need to find your row IDs, you can either look in the database, or use the {field:field_name} tag and look at what it outputs.

The first input sets the row as a new row, while the following rows are your column data inputs. If we just wanted to create 3 rows of static input, we would duplicate the div “field” two more times and change “row_new_0” in the first input to increase by one. For example, the second iteration of the div would be “row_new_1,” and the third would be “row_new_2.” Since we will be mimicking the CP function, we'll want an “add row” function attached to the button with ID “new.”

It’s important to understand that “row_new_x” does NOT indicate the row_id – it is an identifier to separate it from all other rows that you will be adding. The row ID will be designated from the count in the database.

Next we’re going to add the following to our file:

<script src="http://code.jquery.com/jquery-1.8.1.js"></script>
<script src="http://code.jquery.com/ui/1.10.1/jquery-ui.js"></script>
<script type="text/javascript">
$(function() {
    $( "#sortable" ).sortable({
     containment: "parent"
    });
});
$(document).ready(function(){

$('.fields').on('click', '.delete_row', function(e){

  e.preventDefault();
  $(this).parent().remove();
  if ($('.fields div').length < 1){
   $('#new').val('create first row');
  }
});

var $clone = $(".fields div:eq(0)").clone().html();
var $button_txt = $('#new').val();
count = 1;
$('#new').on('click', function(e){
  e.preventDefault();
  if ($('#new').val() != $button_txt){
   $('#new').val($button_txt);
  }
  var $counter = count++;
  var $new_row = $clone.replace(/row_new_0/g,'row_new_'+$counter);

  $(".fields").append('<div class="field">'+$new_row+'</div>');
});

});

</script>

What we're doing is when clicking the “add row” button, a clone of div.field has been created, and all “row_new_x” values are being replaced by the counter integer. This way, when we add, reorder or delete we will not have a duplicate number -- and therefore no lost data.

I've also added a delete row function and text change for the “row” button, so if all rows are deleted, the user is prompted to create a “first” one. Since we're using drag-and-drop for sorting, it's also good to note that the order in which the rows will be saved is based on the HTML DOM order.

There it is: simple with a hint of complexity.

Change existing Matrix rows for an entry: An editing perspective

Editing from an entry detail view is similar to the way in which we normally output Matrix data. It would look something like this:

{exp:safecracker channel="recipes" return="/URL_TITLE" url_title="{segment_1}"}
<input type="hidden" name="title" id="title" value="{title}" size="50" maxlength="100" onkeyup="liveUrlTitle();">
<input type="hidden" name="url_title" id="url_title" value="{url_title}" maxlength="75" size="50">
<input type="hidden" name="entry_date" id="entry_date" value="{entry_date}" maxlength="23" size="25">

<div class="fields" id="sortable">

  {recipes_ingredients}
  <div class="field"><!--THIS IS A MATRIX ROW-->
   <input type="hidden" name="field_name[row_order][]" value="row_id_{row_id}" />
   <input type="text" id="field_name" name="field_name[row_id_{row_id}][col_id_1]" value="{col_field_name_1}">
   <input type="text" id="field_name" name="field_name[row_id_{row_id}][col_id_2]" value="{col_field_name_2}">
   <input type="checkbox" name="field_name[deleted_rows][]" value="row_id_{row_id}">delete
  </div>
  {/recipes_ingredients}

</div>
<div class="buttons">
   <input type="submit" value="Save">
</div>
{/exp:safecracker}

Our row identifiers are now defined by the row ID. Again, we can sort via jQuery and the rows will be saved in the HTML DOM order in which you have arranged them. The input with name “field_name[deleted_rows][]” will delete the row if checked, then submitted. You could also use Ajax to submit the form and delete– be creative!

Now you can utilize two of the most useful (not to mention just plain cool) ExpressionEngine modules/add-ons together to make magic happen. Let your imagination run wild and find creative uses for using Matrix on the front-end through Safecracker.


Mike Wenger's avatar

Mike Wenger

Front-end designer and developer at Q Digital Studio

Mike Wenger is a front-end designer and developer at Q Digital Studio. Mike’s both a left brain and a right brain kind of guy, and as such, enjoys a nice mix of design and development work. He loves being able to flex his creative muscles and work on his analytical skills. When he wants to flex his physical muscles, Mike likes to spend time in the great outdoors – hiking, biking, backpacking and kayaking.


What others are saying

John

I’m afraid i’m going to be “that guy” that points this out but…I’m not sure that js is going to work as “live” was deprecated as of jquery 1.7 wasn’t it? And you are using jquery 1.8 in the example.

You should be using “on” instead (which obviously has slightly different syntax to be watchful for).

Mike Wenger

John, thanks for your comment. You are correct - .live() was depreciated in 1.7 and removed in 1.9. I have updated the example above to reflect .on() for use with jQuery 1.8.

For anyone else reading this article and these comments, .on() allows for a more efficient method by allowing selection of a static parent, rather than attaching event handlers to the document object as .live() did. It is ok to use $(‘body’), but the more closely you can constrain your selector to the element, the better.

http://api.jquery.com/on/

Seamus Holman

This is a great write-up. I’ve spent far too much time trying to accomplish the same end result with CSS overrides. Never even considered making a custom Matrix field. So much cleaner and efficient this way.

Thanks for sharing.

Matt Green

Good article! I have done this plenty of times but have also learned the hard way when NOT to do it. If you allow new Matrix rows to be generated by users please be aware that Matrix was never intended to house a LOT of rows. It will time out within the control panel in the edit entry template after a certain amount, and slow down page load quite a bit on the front end. I wouldn’t have more than a couple hundred at the MOST, and less than a hundred of you can do that. Even Brandon Kelly(Author of Matrix) will tell you that. IE is especially bad with this.

Also take note that the default entry limit in a channel entries loop, or matrix, is 100 rows. You will need to specify a higher limit if you have more. Just another fun fact that has made me pull out a few hairs before I found that out.

Kind of a tangent but I ran into these things so figured I would share and hopefully help someone out.

Rachel Rine

This is by far the most helpful article I’ve read on the topic. Thank you!

The matrix I’ve created has some rows already created and filled with content. I can edit these existing fields just fine in the form I’ve created, but I can’t figure out how to incorporate the adding and removing rows you’ve demonstrated here. The biggest difference is I don’t want a blank row to show by default. I want the new row to be created when the user clicks the add row button. Can you point me in the direction of how to accomplish this or where to find more documentation?

Thanks!

Mike Wenger

Hi Rachel,

Thats a good question. In the example above I was approaching the matter from the primary two views of Matrix - Adding data from one blank row, and editing existing data. Because there are different name attributes for each, there would need to be some modifications to the above examples to mix the two. What you would essentially want to do is establish the markup for the code either in your JS as a variable, or repeat the row code at the very bottom of the document in a hidden div and clone that like I cloned the first row in the first example above. I’ll provide an example of the JS method (it’s a basic example sans some extras from the example above, like button text, etc):

count = 1;
$(‘form’).on(‘click’,’#new’, function(e){
  e.preventDefault();

  var $counter = count++;
  var $row = $(’<div class=“field”>’
    +  ‘<th class=“matrix matrix-first matrix-tr-header”>’
    +  ‘<input class=“new_row_id” type=“hidden” name=“field_name[row_order][]” value=“row_new_’+$counter+’”>’
    +  ‘<input type=“text” name=“field_name[row_new_’+$counter+’][col_id_1]” value=”“>’
    +  ‘<input type=“text” name=“field_name[row_new_’+$counter+’][col_id_2]” value=”“>’
    +  ‘<span class=“delete_row”>[x]</span>’
    + ‘</div>’);

  $(”.fields”).append($row);
});

So when you add the ‘Add New’ button:
<input id=“new” type=“button” value=“add row”>
to the form, it will grab the ‘template’ in your JS each time the button is clicked and insert the counter. I’ll add that since I wrote this article, this is the method I prefer to use since it doesn’t need an existing row to clone (so you can have your rows without a blank one). Any new rows will have this format (name attribute) anyhow.

Hope that helps!

Hans

Hello Mike,

Thank you much for posting this.  I had a hard time getting safecracker files to work in already existing entries, but I finally did.

In this instance I did not want the front-end user to be able to edit the actual file download, so I kept that hidden.  But to keep it from getting deleted from the entry, I had to add both these lines for the file (even though there is only one file)
88 is simply the number for my column in this instance.

<input type=“hidden” name=“field_name[row_id_{row_id}][col_id_88_hidden]” value=”{col_name}{file_name}{/col_name}” />

<input type=“hidden” name=“field_name[row_id_{row_id}][col_id_88]” value=“field_name[row_id_{row_id}][col_id_88]” />

I then added this so the user can see what the filename is:
<input class=“span3” value=”{col_name}{file_name}{/col_name}” disabled=“true” />

Mike Wenger

Thanks for the addition, Hans!

Lincoln

Thanks so much for the article.

I need to customize a Matrix field that contains a P&T Dropdown field.
Code:
<select name=“skill_level[row_new_0][col_id_46]”>
{options:skill_level}
<option value=”{option_value}”{selected}>{option_name}</option>
{/options:skill_level}
</select>

My code above doesn’t show the select loop with the list of options. Is there a way to do this?


Speak your mind