Skip to Main Content

Editing a Front End Form with Matrix

This tutorial demonstrates how to create a front end entry form with Craft CMS that works with the Matrix, Store Hours, Lightswitch, and other standard field types. The tutorial allows the manager of a store to edit their store entry. Part 2 will demonstrate how the manager can edit and jobs, created by the manager as well as head office, and create new jobs in the jobs channel

Requirements

I built this functionality for another agency where their client requested that managers be able to edit the content of their stores as well as create job postings and edit job postings that were created by head office. All of the editing/creating was to be done on the front end of the site as head office does not want managers to have access to the Craft Control panel.

I found the following links to be helpful in building this out and include here as citation.

The html in this tutorial includes limited styling for a couple of reasons. The form was built before the page was styled so that we could ensure that all the functionality was working first and then build in the styles. Second anyone using this tutorial is going to want to style it themselves. Having said that there are some bootstrap classes in use.

Let's Begin

Feel free to skip to the Matrix Field Section if that's what you are looking for.

The edit store form doesn't include every field because it's not going to be necessary for the manager to change the store address and some other options. This helped keep the form simpler, though it is still rather long and complex. To keep the code readable I broke the form out into various includes. Here you can see the form without the includes.

    
    
      {# set segment_2 variable #}
{% set segment_2 = craft.request.getSegment(2) %}

<div class="container">
	<div class="row">
		<div class="col-sm-12">

		{% if entry is not defined %}
			{% set entry = craft.entries.slug(segment_2).first() %}
		{% endif %}

		{% if currentUser %}
			<p><a href="{{ logoutUrl }}">Logout</a></p>
			<p><a href="/store/store-list">view all stores</a> you can edit.</p>
			<p><a href="/store/{{ segment_2 }}" target="_blank">View this stores page</a></p>
		{% else %}
			{% redirect "store/" ~ segment_2 ~ "/login" %}
		{% endif %}

		<form method="post" accept-charset="UTF-8" enctype="multipart/form-data" class="edit-store">
			{{ getCsrfInput() }}
			<input type="hidden" name="action" value="entries/saveEntry">
			<input type="hidden" name="redirect" value="store/{{ entry.slug }}/success">
			<input type="hidden" name="sectionId" value="4">
			<input type="hidden" name="enabled" value="1">
			<input type="hidden" name="entryId" value="{{ entry.id }}">

			{# various includes here #}

			<div class="row">
				<div class="col-sm-12">
					<input type="submit" value="Save" class="btn btn-primary">
				</div><!-- /.col-sm-12 -->
			</div><!-- /.row -->
		</form>

		</div><!-- /.col-sm-12 -->
	</div><!-- /.row -->
</div><!-- /.container -->
    
  

The first line I set segment_2 to use further down in the template and it's easier than writing out craft.request.getSegment(2) all the time.

Next we tell the form what entry to edit, which is based off of segment_2. Then we check that the visitor is logged in and if they are we show a list of stores they can edit as well as a link to the public store page and a logout link.

If your form includes options to upload images or other files ensure that your form tag includes enctype="multipart/form-data".

The next 5 lines are from the official docs sample form. Be sure to change Section Id to the id of the channel you want to edit. The next line tells the form which entry to edit as we're not creating a new store.

Input Fields

The first include has several basic input fields. Only one listed here for brevity and it's easy to copy/paste and make changes as needed. the key to note is that the field name is always like this name=fields[phoneNumber].

Additionally because we are editing the store we want to display any content that may already exist. {%- if entry is defined %} value="{{ entry.phoneNumber }}"{% endif -%}

    
    
      {# start phone #}
<div class="form-group">
    <label for="phoneNumber" class="col-sm-2 col-form-label">Phone</label>
<input id="title" class="form-control" type="text" name="fields[phoneNumber]"
    {%- if entry is defined %} value="{{ entry.phoneNumber }}"{% endif -%}>
</div><!-- /.form-group row -->
    
  

Redactor Fields

With redactor fields you want to initialize the redactor rich text editor. I'm using the redactor that ships with Craft because it's simpler, but in this Stack Exchange Post it is recommended to install your own. First you need to include the following, which I did in the head of the page.

    
    
      {# for front end form redactor field #}
  {% includeJsResource "lib/redactor/redactor.js" %}
  {% includeCssResource "lib/redactor/redactor.css" %}
{# end for front end form #}
    
  

Then the rich text field itself.

    
    
      <div class="row">
  <div class="form-group">
    <label for="exampleTextarea">Main Copy</label>
    <textarea class="form-control maincopy" id="maincopy" rows="3" name="fields[mainCopy]">{{ entry.mainCopy }}</textarea>
  </div>
  {% includeJs "$('.maincopy').redactor();" %}
</div><!-- /.row -->
    
  

Lightswitch Field

Next a lightswitch field which on the front end is set as radio buttons. The key is the conditional around checked="checked" in each option. 0 is off and 1 is on.

    
    
      <div class="row">
	<div class="form-check">
	  <label class="form-check-label">
	    <input type="radio" class="form-check-input" name="fields[donationPickup]" id="fields[donationPickup]" value="1" {% if entry.donationPickup %}checked="checked"{% endif %}>
	    Donation Pickup = Yes
	  </label>
	</div>
	<div class="form-check">
	  <label class="form-check-label">
	    <input type="radio" class="form-check-input" name="fields[donationPickup]" id="fields[donationPickup]" value="0" {% if entry.donationPickup =="0" %}checked="checked"{% endif %}>
	    Donation Pickup = No
	  </label>
	</div>
</div><!-- /.row -->
    
  

Store Hours Field

The store hours field type had me stumped. However it is used twice for each store, once for operating hours and another for donation hours so it was important to get it working. In the Craft CMS Slack channel Brandon Kelly instructed me to inspect how it displays in the control panel. I did that and then combined it with how I am displaying hours on the front-end of the site, detailed here. The final html/twig code is:

    
    
      <div class="row">
	<table class="data">
		<thead>
			<tr>
				<td></td>
				<th>Opening Time</th>
				<th>Closing Time</th>
			</tr>
		</thead>
		<tbody>

		{% set days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] %}
		{% for dayHours in entry.donationHours %}

		<tr>
			<th>
			{{ days[loop.index0] }}
			</th>
			<td>
				<input class="text ui-timepicker-input timepicker" type="text" id="fields-donationHours-{{ loop.index0 }}-open-time" size="10" name="fields[donationHours][{{ loop.index0 }}][open][time]" value="{% if dayHours.open|length %}{{ dayHours.open|date('h:i a') }}{% endif %}" autocomplete="off">
			</td>
			<td>
				<input class="text ui-timepicker-input timepicker" type="text" id="fields-donationHours-{{ loop.index0 }}-close-time" size="10" name="fields[donationHours][{{ loop.index0 }}][close][time]" value="{% if dayHours.close|length %}{{ dayHours.close|date('h:i a') }}{% endif %}" autocomplete="off">
			</td>
		</tr>

		{% endfor %}
		</tbody>
	</table>
</div><!-- /.row -->
    
  

Next I pass some javascript to the layout template through a javascript block

    
    
      {#jquery time picker for store edit page #}
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-timepicker/1.10.0/jquery.timepicker.js" integrity="sha256-FaYRflg3IJpVUN+JgijEHFUYgsn1wS1xRlcKTIgToIo=" crossorigin="anonymous"></script>

// timepicker for hours of operation and donation hours
$('.timepicker').timepicker();
    
  

Related Entries

The next section of the form is options to add jobs available at this store's location. Jobs are divided into two types: Volunteer and Paid - which have corresponding entries fields in the store channel. The types are determined by a radio field in the jobs channel. The code below is used twice with changes to filter by position type.

    
    
      <div class="row" >
	{# volunteer positions #}
	{# get all entries from volunteerPostion field#}
	{% set entries = entry.volunteerPositions %}

	{# create array of entries that are already related #}
	{% set selectedEntries = [] %}

	{% for entry in entries %}
		{% set selectedEntries = selectedEntries|merge([entry.id]) %}
	{% endfor %}

	<div class="form-group">
		<label for="volunteerPositions">Volunteer Job Positions <a href="/careers/new" target="_blank">Create a new volunteer position</a></label>
		<select multiple="" class="form-control" id="volunteerPositions" name="fields[volunteerPositions][]">
			{% for volunteer in craft.entries.section('careers').search('positionType:volunteerPosition') %}
				<option value="{{volunteer.id}}" {% if volunteer.id in selectedEntries %}selected{% endif %}>{{volunteer.title}} | {{volunteer.author }} </option>
			{% endfor %}
		</select>
	</div>
	{# end volunteer positions #}
</div><!-- /.row -->
    
  

First we set entries = to the volunteer field and create an empty array of entries and then merge it with entries that are already related to this entry. The for loop around the option tag finds all volunteer jobs (by filtering by the radio field mentioned above). This creates a list of all available volunteer positions to choose from and pre-selects those which have ids matching from the selectedEntries array.

Matrix Neo Stops Bullets

The Matrix Field

This is the most complex part of this form and was the trickiest to get working. The matrix field is used for the staff at this particular branch. There is only one block type, which makes things much simpler, and that block type has 4 fields: 2 inputs, an assets field for photos, and a redactor field for the bio.

I'm going to break this down into chunks and then at the end show the full code block. First you need to add a hidden field with the name of the matrix itself like this:

    
    
      {# what matrix field is in the for loop - don't remove this (keep it out of the forloop)#}
<input type="hidden" name="fields[staffProfiles]" />
    
  

Next we add the for loop so we can see all staffblocks that have already been added to this entry. Here I've Added the name of the staff member we're editing. and then a wrapping div that contains all the fields. What's important here are the two required hidden fields. The first field indicates the block type and the second one indicates that the block is enabled.

Notice the redactor javascript is getting included here. it's important to keep it out of the for loop or else it will create duplicates for every block.

    
    
      {% for block in entry.staffProfiles %}
	<h3>{{ block.employeeName }}</h3>

	<div class="matrixblock ">
		{# hidden field with block type that is being updated #}
		<input type="hidden" name="fields[staffProfiles][{{ block.id }}][type]" value="employee">
		{# is this block enabled #}
		<input type="hidden" name="fields[staffProfiles][{{ block.id }}][enabled]" value="1">
	</div>
{% endfor %}

{# keep this out of the for loop or it creates duplicates #}
{% includeJs "$('.employeeBio').redactor();" %}
    
  

Next inside the matrixblock div above we add the 4 fields that can be edited by the store manager. The first part of this code displays the current photo if it exists. The transform is not included in the code block, but is referenced in the image tag.

Notice that the field names are now getting longer as it is required to include the matrix field name and then the sub field name as arrays like this: name="fields[staffProfiles][{{ block.id }}][fields][employeeName]" block.id outputs the current block of the matrix to ensure any edits are saved correctly.

    
    
      <div class="col-sm-6">

	{% if block.photo|length %}
	{% set staffPhoto = block.photo.first() %}
		<img src="{{ staffPhoto.url(croppedStaff) }}" alt="" class="person" />
	{% else %}
		<i class="fa fa-user" aria-hidden="true"></i>
	{% endif %}
	<p>save entry to update photo</p>
</div><!-- /.col-sm-6 -->

<div class="col-sm-6">
{# 
	https://craftcms.com/docs/assets-fields#uploading-files-from-front-end-entry-forms
#}
	<input type="file" name="fields[staffProfiles][{{ block.id }}][fields][photo]">
</div><!-- /.col-sm-6 -->

<div class="col-sm-6">
	<input type="text" name="fields[staffProfiles][{{ block.id }}][fields][employeeName]" value="{{ block.employeeName }}">
</div><!-- /.col-sm-6 -->
<div class="col-sm-6">
	<input type="text" name="fields[staffProfiles][{{ block.id }}][fields][jobTitle]" value="{{ block.jobTitle }}">
</div><!-- /.col-sm-6 -->

<div class="col-sm-8">
	<textarea class="employeeBio" name="fields[staffProfiles][{{ block.id }}][fields][bio]">{{ block.bio }}</textarea>
</div><!-- /.col-sm-8 -->

{# Delete staff member #}
<a href="" class="remove-row"><i class="fa fa-times"></i></a>
    
  

Deleting a staff member

Deleting a staff member occurs on click of the .remove-row link at the bottom of each block. On the main edit template in addition to the content block there is a javascript block which has both the new person and remove person code. Below is the remove person section.

    
    
      // remove a person or row
// bind ajax content to the document so it also can be deleted
$(document).on("click", ".remove-row", function(e){
	e.preventDefault();

	// find parent div and remove it
	$(this).parent().fadeOut("slow", function(){
		$(this).remove();
	});
})
    
  

Add a Staff Member

This part was the trickiest. What I wanted to do was be able to add as many new staff members as you wanted. However I ran into an issue where I couldn't get the new block to have unique names. The compromise was to add a person and then fade out the add button and replace it with a message to save the entry before adding a new person. There are advantages to this as it forces the manager to save more often and thus reducing the chance of accidentally losing changes.

    
    
      {# new row #}
<div class="newBlock">
</div><!-- /.matrixblock new -->
{# end new row #}

<button type="button" id="addPerson">Add Person</button>

<div class="addMorePeople bg-danger text-white" style="display:none;margin-top:30px;margin-bottom:30px;">
	<h3>You must save your entry to add more staff</h3>
</div><!-- /.addMorePeople -->
    
  

The next bit of HTML is where we add the new person. The .newBlock div has another template, /store/newperson, added in via ajax when the add person button is clicked. The .addMorePeople div appears after the addperson button disappears.

The Javascript:

    
    
      // add a new person
$('#addPerson').click(function(e){
	e.preventDefault();
	console.log('clicked addPerson');

	var newPerson = "<div class='newperson'></div>";
	$('.newBlock').append(newPerson);
	$('.newBlock .newperson:last-child').load('/store/newperson');

	// hide add button so that they must save to add more
	$(this).fadeOut("slow", function(){
		$this.remove();
	});
	$('.addMorePeople').fadeIn('slow');

	{# load redactor on the new row #}
	{% includeJs "$('.newperson:last-child .employeeBio').redactor();" %}
})
    
  

New Person Ajax Template

This code is essentially the same as that for editing existing staff members. However because this person doesn't exist in each field's name attribute we replace [{{ block.id }}] with [new_o] really this could be any value, such as [ foobar_1] as long as it's unique.

The reason I ended restricting new staff members to one at a time is that I was unable to change the [new_0] with javascript after the new person was added in via ajax. I tried several different approaches and nothing worked. If you are able to get this working, send me an email with the details and I'll update this post giving credit where due.

    
    
      {# hidden field with block type that is being updated #}
<input type="hidden" name="fields[staffProfiles][new_0][type]" value="employee">
{# is this block enabled #}
<input type="hidden" name="fields[staffProfiles][new_0][enabled]" value="1">

<div class="col-sm-6">
	<input type="file" name="fields[staffProfiles][new_0][fields][photo]">
</div><!-- /.col-sm-6 -->
<div class="col-sm-6">
	<input type="text" name="fields[staffProfiles][new_0][fields][employeeName]" value="" placeholder="Full Name">
</div><!-- /.col-sm-6 -->
<div class="col-sm-6">
	<input type="text" name="fields[staffProfiles][new_0][fields][jobTitle]" value=""  placeholder="Job Title">
</div><!-- /.col-sm-6 -->

<div class="col-sm-8">
	<textarea class="employeeBio" name="fields[staffProfiles][new_0][fields][bio]"  placeholder="Employee Bio"></textarea>
</div><!-- /.col-sm-8 -->						

<a href="" class="remove-row"><i class="fa fa-times"></i></a>

{# load redactor on the new row #}
{% includeJs "$('.newperson:last-child .employeeBio').redactor();" %}
    
  

Conclusion

Later I will be adding in jQuery sortable to allow reordering of staff members. A matrix with multiple block types would get very complex quickly. I'm not sure that would be worth pursuing.

Doing that I would add one button for each block type called New blockName and ajax in the content like I did here for the person and restrict to adding one of each block type at a time. Each block type could be unique by using [new_foo_0] and [new_bar_0] where foo and bar are different block types.

Comments

Have comments about this post? Send me an email, tweet or a dm in the Craft slack channel.

More Articles You May Like