module YuiTree::ClassMethods

Public Instance Methods

uses_yui_tree( tree_options = {}, filter_options = {} ) click to toggle source

Define the “#uses_yui_tree” class method, which sets up instance variables “@yui_tree_options” and “@uses_yui_tree” within any controller calling the method. See “self.included” in the plugin code for more information.

The “#uses_yui_tree” method can be passed two optional options hashes. The first specifies options for the YUI tree. The second is passed directly to the Rails before_filter method as filter options and this lets you state conditions for which “#uses_yui_tree” applies (e.g. “:only => ...” for only certain actions or “:except => ...” for all actions except those listed). See the Rails filter API documentation for full details.

Most options for YUI trees can be specified in any of three places:

  1. In the YAML configuration file.

  2. In a call to “#uses_yui_tree” from a controller.

  3. In a call to the “YuiTree::YuiTreeHelper#yui_tree” helper method from a view.

Options in the YAML file are application-global and overriden by those specified in a controller, which are controller-global and in turn overriden by local options given to helper method calls in views.

Some options only make sense if specified in the YAML configuration file and so cannot be specified elsewhere. When installed the plugin writes a default configuration file in “config/yui_tree.yml”; see this for further information on the option meainings:

  • :version

  • :javascript_base_uri

  • :additional_yui_javascripts (an optional item)

The following options usually only make sense if given in the YAML configuration file and have default values written there, but they can be specified per-controller or in each helper call in views if you really want to do that:

  • :xhr_timeout

  • :div_class (the class assigned to any DIV into which a YUI tree is built)

These options can appear in the YAML file or here only, but not in helper calls for individual views:

  • :body_class (a class name added to any existing class name(s) on the BODY element of the HTML document) - if "yui-skin-sam" is used then CSS files for this skin will be included automatically, else you must manually ensure that all relevant CSS resources are included.

Other options must be specified in calls to “#uses_yui_tree”, or to a helper function like “YuiTree::YuiTreeHelper#yui_tree” as they have no default values. You could put them into the YAML file but due to the nature of the options it is very likely that you’ll specify them in method calls instead. In fact it’s more likely that you will want to specify them in calls made in views rather than here in “#uses_yui_tree”, but you can establish Controller-global defaults by specifying the options here if you find this useful.

:root_model

If your model supports tree/nested-set like behaviour - for example, if your model declares “acts_as_nested_set” via the Awesome Nested Set plugin - then specify the root model class here (e.g specify “:root_model => Location” or “:root_model => Category”) as a Controller-global default. Nested set behaviour assumes a class method called “roots” which takes no parameters and returns an array of model instances which are tree roots; and an instance method “leaf?” which also takes no parameters and returns ‘true’ if the instance has no children (is a leaf node), else returns ‘false’.

The root model is used by YUI tree creation code (e.g. see helper method “YuiTree::YuiTreeHelper#yui_tree”) to generate the root data set for the initial tree view. Alternatively, specify this when you make the call to e.g. “YuiTree::YuiTreeHelper#yui_tree” if you don’t want to set a default here. By default the helper code calls an instance method called “name” to obtain label text for the tree nodes. Use the “:root_title_method” option (see below) to specify a different method name if necessary.

The root and child items can be sorted in various ways. See method “yui_tree_handled_xhr_request?” method for further information.

If you want to supply the collection of root objects yourself, rather than using a “:root_model” (see above) and its “roots” class method, you can do so when building the tree in a view through a call to the “YuiTree::YuiTreeHelper#yui_tree” helper method. See its “:root_collection” option for details. Whenever you need to assemble an array of objects to be used as roots or children, you must always build those objects with the “YuiTree.make_node_object” method.

:root_title_method

If using “:root_model” (see above) but with a model which uses something other than a “name” method to return text for showing in tree nodes, then specify the alternative method’s name here as a symbol. It must be an instance method for the model in question and should return a non-empty string.

:xhr_url_method

Name of a method which will be invoked to get the prefix of a URL used to fetch more tree data (when parent nodes are expanded for the first time). This is usually a path to a controller’s ‘index’ action, e.g. “:locations_path” or “:categories_path”. The corresponding Controller action must return an array of child data. It can use the “yui_tree_handled_xhr_request?” method to do this or use entirely custom code. It must return an array of child data as described above (see “:root_model”), generating array entries with the “YuiTree.make_node_object” method. The parent’s database ID is delivered to the controller action via the “params” hash under key “:tree_parent_id” and may have a string, symbol or integer value type.

Some options are likely to be set per-tree but do have default values:

:select_leaf_only

Set to ‘true’ if leaves (nodes without children) can be selected but parents cannot, else ‘false’. Has an internal default value of ‘false’ (i.e. can select anything).

A few YUI tree options can be set as global defaults when making calls to “#uses_yui_tree”, but such defaults only make sense if you use a single tree in any of the containing controller’s views. For example, the option giving the ID of enclosing DIV inside which the tree is built falls into this category, since no two elements in a valid HTML document are allowed to have the same ID. It is up to you where you choose to specify these options - here, or individual calls to “YuiTree::YuiTreeHelper#yui_tree”.

:div_id

ID given to the DIV container into which trees are built. Only specify this as a controller-global default if you are only going to use one tree control in any given view, else more than one DIV container for more than one tree view would have the same ID; or make very sure that any views using more than one tree view specify overriding DIV IDs in those views’ calls to “YuiTree::YuiTreeHelper#yui_tree”.

:div_class

As “:div_id”, but even less frequently used! Usually you will want application-wide styles to apply to trees via CSS and thus specify a single application-global class name in the YUI tree plugin’s YAML configuration file, rather than specifying a controller-global value here, or even a per-tree value in indivdual calls to “YuiTree::YuiTreeHelper#yui_tree”. Nonetheless, the facility exists to use any of those three approaches should they prove useful to you.

:target_form_field_id

Whenever a node is selected, its database ID is written as a value of the “value” (sic.) attribute of the HTML element identified by this option so that a related form submission will contain the selected ID. The field is usually a hidden INPUT element.

Although the tree has a multiple selection mode (see the documentation for “YuiTree::YuiTreeHelper#yui_tree” for details), by default it assumes that you always want to ultimately let the user select a single item in the tree and have that item’s database ID passed back to a controller in a related form submission.

A value of 0 may indicate that a blank entry is use and has been selected. See option “:include_blank” in helper method “YuiTree::YuiTreeHelper#yui_tree”.

An empty string indicates that no nodes were highlighted at all. If you allow such things you should probably interpret this the same as the user explicitly selecting a blank entry. It’s good to allow both as it isn’t too friendly to assume the user will mean that deselecting all nodes means “none/blank”; it’s good to have an explicit entry for that. You can still fault a no-selection form submission from the controller if the target form field value is empty rather than zero, should you wish to do so.

:target_name_field_id

Optional; if included, then when a node is selected, its display text is written as innerHTML inside the identified element. This lets you show the user which node was selected by its text, as well as by the TreeView’s own highlight mechanism. This is handy if you’re worried that a selected node in a collapsed branch remains selected, albeit invisibly - a TreeView implementation quirk, it seems.

If you specify a value for the “:include_blank” option in a call to helper method “YuiTree::YuiTreeHelper#yui_tree”, then note that the specified string will be written into the name field if either a “blank” tree entry item is explicitly selected, or if all items in the tree are entirely deselected. This goes back to the idea in “:target_form_field_id” above of a zero ID value, or a blank ID value, being usually treated as the same thing; but again, the controller can always treat the two conditions as distinct from one another if it so wishes.

Example

# Location model "acts_as_nested_set"; uses Awesome Nested Set plugin;
# thus has "roots" and "leaf?" methods which act as expected. Location
# objects have a displayable name property in "name".

uses_yui_tree(
  { :root_model => Location, :xhr_url_method => :locations_path },
  { :only => [ :edit, :update ] }
)

This prepares the Controller for a YUI tree using a “Location” model. The tree will obtain new data by a JSON XHR request to the URL returned by a call to “locations_path” with “.js” added as a suffix. Only the “:edit” and “:update” actions can use the tree. Listing both of these actions in the “:only” clause is vital since the Controller’s “update” code might re-render the “edit” view (and thus the tree) because of form validation errors. Alternatively use “:exclude” rather than “:only”, or omit such a clause altogether to prepare all the controller’s views for YUI trees.

An XHR URL method of “locations_path” will (assuming sane routes!) lead to an “index” action of a controller for the Location model. The index method must be aware of the YUI tree JSON requests - see “yui_tree_handled_xhr_request?” or use the usual Rails “respond_to do |format| ... format.js do ...” construct.

def index
  return if yui_tree_handled_xhr_request?( Location );
  # ...other processing code...
end

A helper method such as “YuiTree::YuiTreeHelper#yui_tree” must be invoked somewhere in the “:edit” action’s view (e.g. in “edit.html.erb”) so that the tree actually gets built:

<%= hidden_field_tag( 'foo', '[currently selected item ID here]' ) %>
<%= yui_tree( :target_form_field_id => 'foo' ) %>
# File lib/yui_tree/yui_tree.rb, line 247
def uses_yui_tree( tree_options = {}, filter_options = {} )
  proc = Proc.new do | c |
    c.instance_variable_set( :@yui_tree_options, tree_options )
    c.instance_variable_set( :@uses_yui_tree,    true         )
  end

  before_filter( proc, filter_options )

  # See the dummy, static version of "yui_tree_handled_xhr_request?" below
  # for documentation.
  #
  self.class_eval %Q(
    define_method( :yui_tree_handled_xhr_request? ) do | model, *optional |
      result = false

      # WARNING: By default Rails treats XHR requests as ".js" format, if
      # no other format indication (e.g. filename extension, very specific
      # HTTP Accept header) exists. As a result this code can be run for
      # other XHR requests coming into your controller even though you
      # think it ought not to be. Hence the "params.has_key?" check, to try
      # and guard against accidental execution.

      if ( request.xhr? && params.has_key?( :tree_parent_id ) )
        respond_to do | format |
          format.js do

            # Use find_by_id() rather than just find() to avoid an exception if
            # the item cannot be located.

            parent = model.find_by_id( params[ :tree_parent_id ] )

            if ( parent.nil? )
              render :json => []
            else
              children = parent.children()

              if ( model.respond_to?( :apply_default_sort_order ) )
                model.apply_default_sort_order( children )
              end

              children.map!() do | child |
                YuiTree::make_node_object(
                  child.id,
                  child.send( optional[ 0 ] || YuiTree::YUI_TREE_DEFAULT_TITLE_METHOD ),
                  child.send( optional[ 1 ] || :leaf? )
                )
              end

              render :json => children
            end

            result = true

          end # format.js do
        end   # respond_to do | format |
      end     # if ( request.xhr? ... )

      result # Can't do "return result"; leads to ThreadError exception.

    end # define_method...
  )
end
yui_tree_handled_xhr_request?() click to toggle source

When the tree calls back via AJAX and needs some children to fill in a tree branch, call here to return appropriate data. Pass in the model to find from (e.g. Category, Location - a class, not a string or symbol), then optionally the name of the model instance method used to obtain the text to show for each node (default is “name”) and the name of a method used to find out if the item is a leaf (default is “leaf?”) - it should return ‘true’ if so, else ‘false’. Models which “act_as_nested_set” using the Awesome Nested Set plug-in are compatible with the defaults.

If you want the tree order items automatically sorted then add a class method called “apply_default_sort_order” to your model. This is passed an array of model object instances and should sort the array in-place (e.g. with the Array “sort!” method). The method’s return value is ignored. If the model has no “apply_default_sort_order” method then the collection retrieved from the database is left in default sort order. If you have simple requirements then using a call to Rails’ “default_scope” method from your model is the most efficient way to achieve sorted results. For example, in your model issue:

default_scope( { :order => 'name DESC' } )

…to have all collections of that model returned by finder methods sorted in descending order by a “name” field by default. This applies to all finds done by your application, not just those related to YUI trees, so use the “apply_default_sort_order” approach to restrict the ordering to YUI tree views only.

Example

A controller example inside an action which gets invoked when the XHR request comes in:

def action_name
  return if yui_tree_handled_xhr_request?( Location, :title, :isLeaf? )
  # ...rest of normal action code...
end

This would handle YUI tree requests for a Location model using method “title” (rather than the default of “name”) to obtain the human-readable names of locations and method “isLeaf?” (rather than the default of “leaf?”) to determine whether or not the item is at the end of a branch.

# File lib/yui_tree/yui_tree.rb, line 354
def yui_tree_handled_xhr_request?
  # This method is generated dynamically at run-time, the dynamic version
  # overwriting this one. The method here exists purely so that the RDoc
  # documentation generator will create documentation for the method.
  # Please see the implementation of "uses_yui_tree" above to see the code
  # for the dynamically generated method.
end