Published
Monday, April 4, 2011 - 10:57
Written by
There are three ways one can customize the look of CiviCRM pages:
  1. Customizing CiviCRM templates
  2. Custom CSS in a Joomla! template or Drupal theme
  3. Custom jQuery code
In this post we will review them and provide a few examples of the most complicated method, jQuery manipulation. Customizing CiviCRM templates This method is described in depth on our wiki under Theming CiviCRM and the subpages therein. The advantage of this method is that it is fairly easy to make simple changes. The disadvantage is that upgrades may significantly change the core templates and thus require one to redo the customizations “from scratch." Custom CSS in a Joomla! template or Drupal theme If your site uses a custom template or theme, then using normal CSS rules, one can fairly simply override the core CiviCRM CSS rules and replace them with your own custom ones. The advantage of this method is that it is fairly easy to make simple changes. I am not aware of any particular disadvantages of this method and in fact whenever it's feasible, this is the method I would recommend. Custom jQuery code There are some customizations which can not be done via CSS, or which can be done via a custom CiviCRM template, but are quite difficult and there are others which are simply impossible to do purely via a CiviCRM template change. In many instances, however, such customizations can be safely implemented via jQuery. The first step is to put a jQuery file onto your site. For Joomla! sites see Adding Javascript and for Drupal sites see Adding JavaScript to your theme or module and Working with JavaScript and jQuery. There may be other ways to add a JS file to a Joomla! site and it may be possible to achieve the functionality described below with Mootools (which ships with Joomla!). I will leave it up to any Joomla! or Mootools gurus to add their replies regarding such below. Once the file is added to the site, now we just put in it whatever DOM manipulation we want. We will examine a few examples here and the concept will hopefully be clear. Let's start with a simple example. If one has an event page with a long text and map etc. the “Register Now" button may only be found after scrolling down a bit. If a user comes to the page already intending to register, it would be easier for him to have that button right at the top of the page. To move this button we simply use Firebug in Firefox to find the ids or classes of the relevant items, in this case, the button itself and the event description section at the top. Then we move the button like this: if (Drupal.jsEnabled) { $(document).ready(function () { $('div.event_description-section').prepend($('div.register_link-section')); }); } In actuality only the middle line interests us, because the rest is just a wrapper to have the code executed in the correct environment. We will focus now on just that code, which is what does the work. This one line is not actually correct, however, because while it did put a button at the top, it removed the button from the bottom and really is the equivalent of this line, i.e. with the remove function: $('div.event_description-section').prepend($('div.register_link-section').remove()); What we want is to clone the button so that it's at both locations. Luckily jQuery has precisely such a function and so with this: $('div.event_description-section').prepend($('div.register_link-section').clone()); We now have exactly what we want—two Register Now buttons, one at the top and one at the bottom of the page. For a second, more complicated, examle, let's imagine an NPO which works with addiction therapy. We have a custom tab for Contacts with a checkboxes called “Alcohol" and “Crack" etc. which are meant to indicate whether or not the Contact has these addictions. After each is a date field to store when he began that addiction. With no customization, here is how the fields look: If this were a real site, however, the list of substances would be much longer. It would be easier to read and use if the date fields could be to the right of the checkboxes. It would be even clearer if we could hide those date fields which are not relevant, i.e. where the checkbox is not checked. This type of customization is well-suited for jQuery. The first step here is to find how to target the DOM elements and then move them. Using Firebug, we can find that the classes assigned to the “Crack" row are “custom_field-row custom_256_155-row" It is tempting to therefore use the class “custom_256_155-row" as our selector. If we load up a few other contacts, however, we will eventually find that whereas the 256 number remains the same (as that is the ID of the custom field) the 155 number changes with each contact. So our actual selector is “custom_256_*" where the * is a wildcard. The way to target a class with a wildcard such as that is with this: $("tr[class^=custom_field-row custom_256_]") which means any TR with a full class definition starting with “custom_field-row custom_256_". Now that we have targeted our first row, we can add a TABLE tag onto the second TD tag, which sets up a container to use to relocate our date row. Now we remove our date row, which is id 257 in this case, from the main table and append it to our new table. This is half the job (for this one substance anyhow) and is the first two lines in the code below. The second half of the job is to add an onclick handler to hide or show the date field depending on the state of the addiction checkbox. To do this, we use the same row target method to target our input and then add a fairly simple click handler. Here is the full code: $("tr[class^=custom_field-row custom_256] .html-adjust").append('
'); $("tr[class^=custom_field-row custom_256_] table").append($('tr[class^=custom_field-row custom_257_]').remove()); $("tr[class^=custom_field-row custom_256_] input").click(function(){ if ($(this).attr('checked')) { $(this).parent().parent().find('table').show(); } else { $(this).parent().parent().find('table').hide(); } })
Here is how the fields look afterward, with the Crack date hidden after we clicked it to be not checked: The truth is that this is not a complete solution, because when the page loads, the code needs to check the status of the checkbox and hide the date field if necessary. Furthermore, this solution is specific to only one substance and needs to be generalized for a list of them. One could also use the toggle function instead of hide and show. The basic ideas required, however, are above and the savvy developer can expound upon them for a full solution. :)

Comments

Hi,

Like your regex trick: tr[class^=custom_field-row custom_256]. Was doing a loop on the items to find the one I wanted, what you are suggesting is nicer.

 

For everyone, I'd suggest learning firebug to help you find the class/id of the items you want to manipulate if you aren't using it already.

 

In the templates, we have tried to give unique names or classes to most of the elements so it's easier to target the right elements. Some are still missing probably, but if you happen to need one id or class missing, you can fill an issue (with a patch will be faster) and we'll fix it.

 

Hertzel, for the events you are organising, is this necessary to use both crack and alcohol or one is enough to be invited to the party :) ?

 

X+

Hi, I struggle with JQuery / js so this is great. When I did need to do something by js I followed a recommendation by Denver Dave to use   drupal_add_js

 

I'm interested in your opinion as to where this 4th option sits compared to #3

 

My function - just populates a custom field calculated off amount & another custom field

 

function mymodule_civicrm_buildForm( $formname, $form ){

if ($formname == 'CRM_Pledge_Form_Pledge'){
   if($_GET['id']){
     $instance = $_GET['id'];
   }else{
     $instance ='-1';
   }
   $jqueryinsert = "
      cj(document).ready( function() {
      cj('#amount').blur(function(){
        if(cj('#amount').attr('value') > 0){
        var newValue = cj('#custom_57_$instance').attr('value') *.05 / cj('#amount').attr('value') * 100;
         cj('#custom_207_$instance').val(newValue);
         }
        
      });
});
";
   drupal_add_js( $jqueryinsert , 'inline');

}

 }

I would appear to me that this is simply a different way to get the jQuery code onto the page, and thus falls into my #3 actually. :)

It would further appear that if your jQuery used the wildcard method (instead of $instance), this code could be in a file, not inline, and thus not require backend processing, and could also be cached along with other JS code.

Cool. Thanks for that feedback

The disadvantage is that upgrades may significantly change the core templates and thus require one to redo the customizations “from scratch."

To ease this headache I always put notes at the top of each custom template about what was changed.

$("tr[class^=custom_field-row custom_256_]")...

This is an incredibly inefficient line of jQuery. Run this on IE6 and it might take seconds to execute. It would be better to use a containing ID first:

$("#some-id").find("tr[class^=custom_field-row custom_256_]").each(function(){ 

  $(this).find("table")... 

});
 

Better yet to try and split out the .custom_field-row before you do the regex for greater resistance to DOM changes.

 

 

Is more a positive point than a negative one, isn't it ? For those 1% that haven't upgraded, let's push them to do it (or better yet to switch to an open source browser). In this day and age, when even microsoft has stopped supporting ie6 for like 2 years, we really shouldn't take care about it anymore.

 

But you are right, setting a parent will speed up things a bit

instead of

$("#some-id").find("tr[class^=custom_field-row custom_256_]").each(function(){ 

$("#some-id tr[class^=custom_field-row custom_256_]").each(function(){
is more readable

Unfortunately in this case, don't think we have an id set by civi above, so we depend on the theme.

$("form tr[class^=custom_field-row custom_256_]").each(function(){

Is probably going to work and be as fast as a #some-id

Anyway, on any sane browser, that's probably going to save a few ms when you load the registration form.

X+

 

Sadly IE6 still has around 4% share and even more sadly, it's widely used in the public sector in the UK, including the National Health Service - Europe's largest employer - and the Ministry of Defence. We have health sector clients so we have to support it. But its days in the NHS may be numbered.

 

Hi,

 

Thanks for the blog.  I've asked the question below in the forum, but it was relevant to this discussion so I hope you don't mind me asking it here.

 

How does this work when you want to modify something that seems to have other jQuery affecting it?  I'm trying to get an autocomplete (assigned to on activity) pre populated with the logged in user, but I can't seem to do anything on it as it starts life as an input that is changed to a ul.  I think I need to get my javascript to load later than the autocomplete java?  Have you seen this sort of requirement before?

 

Thanks,

Malks.

Hi. I found your forum post but beyond the ideas presented there, I don't know how possible this is. However you could certainly (I think) adjust the autocomplete code in the CiviCRM templates to call a function after converting the input.

 

That would be method #1 described above. It also has a place. :)

Thanks for this useful post. Is there a reason to use jQuery from Drupal/Joomla rather than Civi's jQuery, cj() ?

Hi Hershel,
Thank you for the great writeup on implementing jQuery within Civi. I am trying to use jQuery loaded via a module .info file, to set some default values on a form. However, even though the jQuery is loaded, and no errors are triggered nothing happens. To begin with, to verify jQuery is working properly I am trying to simply remove a class from the New Case screen.

if (Drupal.jsEnabled) {
$(document).ready(function () {
  $("#profiles_1").removeClass("form-select");
});
}

The above snippet does not remove the class as I would expect it too. Does Civi implement only a subset of the core jQuery functionality? Thanks again for the great introductory post.

This jQuery does 3 things: Hides the radio button of a contribution amount, hides the label, and hides the BR tag immediately after the label.

 
$("#YOUR_CIVICRM_QFID_999_9").hide();
$("label[for='YOUR_CIVICRM_QFID_999_9']").next('br').remove();
$("label[for='YOUR_CIVICRM_QFID_999_9']").hide();

Put in on your page using one of the methods Hershel lists above.

For an event and / or profile registration page, exactly where would the jQuery code to customize the form be placed? I'm very new to CiviCRM and I haven't quite figured out the flow of page, and what goes where. Thanks...fred

I wonder, in pages like the Configure Event -> Fee tab, where it loads the contents of the tabbed page area after the page itself, and therefore after any javascript added by drupal_add_js, how do I change things loaded in the tabbed area? It seems that I'd need to load a copy of livequery, which I've done using drupal_add_js, but then I don't know where to go from there, because neither accessing civicrm's copy of jquery like cj('#selector').livequery(function() {....}); nor accessing Drupal's copy of jquery like $('#selector').livequery(function() {....}); has any effect. Anyone know where to go from here?

Here's a solution from the forum where I asked the same question (http://forum.civicrm.org/index.php/topic,24367.msg102482.html#msg102482) :

"You might want to delay where the init code is loaded (eg. on the load instead of the ready event) or wait on an event that is linked to the content you want to alter (with the .on method, you can watch more or less everything, including inserting elements in the dom)"

Hello,

Unfortunatley, I'm not an experienced jquery coder...I'm trying to figure out how I can force a checkbox to be checked on a this specific checkbox "Show my support in the public honor roll"

I'm using CiviCRM in Joomla 2.5.8...I'm trying to do this without hacking the core. I'm using CiviEvent with Personal Campaign Pages and when people register or support a PCP they often miss that checkbox and then call wondering why their name is not showing in the Honor Roll. I'm hoping by making the checkbox checked by default I will recieve less complaints.

I posted in the forum, but they redirected me here. Any help would be greatly appreciated.

Would something like this work?

$("input[for='pcp_display_in_roll']").prop('checked', true);

If so, where would I put it?