Adding WordPress-like tags to a Symfony 1.4 admin generator form
Update 2010-08-06: It looks like the sfDoctrineActAsTaggablePlugin team has incorporated much of this into the version they released today. I’ll post more when I get a chance to try it out.
This week I used the sfDoctrineActAsTaggablePlugin to add tag behavior to a Symfony project. My goal was to have a user interface similar to WordPress’s, which I like a lot:
Another goal was to use JQuery UI with the nice visual theme I built with their Theme Roller tool (which I had already installed and added to my symfony project).
The documentation was a little bit sparse and I’ll probably end up doing this again, so here are relevant instructions so Future Me and others may also benefit:
Set up
- get/install/checkout sfDoctrineActAsTaggablePlugin. I use this line in the `svn:externals` property on plugins/:
sfDoctrineActAsTaggablePlugin/ http://svn.symfony-project.com/plugins/sfDoctrineActAsTaggablePlugin/tags/RELEASE_1_0_0
- enable the plugin in config/ProjectConfiguration.class.php.
$this->enablePlugins( 'sfDoctrinePlugin', ... 'sfDoctrineActAsTaggablePlugin' );
- Add the Taggable behavior to your model(s) in config/doctrine/schema.yml
(not
actAs: { Timestampable: ~ , Taggable: ~ }
templates: [Taggable]
as the README says) - Rebuild your model and forms and everything. My favorite way is to dump the old data to fixtures, then rebuild everything.
./symfony --color doctrine:data-dump ./symfony --color doctrine:build --all --and-load --no-confirmation
At this point you should have new database tables tag
and tagging
, and the proper model relations and everything.
Form fields for tags

UI for entering tags, displaying existing tags (with remove buttons)
I’m going to have
- a single text input field for adding tags,
- a link to display a tag cloud (see below) if the user wants to pick tags by clicking instead of typing
- a styled list of existing tags with delete icons
- a hidden field for storing tags to be removed after clicking on the delete icons.
The delete icons will trigger a Javascript function which populates the hidden field and hides the tag. Changes won’t be saved until the user submits the form, so we’ll also provide a little reminder.

- add the tag field(s) to the
configure()
method of your form, in lib/form/doctrine/ModelForm.class.php.// this text appears in gray until the user focuses on the field $default = 'Add tags with commas'; $this->widgetSchema['new_tags'] = new sfWidgetFormInput ( array('label' => 'Add Tags', 'default' => $default), array( 'onclick' => "if (this.value=='$default') { this.value = ''; this.style.color='black'; }", 'size' => '32', 'id' => 'new_tags', // don't let the browser autocomplete. We'll add typeahead, below 'autocomplete' => "off", 'style' => 'color:#aaa' ) ); // allow the field to remain blank $this->setValidator('new_tags', new sfValidatorString(array('required' => false))); // this hidden field will be populated with JavaScript. $this->widgetSchema['remove_tags'] = new sfWidgetFormInputHidden(); $this->setValidator('remove_tags', new sfValidatorString(array('required' => false)));
- Add a partial with the admin generator. In apps/frontend/modules/mymodule/config/generator.yml:
form: display: # add another section after the other fields... Tags: [_tags]
- edit the partial, apps/frontend/modules/mymodule/templates/_tags.php
<?php use_helper('JavascriptBase', 'Tags') ?> <?php // much of this I copied and adapted from a cached admin generator template. $name = 'new_tags'; $label = ''; $help = ''; $class = 'sf_admin_form_row sf_admin_text sf_admin_form_field_tags'; ?> <div class="<?php echo $class ?><?php $form[$name]->hasError() and print ' errors' ?>"> <?php echo $form[$name]->renderError() ?> <div> <?php echo $form[$name]->renderLabel($label) ?> <div class="content"><?php echo $form[$name]->render($attributes instanceof sfOutputEscaper ? $attributes->getRawValue() : $attributes) ?> <?php // tag cloud will go here, see below ?> </div> <?php if ($help): ?> <div class="help"><?php echo __($help, array(), 'messages') ?></div> <?php elseif ($help = $form[$name]->renderHelp()): ?> <div class="help"><?php echo $help ?></div> <?php endif; ?> </div> </div> <?php // list of current tags, with remove buttons ?> <div class="sf_admin_form_row sf_admin_text sf_admin_form_field_tags"> <div> <label>Current tags</label> <div class="content"> <div class="taglist"> <?php foreach ( $form->getObject()->getTags() as $t): ?> <span><nobr><?php echo link_to_function("Remove '$t'", "remove_tag(".json_encode($t).", this.parentElement)", "class=removetag") ?> <?php echo $t ?></nobr></span> <?php endforeach; ?> </div> <span id="remove_tag_help" style="display:none;">Tag(s) removed. Remember to save the complaint.</span> </div> </div> </div>
- Add a Javascript function to support the “remove tag” buttons.
$(function() { // add fancy jQuery UI button styles. See additional in "CSS" below $(".taglist a").button({icons:{primary:'ui-icon-trash'}, text: false}); }); function remove_tag (tag, element) { remove_field = $("#complaint_remove_tags"); if ( remove_field.val().length ) { remove_field.val( remove_field.val() + "," + tag ); } else { remove_field.val( tag ); } $(element).hide(); $("#remove_tag_help").show(); }
- edit the `processForm` action to process the `new_tags` and `remove_tags` fields. I started by copying the `processForm` method from cache/frontend/dev/modules/autoMymodel/actions/actions.class.php into apps/frontend/modules/mymodule/actions/actions.class.php, then added code after validation:
protected function processForm(sfWebRequest $request, sfForm $form) { $form->bind($request->getParameter($form->getName()), $request->getFiles($form->getName())); if ($form->isValid()) { $notice = $form->getObject()->isNew() ? 'The item was created successfully.' : 'The item was updated successfully.'; // NEW: deal with tags if ($form->getValue('remove_tags')) { foreach (preg_split('/\s*,\s*/', $form->getValue('remove_tags')) as $tag) { $form->getObject()->removeTag($tag); } } if ($form->getValue('new_tags')) { foreach (preg_split('/\s*,\s*/', $form->getValue('new_tags')) as $tag) { // sorry, it would be better to not hard-code this string if ($tag == 'Add tags with commas') continue; $form->getObject()->addTag($tag); } } try { $complaint = $form->save(); // and the remainder is just pasted from the generated actions file
Yay! Now I can add, display, and remove tags. Now for the fancy parts.
Typeahead tag autocompletion
As detailed in the README, sfDoctrineActAsTaggablePlugin has typeahead support. I tried it, but I didn’t feel like the user interaction was quite smooth enough, and I wanted to use the excellent, beautiful, and user-expectation-meeting jQuery UI Autocomplete widget.
I ended up using the action provided by sfDoctrineActAsTaggablePlugin, but modified the view to return JSON instead of an HTML <ul>, and I used the multiple, remote demo code from the jQuery UI Autocomplete documentation.
- override the
completeSuccess
view for thetaggableComplete/complete
actionAnd here is my new template:mkdir -p apps/frontend/modules/taggableComplete/templates touch apps/frontend/modules/taggableComplete/templates/completeSuccess.php
You can test your autocomplete action with a URL like<?php // we're rewriting the view for the taggable plugin to output JSON instead of HTML // see http://wiki.jqueryui.com/Autocomplete $tags_simple = array(); foreach ( $tagSuggestions as $suggestion ) { $tags_simple[] = $suggestion['suggested']; } echo json_encode($tags_simple);
http://myserver/myproject/taggableComplete/complete/current/p
. In my case (because I have some data in mytag
table) it returns this JSON-formatted array:["PHP","plugins"]
- Add jQuery UI javascript. I used a
<script>
section in my _tags.php partial, but you could also use an external javascript file (but mind the line of PHP code using theurl_for()
helper).$(function() { // for debug info, uncomment these lines and add a <div id="autocomplete_log"> // function log(message) { // $("<div/>").text(message).prependTo("#autocomplete_log"); // $("#autocomplete_log").attr("scrollTop", 0); // } function split(val) { return val.split(/\s*,\s*/); } function extractLast(term) { last = split(term).pop(); // log ( "extracted last = "+last ); return last; } $("#new_tags").autocomplete({ source: function(request, response) { $.getJSON(<?php echo json_encode(url_for("taggableComplete/complete")) ?>, { current: extractLast(request.term) }, response); }, search: function() { // custom minLength var term = extractLast(this.value); if (term.length < 1) { return false; } }, focus: function(event, ui) { // prevent value inserted on focus return false; }, select: function(event, ui) { var terms = split( this.value ); // remove the current input terms.pop(); // add the selected item terms.push( ui.item.value ); // add placeholder to get the comma-and-space at the end terms.push(""); this.value = terms.join(", "); return false; } }); });
tag cloud

I like how WordPress has an option for picking tags from a tag cloud too. The sfDoctrineActAsTaggablePlugin made this nice and easy.
- Here’s the code for the view (in the _tags.php partial)
<?php echo link_to_function("Choose from the most used tags >>", '$("#add_tag_from_cloud").show(); $(this).hide()' ) ?> <div id="add_tag_from_cloud" class="tag_cloud popular" style="display:none"> <h3>Popular tags</h3> <?php // gets the popular tags $tags = PluginTagTable::getPopulars(); // Display the tags cloud, using link_to_function() instead of link_to() // The %s in the second arg will be substituted with the tag text. echo tag_cloud($tags, 'add_tag("%s")', array( 'link_function' => 'link_to_function', 'link_options' => array('class=addtag') ) ); ?> </div>
- And here’s the javascript to handle those links.
function add_tag (tag) { add_field = $("#new_tags"); if ( add_field.val() == "Add tags with commas" ) { add_field.val( tag ); add_field.css("color", "black"); } else if ( add_field.val().length ) { add_field.val( add_field.val() + ", " + tag ); } else { add_field.val( tag ); } $(element).hide(); }
CSS
I made lots of incremental changes to my CSS but here are all of the sections relevant to the tag stuff I’ve shown here, I think.
.taglist .removetag { vertical-align: middle; width: 18px; height: 18px; } .taglist span { margin-right: 1.4em; } .tag_cloud li { display: inline; list-style: none; } #sf_admin_container .tag_cloud li a { background: transparent; padding-left: 0px; } |
And I had this in my apps/frontend/templates/layout.php to apply JQuery UI button styling to all my form buttons and hyperlinks with the “button” CSS class:
<script type="text/javascript"> // use JQueryUI buttons $(function() { $("input:submit, button, a.button").button(); }); </script> |
Yay! I’m pretty happy with it so far. I hacked at it for a couple days though and have been cloudy with fighting a cold, so if I’ve missed anything let me know. I think this is enough to get me there more quickly the next time.
One thing I have noticed with model’s that are created via behaviors is that I ran into issues when trying to create an admin generator module for them. Did you create a custom module to maintain the tags (I would like a way to add new tags manually as well as rename and merge tags).
[…] Adding WordPress-like tags to a Symfony 1.4 admin generator form. This entry was posted in Lifestream. Bookmark the permalink. ← May […]
Hi Lukas,
I haven’t made a module for editing tags yet. Tags get automatically added when users type a tag that doesn’t exist yet, so that’s not necessary. Unused tags will get cleaned up by the “taggable:clean” task.
That said, since the models are all built, I found I could easily make a basic admin generator module just by saying
./symfony doctrine:generate-admin frontend Tag
That lets me add, edit (rename) or delete tags right out of the box without messing with the taggable models. Adding a merge feature would just require a custom action, I think; the admin generator isn’t going to do that sort of complexity for you.
In my project we have tags that are actually more like “phrases” would be nice to have the option if you want to only split on comma or spaces ..
Plus I would like some way to actively discourage creation of new tags. Maybe require users to conform if they are adding a tag that doesn’t exist yet.
The way these work right now, it only splits on commas, so it allows multi-word tags that include spaces. I think that’s what you mean, right?
It should also be pretty easy add more javascript to the autocomplete handler to discourage tag creation.
ok thx .. i am pondering to add support for end user tagging over here: http://search.un-informed.org/clause/300
so i might need to sit down and write all of the above little additions, was jsut hoping to get you to write them for me 🙂
I haven’t had a chance to try it yet, but it looks like the sfDoctrineActAsTaggablePlugin team has incorporated most of this into their release today. Yay!
Great post Nathan. Only issue I ran into was using this.parentElement, which seems to have problems in firefox. I replaced it with this.parentNode and it seems to be working. Thank you,
James
Hi
I have the issues with this tutorial. I can’t post my form beacause all I got is validation which is always false. I dug the internet and my project and can’t find where is the bug. Any suggestions?
Doublecheck your
$this->setValidator ... required=>false
lines from the lib/form/doctrine/ModelForm.class.php code sample above?Hi, I’ve a form with city field autocompleted using sfFormExtraPlugin. I followed your post, and I can’ t handle tag autocompletion.
This problem is actually not related to your post or sfDoctrineActAsTaggablePlugin.
Code generated by widget sfWidgetFormDoctrineJQueryAutocompleter from sfFormExtraPlugin, blocks somehow jquery UI autocomplete behavior.
Have you encountered this problem? Can you prompt me possible solution?
Thenks for your post, it helps me a lot. Below is code generated by sfWidgetFormDoctrineJQueryAutocompleter.
jQuery(document).ready(function() { jQuery(“#autocomplete_company_city_id”) .autocomplete(‘/frontend_dev.php/company/ajaxListener/city’, jQuery.extend({}, { dataType: ‘json’, parse: function(data) { var parsed = []; for (key in data) { parsed[parsed.length] = { data: [ data[key], key ], value: data[key], result: data[key] }; } return parsed; } }, { minChars: 2, max: 10, highlight: false, scroll: true, scrollHeight: 300, })) .result(function(event, data) { jQuery(“#company_city_id”).val(data[1]); }); });
Hi kolesGit,
sfFormExtraPlugin actually uses a very old autocompleter jQuery plugin, not the more modern and documented jQueryUI one. For one thing, it expects a totally different format from the JSON action.
Just last week I tore out sfFormExtraPlugin’s autocompleter on a Symfony project in favor of the jQueryUI one, which I think is demonstrated here. Don’t get confused by sfFormExtraPlugin’s autocompleter!
Oh man… Thanks a lot. I perfectly embedded your tag system in my project and works like a charm.