Added by Michelle D'Souza, last edited by Laurel Williams on Oct 01, 2009  (view change)

Labels:

Enter labels to add to this page:
Wait Image 
Looking for a label? Just start typing.

This tutorial will walk you through the creation of a Fluid Component called Flutter, a simple Twitter client. This tutorial is not intended to provide in depth information into what Flutter is or how it works, but to give an understanding of the process of creating a component in general. In order to complete this tutorial, it is assumed that you know and understand HTML, CSS, and JS.

These are the basic tutorial steps:

The rest of this tutorial will explain each of these steps in detail.


Set Up Your Environment

Create a directory structure

For this tutorial, create the following directory layout in your favourite development environment. We use Aptana.

  • A top level directory called webapp
  • Two sub-directories of webapp called flutter-infused and shared
  • A sub-directory of flutter-infused called sample-data
  • Two sub-directories of shared called css and js
  • A sub-directory of css called fss
  • A sub-directory of js called infusion
Click below to enlarge

Add Fluid Infusion dependencies

  • Download and unzip a custom built copy of Fluid Infusion. This was created using the Custom Infusion Builds With Ant process with FSS, Framework and jQuery UI as build parameters.
  • From the framework directory of your Infusion package, copy the contents of the fss directory into your fss directory.
  • Copy the MyInfusion.js file into your infusion directory
  • The result should look something like the image on the right
Click below to enlarge

Add Flutter dependencies

Copy the files below into the appropriate directories.

File Directory
Flutter.css webapp/shared/css
SettingsDialog.js webapp/shared/js
Twitter.js webapp/shared/js
FriendsView.js webapp/flutter-infused
FlutterInfused.html webapp/flutter-infused
Friends webapp/flutter-infused/sample-data
user_timeline_12368532.json webapp/flutter-infused/sample-data
user_timeline_14538601.json webapp/flutter-infused/sample-data
user_timeline_14538636.json webapp/flutter-infused/sample-data
user_timeline_14951188.json webapp/flutter-infused/sample-data
user_timeline_17868497.json webapp/flutter-infused/sample-data
user_timeline_19539154.json webapp/flutter-infused/sample-data
user_timeline_5915782.json webapp/flutter-infused/sample-data
user_timeline_752673.json webapp/flutter-infused/sample-data
The result should look something like the image below (click to enlarge).
Still need help?

Join the infusion-users mailing list and ask your questions there.


Parts of a Fluid Component

A Fluid Component is made up of three main parts: a creator function, fluid.defaults, and helper functions.

The creator function is used to create a component. It is not a constructor, and does not use the "new" keyword. Public functions can be included inside the creator function.

fluid.defaults is where all of the default options for a component are specified. The Fluid Framework manages the component's options. It makes it easier to find selectors in the DOM, handle programmatically changed styles, and reference all of the options.

The helper functions are the private functions that a component uses, and does not expose. These are optional and will not be discussed in this tutorial.


The Creator Function

The creator function is not a constructor and does not make use of the "new" keyword. Rather, it replaces the need for a constructor and allows the ability to separate public and private methods.

The code for the creator function is marked in the code below.


(function (nameSpace) {

    var privateFunc = function () { some private function };

    //start of creator function
    nameSpace.componentName = function (container, options) {
        var that = fluid.initView("nameSpace.componentName ", container, options);

        that.refreshView() {
            some refresh function
        }

        return that; 
    };
    //end of creator function

})(nameSpace);




Let's look at this in more detail.

Anonymous functions

By wrapping all of the javascript for the component inside an anonymous function, we can separate private and public methods. The anonymous function is accomplished with the code below.

(function (nameSpace) {

// place javascript here

})(nameSpace)

The creator function signature

The next step is to prepare the creator function for the component. It's signature is defined by giving the function a name and assigning it to a function with the parameters (container, options).

  • componentName is the name of the component you are creating.
  • container is a "jQueriable" element (a string representing a selector, a DOM element, or a jQuery) which will contain the component.
  • options is a JavaScript object specifying the configuration details of the component

The result is shown below.

(function (nameSpace) {

    nameSpace.componentName = function (container, options) {};

})(nameSpace)

Read more about the creator function:

initView

To make componentName an actual component, the first task within the creator is to create a new object called that, which is assigned the value of fluid.initView(componentName, container, options).

(function (nameSpace) {

    nameSpace.componentName = function (container, options) {
        var that = fluid.initView("nameSpace.componentName", container, options);

        return that; //should always be the last line of the creator function
    };

})(nameSpace)

Read more about the creator function and initView:

More detailed information about the initView function can be found here.

Public functions

To add public functions to the component, you can define a function within the scope of the creator function and add it to the that object. For example if we wanted to add a refreshView function to the component, the code would look like...

(function (nameSpace) {

    nameSpace.componentName = function (container, options) {
        var that = fluid.initView("nameSpace.componentName", container, options);

        that.refreshView() = function { some refresh function };

        return that;
    };

})(nameSpace)

To call this function you use:

nameSpace.componentName.refreshView();

Private functions

Functions defined within the anonymous function but outside the creator function and without having the nameSpace prefix, will be private and inaccessible to the global namespace. Thus, a private function can be defined as shown below.

(function (nameSpace) {

    var privateFunc = function () { some private function };

    nameSpace.componentName = function (container, options) {
        var that = fluid.initView("nameSpace.componentName", container, options);

        that.refreshView() = function { some refresh function };

        return that;

    };

})(nameSpace)

Read more about private functions:

Writing a creator function for Flutter

Try writing a creator function for Flutter's status view. Download the Sample Code. Rename the file to StatusView.js and place into the "flutter-infused" directory. The following tutorial code for the creator function should be added starting at line 41 (see comments within the code).

Flutter's views take two extra arguments which are not usually required for Fluid components, however Fluid components are flexible and can accommodate additional arguments. These two arguments, "twitter" and "events", are added to the statusView creator function as shown below.

    /**
     * A View representing the Flutter status panel.
     * 
     * @param {Object} container
     * @param {Object} options
     */
    fluid.flutter.statusView = function (container, twitter, events, options) {
        
    };

Next call initView

    /**
     * A View representing the Flutter status panel.
     * 
     * @param {Object} container
     * @param {Object} options
     */
    fluid.flutter.statusView = function (container, twitter, events, options) {
        var that = fluid.initView("fluid.flutter.statusView", container, options);
        that.twitter = twitter;
        that.events = events; 
    };

Now add the public methods and any other function that the creator function needs to call.

    /**
     * A View representing the Flutter status panel.
     * 
     * @param {Object} container
     * @param {Object} options
     */
    fluid.flutter.statusView = function (container, twitter, events, options) {
        var that = fluid.initView("fluid.flutter.statusView", container, options);
        that.twitter = twitter;
        that.events = events;
                
        /**
         * Updates the user's status on Twitter.
         * 
         * @param {String} statusMessage the status message to send to Twitter
         */
        that.updateStatus = function (statusMessage) {
            // Post the status to the server
            that.twitter.postStatus(statusMessage,
                                    that.events.onStatusSaveSuccess.fire,
                                    that.events.onStatusSaveError.fire);
        };
        
        /**
         * Shows a message to the user when their status has been successfully updated on Twitter. 
         */
        that.showStatusUpdateSuccess = function () {
            showStatusUpdateMessage(that, "Your status was successfully updated!");
            
            // Clear the edit field so the user has another affordance showing that their status update was a success.
            that.locate("statusEdit").val("");
        };
        
        /**
         * Displays a polite error message to the user if there was a problem updating their status on Twitter.
         */
        that.showStatusUpdateError = function () {
            showStatusUpdateMessage(that, "There was a problem updating your status on Twitter.");
        };
        
        setupStatusView(that);
        return that;    
    };

When you are done, the code should look like this - Complete Code


The fluid.defaults function

The fluid.defaults function allows you specify options for a component and set their defaults. The Fluid Framework manages the options set in the fluid.defaults function, including merging them when any of the default options are overridden.

The fluid.defaults function signature

fluid.defaults is a function that takes two arguments, the component and an object of key value pairs representing the component options. Typical options include "selectors", "styles", "events", and "strings", however, you are free to create your own options. Options are specified with a meaningful name for the key, and their default value.

fluid.defaults("component.name", {
    selectors: {key: value},
    styles: {key: value, ...},
    strings: {key: value, ...},
    events: {key: value, ...},
    optionName: {key: value, ...},
    ...
}

Default selectors

The selectors option is used to specify the default selectors used to find elements in the DOM. The default selectors can be any "jQueriable" element (a string representing a selector, a DOM element, or a jQuery).

Default selectors for Flutter

Try writing an example using Flutter's Tweets view. Download the Sample Code. Rename the file to TweetsView.js and place into the "flutter-infused" directory. Start writing the fluid.defaults function on line 115.

fluid.defaults("fluid.flutter.tweetsView", {

//options go here

});

Now add the selectors options, specifying default values for tweets and tweetTemplate. Notice that tweets uses a DOM element and tweetTemplate uses a class as its selector.

fluid.defaults("fluid.flutter.tweetsView", {
    selectors: {
        tweets: "li",
        tweetTemplate: ".flutter-tweet-template"
    }
});

Now that the selectors are defined they can be used in the code. On line 21 the selectableSelector key can be assigned the default value specified for tweets.

selectableSelector: that.options.selectors.tweets

Often you'll want to use the default selectors for finding elements in the DOM. The Fluid Framework simplifies this task for you, by automatically searching within the scope of the container specified in the creator function. All you have to do is pass the name of the selector to that.locate and the Fluid Framework will take care of finding it for you. For example, add the following code on line 32 to remove an element.

that.tweetTemplate = that.locate("tweetTemplate").remove();

When you are done, the finished code will look like this

Default styles

The styles option is used to specify default CSS classes. It can be used to store all CSS classes that a component uses, but is most useful at storing those classes which are added/removed programmatically. The default values for styles take the name of a CSS class. The specific styling applicable to that CSS class should be defined in a separate CSS style sheet that is linked to by the HTML document.

Default styles for Flutter

Try writing an example using Flutter's Settings view. Download the Sample Code. Rename the file to SettingsView.js and place into the "flutter-infused" directory. Start writing the fluid.defaults function on line 70. As before, pass in the name of the component, but start with an options object that only has the selectors specified (as shown below).

fluid.defaults("fluid.flutter.settingsView", {
    selectors: {
        usernameField: ".flutter-username-edit",
        passwordField: ".flutter-password-edit"
    }
});

Now add the styles options and specify the the defaults for hidden.

fluid.defaults("fluid.flutter.settingsView", {
    selectors: {
        usernameField: ".flutter-username-edit",
        passwordField: ".flutter-password-edit"
    },

    styles: {
        hidden: "flutter-hidden"
    }
});

Put the styles option to use in the code at line 55. This function checks if the hidden class exists in the container and returns the result.

that.isVisible = function () {
    return !that.container.hasClass(that.options.styles.hidden);
};

When you are done, the finished code will look like this

Default strings

The strings option is used to specify the defaults for messages, warnings, and other text that is programmatically set by the component. It is useful for localization, by allowing users to replace the default text with that of another language. Adding default strings to Flutter is similar to adding other defaults.

Default events

The events object is used to specify the events used by the component. (Please see below for how to add listeners via a listeners option)

Defining events is slightly different than other defaults. Most importantly, the events object is included in the defaults for the declaration of events, not as an option. This means that they are not intended to be overriden by a value passed to the creator function, as this would could change behaviour.

The structure of events is different, the key is the name of the event, the value can be one of: null, "unicast", or "preventable". Note that null is not a string value, but the actual value null. null allows the fired event to be heard by all listeners that have registered for it. "preventable" is similar to null, but its propagation can be stopped by a registered listener. "unicast" can be used where one and only one listener should be registered for an event. For more information about what these different values represent, see the Infusion Event System.

Default events for Flutter

Try writing an example for Flutter. Download the Sample Code. Rename the file to FlutterInfused.js and place into the "flutter-infused" directory. Start adding the events options to the fluid.defaults function on line 162. The initial structure of the fluid.defaults is shown below. Don't worry about the view options here, they will be explained in the subcomponents section below.

fluid.defaults("fluid.flutter", {
        friendsView: {
            type: "fluid.flutter.friendsView"
        },

        tweetsView: {
            type: "fluid.flutter.tweetsView"
        },

        settingsView: {
            type: "fluid.flutter.settingsView"
        },

        statusView: {
            type: "fluid.flutter.statusView"
        },

        selectors: {
            mainPanel: ".flutter-main-panel",
            settingsPanel: ".flutter-settings-panel",

            statusPanel: ".flutter-status-panel",
            friendsList: ".flutter-friends",
            tweetsList: ".flutter-tweets",

            allTabs: ".flutter-panel-tabs li",
            friendsTab: ".flutter-friends-tab",
            settingsTab: ".flutter-settings-tab",

            errorDialog: ".flutter-error-dialog"
        },

        styles: {
            hidden: "flutter-hidden",
            activeTab: "fl-activeTab"
        }
    });

Now add the events options.

fluid.defaults("fluid.flutter", {
        friendsView: {
            type: "fluid.flutter.friendsView"
        },

        tweetsView: {
            type: "fluid.flutter.tweetsView"
        },

        settingsView: {
            type: "fluid.flutter.settingsView"
        },

        statusView: {
            type: "fluid.flutter.statusView"
        },

        selectors: {
            mainPanel: ".flutter-main-panel",
            settingsPanel: ".flutter-settings-panel",

            statusPanel: ".flutter-status-panel",
            friendsList: ".flutter-friends",
            tweetsList: ".flutter-tweets",

            allTabs: ".flutter-panel-tabs li",
            friendsTab: ".flutter-friends-tab",
            settingsTab: ".flutter-settings-tab",

            errorDialog: ".flutter-error-dialog"
        },

        styles: {
            hidden: "flutter-hidden",
            activeTab: "fl-activeTab"
        },

        events: {
            // View-related events.
            afterFriendSelected: null,
            onSaveSettings: null,

            // Model change events.
            onFriendsFetchSuccess: null,
            onFriendsFetchError: null,
            onTweetsFetchSuccess: null,
            onTweetsFetchError: null,
            onStatusSaveSuccess: null,
            onStatusSaveError: null
        }
    });

Now that the events have been specified we can fire them or listen to them.

Try firing some events. On line 104, as part of the function to fetch friends, add two event firing functions. The first will fire on success, and the other will fire on error.

that.twitter.fetchFriends(that.events.onFriendsFetchSuccess.fire, that.events.onFriendsFetchError.fire);

Notice that both of these events have a null value, therefore all registered listeners will be notified when these events are fired.

Now listen to some events. On line 26 add a couple of event listeners as part of the bindEventHandlers function.

that.events.onSaveSettings.addListener(that.showMainPanel);
that.events.onFriendsFetchError.addListener(that.friendsErrorDialog.open);

The function which is passed in (i.e. that.showMainPanel) is called when the event is fired and detected by the listener.


Alternatively listeners can be specified inside a listeners options object in defaults. In this case, the key is the name of the event to which the listener is bound, and the value is the function to call after the listener detects the event.

Adding a listener to fluid.defaults would look something like this:

fluid.defaults("fluid.flutter", {

    listeners: {
        eventName: function
    }

});

When you are done, the finished code will look like this


Subcomponents

Depending on how you've planned and or refactored your code, you may find yourself needing to break off chunks of code into their own components. We refer to these chunks as subcompenents (although in reality they are just components used by another component). In Flutter, the individual views are components. Flutter, also a component, makes use of the individual views as subcomponents.

In fluid.defaults, you specify a subcomponent using a name and an object containing the key-value pair of type: "component name" as shown below. In this case the subcomponent's name is "subComponent".

fluid.defaults("fluid.flutter", {
    selectors: {
        mainPanel: ".flutter-main-panel",
        settingsPanel: ".flutter-settings-panel"
    },
    styles: {
        hidden: "flutter-hidden"
    },

    subComponent: {
        type: "nameSpace.componentName"
    }
}};

Initialization is done using fluid.initSubcomponent. This function takes three arguments:

  1. the component you are attaching it too, which is represented by "that"
  2. the name of the subcomponent as specified in fluid.defaults
  3. an array containing the arguments for the subcomponent
that.subComponent = fluid.initSubcomponent(that, "subComponent", [arg1, arg2, ...]);

Subcomponents for Flutter

Try adding a subcomponent to Flutter. Download the Sample Code. Rename the file to FlutterInfused.js and place into the "flutter-infused" directory. To start we'll add the subcomponent options to the fluid.defaults function, on line 109. The initial structure of the fluid.defaults is shown below.

fluid.defaults("fluid.flutter", {

        selectors: {
            mainPanel: ".flutter-main-panel",
            settingsPanel: ".flutter-settings-panel",

            statusPanel: ".flutter-status-panel",
            friendsList: ".flutter-friends",
            tweetsList: ".flutter-tweets",

            allTabs: ".flutter-panel-tabs li",
            friendsTab: ".flutter-friends-tab",
            settingsTab: ".flutter-settings-tab",

            errorDialog: ".flutter-error-dialog"
        },

        styles: {
            hidden: "flutter-hidden",
            activeTab: "fl-activeTab"
        },

        events: {
            // View-related events.
            afterFriendSelected: null,
            onSaveSettings: null,

            // Model change events.
            onFriendsFetchSuccess: null,
            onFriendsFetchError: null,
            onTweetsFetchSuccess: null,
            onTweetsFetchError: null,
            onStatusSaveSuccess: null,
            onStatusSaveError: null
        }
    });

Now add the subcomponents friendsView, tweetsView, settingsVeiw and statusView.

fluid.defaults("fluid.flutter", {
        //subcomponent friendsView
        friendsView: {
            type: "fluid.flutter.friendsView"
        },

        //subcomponent tweetsView
        tweetsView: {
            type: "fluid.flutter.tweetsView"
        },

        //subcomponent settingsView
        settingsView: {
            type: "fluid.flutter.settingsView"
        },

        statusView: {
            type: "fluid.flutter.statusView"
        },

        selectors: {
            mainPanel: ".flutter-main-panel",
            settingsPanel: ".flutter-settings-panel",

            statusPanel: ".flutter-status-panel",
            friendsList: ".flutter-friends",
            tweetsList: ".flutter-tweets",

            allTabs: ".flutter-panel-tabs li",
            friendsTab: ".flutter-friends-tab",
            settingsTab: ".flutter-settings-tab",

            errorDialog: ".flutter-error-dialog"
        },

        styles: {
            hidden: "flutter-hidden",
            activeTab: "fl-activeTab"
        },

        events: {
            // View-related events.
            afterFriendSelected: null,
            onSaveSettings: null,

            // Model change events.
            onFriendsFetchSuccess: null,
            onFriendsFetchError: null,
            onTweetsFetchSuccess: null,
            onTweetsFetchError: null,
            onStatusSaveSuccess: null,
            onStatusSaveError: null
        }
    });

All of the subcomponents have been added to fluid.defaults and are ready to be initialized. On line 33 we'll add in the initialization code.

// Create the Friends View, responsible for showing and selecting the list of  friends.
that.friendsView = fluid.initSubComponent(that, "friendsView",
    [that.locate("friendsList"), that.twitter, that.events, fluid.COMPONENT_OPTIONS]);

// Instantiate the Tweets View, which displays the list of tweets
that.tweetsView = fluid.initSubComponent(that, "tweetsView",
    [that.locate("tweetsList"), that.twitter, that.events, fluid.COMPONENT_OPTIONS]);

// The Settings View controls the panel allowing users to edit their username and password for Twitter.
that.settingsView = fluid.initSubComponent(that, "settingsView",
    [that.locate("settingsPanel"), that.twitter, that.events, fluid.COMPONENT_OPTIONS]);

// The Status View shows the user's icon, name, and allows them to update their status.
that.statusView = fluid.initSubComponent(that, "statusView",
    [that.locate("statusPanel"), that.twitter, that.events, fluid.COMPONENT_OPTIONS]);

Their are two things to note in the initialization code above:

  1. the use of that.locate. Recall from the selectors section above that that.locate is used to find DOM elements.
  2. fluid.Components
    • This tells fluid.initSubComponent to check if any options were specified for the subcomponent when the parent component was initialized. In this case the Fluid Framework will merge in those options. For example:
      fluid.flutter(container, {friendsView: { selectors: { friends: ".liClass"}}});
      

When you are done, the finished code will look like this

Custom Options

There may be times when there are options specific to a component you are writing which don't fit into any of the options already mentioned. fluid.defaults allows you to add custom options.

Here is an example of how you would specify custom options to a component called nameSpace.custom. This declaration follows the same pattern that the other options.

fluid.defaults("nameSpace.custom", {

    customOptions: {
        meaningfulName: value
    }

});

To access this option use:

var getOptionValue = that.options.customOptions.meaningfulName;

To summarize, the following code is a basic template for a fluid component.

/*
 Insert license here
*/

/*global jQuery*/
/*global fluid_1_1*/

fluid_1_1 = fluid_1_1 || {}; //use current released version number

(function($, fluid) {

    var privateFunc = function() {
		
        // some private function 

    };
    
    //see http://wiki.fluidproject.org/display/fluid/The+creator+function
    //start of creator function

    nameSpace.componentName = function(container, options) {
        var that = fluid.initView("nameSpace.componentName ", container, options);
        
        that.publicFunc = function() {

            // some public function

        };
        
        return that;
    };

    //end of creator function
    
    //start of defaults

    fluid.defaults("componentName", {

	//default options can be selectors, styles, strings, or events 
	//also can create options based on subcomponents using subComponentName as the optionName
	//finally, custom options are also possible
	//see http://wiki.fluidproject.org/display/fluid/fluid.defaults 

        option1Name: {
            key: value
        },
        option2Name: {
            key1: value1,
            key2: value2
        }
    
    });

    // end of defaults

})(jQuery, fluid_1_1);


The World Without the Fluid Framework

Overview

The purpose of this section is to provide an example of how Flutter would be built, without the use of the Fluid Framework.

Setup

Working from the directory structure created above, we will add a couple more directories and files.

1) In the "webapp" directory, add a new sub-directory called "flutter-plain-jquery"

2) Inside the "flutter-plain-jquery" directory we'll add another sub-directory called "sample-data"

3) In the "shared/js" directory, add a new sub-directory called "jquery"

Your Directory should now look something like this...

4) Next we'll add the files necessary to make Flutter work. Please copy the files below into the appropriate directories

File Directory
jquery.keyboard.a11y.js webapp/shared/css
jquery webapp/shared/js/jquery
ui.core.js webapp/shared/js/jquery
ui.dialog.js webapp/shared/js/jquery
ui.draggable.js webapp/shared/js/jquery
Flutter.html webapp/flutter-plain-jquery
Flutter.js webapp/flutter-plain-jquery
Friends webapp/flutter-infused/sample-data
user_timeline_12368532.json webapp/flutter-plain-jquery/sample-data
user_timeline_14538601.json webapp/flutter-plain-jquery/sample-data
user_timeline_14538636.json webapp/flutter-plain-jquery/sample-data
user_timeline_14951188.json webapp/flutter-plain-jquery/sample-data
user_timeline_17868497.json webapp/flutter-plain-jquery/sample-data
user_timeline_19539154.json webapp/flutter-plain-jquery/sample-data
user_timeline_5915782.json webapp/flutter-plain-jquery/sample-data
user_timeline_752673.json webapp/flutter-plain-jquery/sample-data
Your Directory should now look something like this...

Comparison

Flutter-Plain-jQuery Flutter-Infused
Relies on hard coded selectors to find elements in the DOM The Fluid Framework allows selectors to be specified by the integrator. The component does come with defaults for the selectors, which the integrator is free to use or override.
Flutter.js is a set of behaviour governing the various aspects of Flutter-Plain-jQuery The Fluid Framework's event system allows the Flutter-Infused to be partitioned into components/subcomponents, by providing a means of communication between them. The event system is constructed in such a way that individual components/subcomponents do not need to be aware of the existence of one another, but can just listen for events they are interested in. These events are also available to the integrator to listen to, thereby allowing Flutter to be extended
There aren't really options. You are able to pass in information about URL's The options merging from the Fluid Framework provides a simple way to make your component customizable. Changing default options are as simple as specifying which options and values to override in an options object, passed to the Creator function