An Introduction to Using Entwine in SilverStripe

Entwine is a jQuery library which allows you to add functions to groups of DOM elements, instead of passing elements as function arguments or storing them as global variables. Entwine powers the entire back-end of the SilverStripe framework and CMS, and is very useful for writing custom functionality in the CMS, so that’s what this article will focus on. You can download the completed example code here.

Note: In SilverStripe 3.2, the format of form field IDs changed. The ideas in this article still apply, but the exact implementation has changed. I’ve updated the completed example code with a 3.2-compatible version which is working, but isn’t as well tested as the original version.

Scenario

Our scenario is that we need a DataObject to be able to link to either an “internal” URL, for this example a page in the CMS, or an “external” URL.

Setup

Firstly, let’s create a simple DataObject that we want to add our link to and a ModelAdmin interface so we can edit it. We’ll call our model “Feature”:

<?php
// Feature.php
class Feature extends DataObject 
{
    private static $db = array(
        "Title" => "Varchar(255)",
        "ExternalURL" => "Text"
    );

    private static $has_one = array(
        "InternalURL" => "SiteTree"
    );

}

Note that we use a $has_one to store our internal URL as we’re linking to a page in the SiteTree.

<?php
// FeatureAdmin.php
class FeatureAdmin extends ModelAdmin 
{

    private static $managed_models = array(
        "Feature"
    );

    private static $menu_title = "Features";

    private static $url_segment = "features";

}

Now, after we visit http://mysite.com/dev/build?flush and refresh the CMS, we should see a new tab on the left-hand side called “Features”. Click this new tab, and click to add a new feature.

screenshot 1

Clearly, the first thing we need to do is replace the two ‘link’ fields with more useful alternatives: TextField for external links and TreeDropdownField for internal links. We can do this by adding a getCMSFields() method to our Feature class:

/**
 * @return FieldList
 */
public function getCMSFields() 
{
    $fields = parent::getCMSFields();

    // Firstly. remove the old fields
    $fields->removeByName("ExternalURL");
    $fields->removeByName("InternalURLID"); // Note 'ID' on the end of the field name as this is a has_one

    $externalURLField = TextField::create("ExternalURL", "Link to external page");
    $internalURLField = TreeDropdownField::create("InternalURLID", "Choose a page to link to", "SiteTree");

    $fields->addFieldsToTab('Root.Main', array($externalURLField, $internalURLField));

    return $fields;
}

Refresh the CMS and our new fields will appear, looking much better already:

screenshot 2

Next we need to add radio buttons to the CMS that we will use to decide which of our two link fields to show, so let’s amend the getCMSFields() method to the following:

/**
 * @return FieldList
 */
public function getCMSFields() 
{
    $fields = parent::getCMSFields();

    // Firstly. remove the old fields
    $fields->removeByName("ExternalURL");
    $fields->removeByName("InternalURLID"); // Note 'ID' on the end of the field name as this is a has_one

    // The two options for which type of link to add
    $linkOptions = array("ExternalURL" => "Link to an external page", "InternalURLID" => "Link to an internal page");

    // If we've set an internal link already, then that option should be pre-selected
    $selectedOption = ($this->InternalURLID) ? "InternalURLID" : "ExternalURL";
    $linkTypeField = OptionsetField::create("LinkType", "", $linkOptions, $selectedOption);

    $externalURLField = TextField::create("ExternalURL", "Link to external page");
    $internalURLField = TreeDropdownField::create("InternalURLID", "Choose a page to link to", "SiteTree");

    $fields->addFieldsToTab('Root.Main', array($linkTypeField, $externalURLField, $internalURLField));

    return $fields;
}

You can see in the above snippet that we’ve added an OptionsetField to our list of fields, with two options (one for each type of link) and then pre-selected an option based on whether a link already exists in the database. Refresh the CMS and your new field should appear (make sure you’ve included it in the $fields->addFieldsToTab() line!):

screenshot 3

Javascript

Now we just need to add the JavaScript we need to show and hide the correct field based on the user’s selection. Let’s add an extra class to each of our link fields and include an external JavaScript file:

<?php
/**
 * @return FieldList
 */
public function getCMSFields() 
{
    $fields = parent::getCMSFields();

    // Include link switcher JavaScript
    Requirements::javascript('mysite/javascript/LinkSwitcher.js');

    // Firstly. remove the old fields
    $fields->removeByName("ExternalURL");
    $fields->removeByName("InternalURLID"); // Note 'ID' on the end of the field name as this is a has_one

    // The two options for which type of link to add
    $linkOptions = array("ExternalURL" => "Link to an external page", "InternalURLID" => "Link to an internal page");
    // If we've set an internal link already, then that option should be pre-selected
    $selectedOption = ($this->InternalURLID) ? "InternalURLID" : "ExternalURL";
    $linkTypeField = OptionsetField::create("LinkType", "", $linkOptions, $selectedOption);

    $externalURLField = TextField::create("ExternalURL", "Link to external page")
        ->addExtraClass('switchable');
    $internalURLField = TreeDropdownField::create("InternalURLID", "Choose a page to link to", "SiteTree")
        ->addExtraClass('switchable');

    $fields->addFieldsToTab('Root.Main', array($linkTypeField, $externalURLField, $internalURLField));

    return $fields;
}

We’ve added a class ‘switchable’ to each of our link fields and included a Requirements call to fetch an extra JavaScript file. Let’s create our JavaScript file (in mysite/javascript/LinkSwitcher.js) and make sure that it’s being loaded:

// LinkSwitcher.js
alert('testing!');

If you now refresh the CMS, you should get an alert dialog appear. If not, check that the path set when we call Requirements::javascript() is correct.

To recap, so far we’ve created a model which supports adding external or internal links, created appropriate CMS fields and included an extra JavaScript file. Now that the easy part’s out of the way, we can get stuck into Entwine!

Entwine

Open up your LinkSwitcher.js file and add the following JavaScript which will wrap all our logic to show/hide fields:

// LinkSwitcher.js
(function($) {
    $.entwine(function($) {
        // All our Entwine logic will go here
    });
})(jQuery);

You may recognise the first & last lines that wrap our entire script. This is a fairly common pattern when using jQuery.noConfict() (all CMS code uses this) and ensures that there are no conflicts with any other libraries which may use the $ symbol as a variable. The $.entwine(function($) {}); block is specific to Entwine - when this code is executed, Entwine will parse everything inside the function and begin attaching event handlers etc.

We’re now able to start adding functions to our DOM elements! Let’s start off simple and tell Entwine to say hello when it detects a field with the ‘switchable’ class we added earlier. Amend your LinkSwitcher.js file to match the following:

// LinkSwitcher.js
(function($) {
    $.entwine(function($) {
        /**
         * Class: .cms-edit-form .field.switchable
         *
         * Say hello
         */
        $('.cms-edit-form .field.switchable').entwine({
            onmatch: function() {
                alert('hello!');
            }
        });
    });
})(jQuery);

If you refresh the CMS, you should see two popups saying ‘hello’ - one popup for each field with the class ‘switchable’. Now that we’ve got Entwine detecting our fields, let’s start to build some of the logic for our link field switcher:

// LinkSwitcher.js
/**
 * Class: .cms-edit-form .field.switchable
 *
 * Hide each switchable field
 */
$('.cms-edit-form .field.switchable').entwine({
    onmatch: function() {
        var id = this.attr('id');
        this.hide();
        this._super();
    }
});

We’re now doing three things; we’re fetching the ID of the current switchable field, we’re hiding the field, and then we’re calling a function called _super().


The _super() function is important, it means that any other code that has added handlers for the current event (e.g. ‘onmatch’) to any of the DOM elements that match your selector will also run - it’s a vaguely similar pattern to parent::__construct() in PHP. A good general rule of thumb is: if you’re adding extra functionality call this._super() after your code, if you’re replacing functionality then don’t.


If you refresh the CMS now, both the link fields should be hidden. We now need to write some code to show the currently selected link type. Firstly, let’s modify our existing onmatch function to only hide fields if they’re not the currently selected field type:

/**
 * Class: .cms-edit-form .field.switchable
 *
 * Hide each switchable field except for the currently selected link type
 */
$('.cms-edit-form .field.switchable').entwine({
    onmatch: function() {
        var id = this.attr('id'),
            form = this.closest('form');

        if(form.find('input[name=LinkType]:checked').val() !== id) {
            this.hide();
        }

        this._super();
    }
});

Firstly, we find the closest form element on the page (i.e. the current form). Next, we check whether the currently selected link type matches this field and we hide it if it doesn’t. If you refresh the CMS, the external link box should be ticked and the external link field should be showing:

screenshot 5

We now just need to add the logic to handle when someone clicks between the two different field types. Add the following code just after the existing $('.cms-edit-form .field.switchable').entwine({}) block:

// LinkSwitcher.js
/**
 * Input: .cms-edit-form input[name=LinkType]
 *
 * On click of radio button, show selected field, hide all others
 */
$('.cms-edit-form input[name=LinkType]').entwine({
    onclick: function() {
        var id = this.val(),
            form = this.closest('form');

        form.find('.field.switchable').hide();
        form.find('#' + id).show();

        this._super();
    }
});

We’re handling a click event (similar to jQuery’s .on('click') / .click() event), hiding all the switchable fields and then showing the field which matches the currently selected link type. If you refresh the CMS, you should now be able to click between the different field types.

You can download the completed example code, including a handy $Link function to use in templates, here.

If you need more examples of how you can use Entwine, simply looking at the CMS and framework core JavaScript is a great learning resource!

Bonus round!

So far, we’ve only used built-in functions available to Entwine (onmatch and onclick - there are other default functions available). You can, however, add any function you like to each element. Let’s try this out by adding custom functions to show and hide our link fields.

As these functions will affect the link fields, we want to add them to our $('.cms-edit-form .field.switchable') collection, so amend that entwine block to the following:

// LinkSwitcher.js
/**
 * Class: .cms-edit-form .field.switchable
 *
 * Hide each switchable field except for the currently selected link type
 */
$('.cms-edit-form .field.switchable').entwine({
    onmatch: function() {
        var id = this.attr('id'),
            form = this.closest('form');

        if(form.find('input[name=LinkType]:checked').val() !== id) {
            this.hide();
        }

        this._super();
    },
    disappear: function() {
        this.slideUp(500);
    },
    reappear: function() {
        this.slideDown(500);
    }
});

Now we need to tell Entwine to use these functions when showing/hiding our fields, so modify the $('.cms-edit-form input[name=LinkType]') block to match the following:

/**
 * Input: .cms-edit-form input[name=LinkType]
 *
 * On click of radio button, show selected field, hide all others
 */
$('.cms-edit-form input[name=LinkType]').entwine({
    onclick: function() {
        var id = this.val(),
            form = this.closest('form');

        form.find('.field.switchable').disappear();
        form.find('#' + id).reappear();

        this._super();
    }
});

If you refresh the CMS, the fields should now slide up/down when switching between the different link types.

The modified example code with these extra functions can be downloaded here.

Published on

25th June 2014
by Loz Calver

Filed Under

SilverStripe
Front-end

How can we help?

Please send us some details of your requirements and we’ll be in touch.

Please note by submitting your details you are agreeing for Bigfork Ltd to store your data in order to process your enquiry and that you have read our Privacy Policy.