Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Docs! More Specs.

* Also added api.refreshAPI to reset cached results.
* Disabled the ability to pass a method that's already defined.
* Changed the storage value for returned values from filters to be a little more explicit.
  • Loading branch information...
commit 2e1877c989d8230422fc2b30ce9cfce4dfa84bbe 1 parent 3c0a92a
Aaron Newton authored
169 Docs/Behavior.API.md
View
@@ -0,0 +1,169 @@
+Class: Behavior.API {#Behavior.API}
+==========================
+
+Provides methods to read values from annotated HTML configured for the [Behavior][] class and its associated [Filters](Behavior.md#Behavior.Filter).
+
+### Syntax
+
+ new Behavior.API(element[, prefix]);
+
+### Arguments
+
+1. element - (*element*) An element you wish to read.
+2. prefix - (*string*; optional) A prefix to all the properties; a namespace.
+
+### Notes
+
+Examples of the HTML expressions evaluated are as follows (all of the following produce the same output*):
+
+ <tag data-filters="Filter1 Filter2" data-Filter1-options="{opt1: 'foo', opt2: 'bar', selector: '.selector'}"> //prefered
+ <tag data-filters="Filter1 Filter2" data-Filter1-options="opt1: 'foo', opt2: 'bar', selector: '.selector'"> //no braces on JSON
+ <tag data-filters="Filter1 Filter2" data-Filter1-options="{opt1: 'foo', opt2: 'bar'}" data-Filter1-selector=".selector">
+ <tag data-filters="Filter1 Filter2" data-Filter1-opt1='foo' data-Filter1-opt2='false' data-Filter1-selector=".selector">
+
+The `-options` value is parsed as JSON first (it's slightly more permissive in that 1: you don't have to wrap it in `{}` just for convenience and 2: you don't have to quote all the names). Values defined here are read as defined allowing you to express arrays, numbers, booleans, etc. Functions / callbacks are generally not used by [Behavior][].
+
+If you attempt to read a value that isn't defined in this options object, the property name is attempted to be read from the property directly (e.g. `data-filtername-prop`). This value is *always* a string unless you specify a type. If a type is specified the value is run through the JSON parser and validated against that type.
+
+Behavior.API Method: get {#Behavior.API:getAs}
+------------------------------------------
+
+Gets a value for the specified name.
+
+### Syntax
+
+ api.get(name[, name, name, name])
+
+### Arguments
+
+1. name - (*string*) The name of the property you wish to retrieve. Pass more than one to get back multiple.
+
+### Example
+
+ var api = new Behavior.API(target, 'foo');
+ api.get('bar'); //returns the value of data-foo-bar or null
+ api.get('bar', 'baz'); //returns {bar: 'value', baz: 'value'}
+
+### Returns
+
+* (*mixed*) Values defined as strings will be returned as strings. Values defined in JSON will be returned as their
+ type is evaluated. When you expect anything other than a string it's better to use [getAs](#Behavior.API:getAs).
+ When more than one name is specified you'll receive an object response with key/value pairs for the name/property values.
+
+Behavior.API Method: getAs {#Behavior.API:getAs}
+------------------------------------------
+
+Gets a value for the specified name and runs it through [JSON.decode][] and verifies that the value is parsed as the specified type (specifically a MooTools Type: [String](http://mootools.net/docs/core/Types/String), [Function](http://mootools.net/docs/core/Types/Function), [Array](http://mootools.net/docs/core/Types/Array), [Date](http://mootools.net/docs/more/Types/Date), etc.).
+
+### Syntax
+
+ api.getAs(Type, name[, defaultValue]);
+
+### Arguments
+
+1. Type - (*Type*) A MooTools Type instance (a function) that the value, when run through [JSON.decode][], should return
+2. name - (*string*) The name of the value to read.
+3. defaultValue - (*mixed*) The value to set if there no value found.
+
+### Example
+
+ var api = new Behavior.API(target, 'foo');
+ api.getAs(Number, 'some-number');
+
+### Returns
+
+* (*mixed*) Either returns the value as the Type you specified, the default (if provided), or undefined.
+
+Behavior.API Method: require {#Behavior.API:require}
+------------------------------------------
+
+Validates that an element has a value set for a given name. Throws an error if the value is not found.
+
+### Syntax
+
+ api.require(name[, name, name]);
+
+### Arguments
+
+1. name - (*string*) The name of the property you wish to require. Pass more than one if needed.
+
+### Example
+
+ var api = new Behavior.API(target, 'foo');
+ api.require('foo'); //throws an error if data-foo-foo is not set
+ api.require('foo', 'bar'); //throws an error if data-foo-foo or data-foo-bar are not set
+
+### Returns
+
+* *object* - the instance of Behavior.API.
+
+Behavior.API Method: requireAs {#Behavior.API:requireAs}
+------------------------------------------
+
+Requires that an element has a value set for a given name that can be parsed into a given type (using [JSON.decode][]). If a value is not present or does not parse to the specified Type an error is thrown.
+
+### Syntax
+
+ api.requireAs(obj);
+
+### Arguments
+
+1. obj - (*object*) a set of name/Type pairs to require.
+
+### Example
+
+ api.requireAs({
+ option1: Number,
+ option2: Boolean
+ });
+
+### Returns
+
+* *object* - the instance of Behavior.API.
+
+Behavior.API Method: setDefault {#Behavior.API:setDefault}
+------------------------------------------
+
+Sets the default values. Note that setting defaults for required properties is not useful.
+
+### Syntax
+
+ api.setDefault(name, value);
+ api.setDefault(obj);
+
+### Arguments
+
+1. name - (*string*) The name of the property you wish to set.
+2. value - (*mixed*) The default value for the given name.
+
+OR
+
+1. obj - (*object*) a set of name/value pairs to use if the element doesn't have values present.
+
+### Example
+
+ api.setDefault('duration', 1000);
+ api.setDefault({
+ duration: 1000,
+ link: 'chain'
+ });
+
+### Returns
+
+* *object* - the instance of Behavior.API.
+
+Behavior.API Method: refreshAPI {#Behavior.API:refreshAPI}
+------------------------------------------
+
+The API class caches values read from the element to avoid the cost of DOM interaction. Once you read a value, it is never read from the element again. If you wish to refresh this to re-read the element properties, invoke this method. Note that default values are maintained.
+
+### Syntax
+
+ api.refreshAPI();
+
+### Returns
+
+* *object* - the instance of Behavior.API.
+
+[Behavior]: Behavior.md
+[JSON.decode]: http://mootools.net/docs/core/Utilities/JSON#JSON:decode
272 Docs/Behavior.md
View
@@ -17,14 +17,13 @@ Auto-instantiates widgets/classes based on parsed, declarative HTML.
### Options
-* breakOnErrors - (*boolean*) By default, errors thrown by filters are caught; the onError event is fired. Set this to *true* to NOT catch these errors to allow them to be handled by the browser.
+* breakOnErrors - (*boolean*) By default, errors thrown by filters are caught; the onError event is fired. Set this to `true` to NOT catch these errors to allow them to be handled by the browser.
+* container - (*element*; optional) The DOM element (or its ID) that contains all the applied behavior filters. Defaults to *document.body*;
### Events
-* error - function invoked when an error is caught in a filter. Defaults to console warnings if console.warn is available.
-* resize - call this event, passing in x/y values for the new element size, when the container changes size. See the [resize](#Behavior:resize) method.
-* show - call this event when the container is displayed. See the [resize](#Behavior:show) method.
-* hide - call this event when the container is hidden. See the [resize](#Behavior:hide) method.
+* error - function invoked when an error is caught in a filter. Defaults to console errors if console.error is available.
+* warn - function invoked when a filter calls the warn method no the method api. Defaults to console warnings if console.warn is available.
### Usage
@@ -46,6 +45,14 @@ Behavior is applied to an element whenever you want to parse that element's DOM
The above example will invoke the registered "Accordion" filter. See the section on [Behavior.Filter][] below.
+### HTML properties
+
+Behavior uses a clearly defined API to read HTML properties off the elements it configures. See [Behavior.API][] for details as well as [passMethod](#Behavior:passMethod) for methods that Behavior instances themselves provide.
+
+### Using Multiple Filters Together
+
+It's possible to declare more than one data filter property for a single element (`data-filters="FormRequest FormValidator"`)
+
Behavior Method: passMethod {#Behavior:passMethod}
--------------------------------------------------
@@ -61,14 +68,18 @@ Defines a method that will be passed to filters. Behavior allows you to create a
### Notes
-By default, Behavior passes the following methods to filters:
+By default, Behavior passes the following methods to filters in addition to the methods defined in the [Behavior.API][]
* addEvent - the addEvent on the behavior instance method provided by the [Events][] class.
* removeEvent - the removeEvent on the behavior instance method provided by the [Events][] class.
* addEvents - the addEvents on the behavior instance method provided by the [Events][] class.
* removeEvents - the removeEvents on the behavior instance method provided by the [Events][] class.
-* getContainerSize - returns the current value of *this.containerSize* - does not actually measure the container (which may be hidden or not in the DOM at the time). See the [resize](#Behavior:resize) method for more details.
-* error - fires the behavior instance's *error* event with the arguments passed.
+* fireEvents - the fireEvents on the behavior instance method provided by the [Events][] class.
+* getContentElement - returns the "container" element of the Behavior instance. By default this points to *document.body*. Set `options.container` to change it.
+* getContainerSize - returns the value of getContentElement().getSize(); Note that if that element is not in the DOM this will return zeros.
+* error - fires the behavior instance's `error` event with the arguments passed.
+* fail - stops the filter from iterating and passes a message through to the error logger. Takes a string for the message as its only argument.
+* See the [Behavior.API][] for additional methods passed by default.
You can add any other methods that our filters require. In general, your filters shouldn't reference anything in your environment except these methods and those methods defined in [Behavior.Filter][].
@@ -79,33 +90,11 @@ Iterates over an object of key/values passing them to the [passMethod](#Behavior
### Syntax
- myBehaviorInstance.passMethods(method);
-
-### Returns
-
-* (*object*) this instance of Behavior
-
-Behavior Method: show {#Behavior:show}
---------------------------------------------------
-
-Fires the *show* event which filters can monitor. Does not actually alter the visibility of anything. This is used for filters that need to know when their elements are visible.
-
-### Syntax
-
- myBehaviorInstance.show();
-
-### Returns
-
-* (*object*) this instance of Behavior
-
-Behavior Method: hide {#Behavior:hide}
---------------------------------------------------
-
-Fires the *hide* event which filters can monitor. Does not actually alter the visibility of anything. This is used for filters that need to know when their elements are hidden.
+ myBehaviorInstance.passMethods(obj);
-### Syntax
+### Arguments
- myBehaviorInstance.hide();
+1. obj - (*object*) a set of name/function pairs to pass to the passMethod method.
### Returns
@@ -123,7 +112,7 @@ Applies all the behavior filters for an element and its children.
### Arguments
1. container - (*element*) The DOM container to process behavior filters.
-2. force - (*boolean*; optional) if *true* elements that have already been processed will be processed again.
+2. force - (*boolean*; optional) if `true` elements that have already been processed will be processed again.
### Returns
@@ -142,7 +131,7 @@ Applies a specific behavior filter to a specific element (but not its children).
1. container - (*element*) The DOM container to process behavior filters.
2. filter - (*object*) An instance of [Behavior.Filter][].
-3. force - (*boolean*; optional) if *true* elements that have already been processed will be processed again.
+3. force - (*boolean*; optional) if `true` elements that have already been processed will be processed again.
### Returns
@@ -163,7 +152,7 @@ Given a name, return the registered filter.
### Returns
-* (*object*) the instance of [Behavior.Filter][] or *undefined* if one is not found.
+* (*object*) the instance of [Behavior.Filter][] or `undefined` if one is not found.
Behavior Method: getPlugins {#Behavior:getPlugins}
--------------------------------------------------
@@ -180,7 +169,7 @@ Given a name, return the plugins registered for a filter by that name. See the s
### Returns
-* (*object*) the instance of [Behavior.Filter][] or *undefined* if one is not found.
+* (*object*) the instance of [Behavior.Filter][] or `undefined` if one is not found.
Behavior Method: cleanup {#Behavior:cleanup}
--------------------------------------------------
@@ -202,7 +191,7 @@ Garbage collects the specified element, cleaning up all the filters applied to i
Filters
=======
-Behavior applies all the registered filters to the element you specify (and its children). This requires that each element that should have a behavior applied name the filters it needs in its *data-filters* property. It also means that every named filter must be registered.
+Behavior applies all the registered filters to the element you specify (and its children). This requires that each element that should have a behavior applied name the filters it needs in its `data-filters` property. It also means that every named filter must be registered.
Filters can be registered to an *instance* of Behavior or to the global Behavior namespace. So if you register a "Foo" filter globally, all instance of Behavior get that filter. If a specific instance of Behavior defines a "Foo" filter, then the local instance is used regardless of the presence of a global filter.
@@ -215,13 +204,13 @@ Add a new filter.
### Syntax
- myBehaviorInstance.addFilter(name, fn[, overwrite]);
+ myBehaviorInstance.addFilter(name, filter[, overwrite]);
### Arguments
1. name - (*string*) the name of the filter to add.
-2. fn - (*function*) the function invoked when that filter is run against an element. See [Behavior.Filters][] below.
-3. overwrite - (*boolean*; optional) if *true* and there is already an existing filter by the given name, that filter will be replaced, otherwise the original is retained.
+2. filter - (*function* or *object*) See [Behavior.Filters][] below.
+3. overwrite - (*boolean*; optional) if `true` and there is already an existing filter by the given name, that filter will be replaced, otherwise the original is retained.
### Returns
@@ -239,7 +228,7 @@ Adds a group of filters.
### Arguments
1. obj - (*object*) a key/value set of name/functions to be added.
-2. overwrite - (*boolean*; optional) if *true* and there is already an existing filter by the given name, that filter will be replaced, otherwise the original is retained.
+2. overwrite - (*boolean*; optional) if `true` and there is already an existing filter by the given name, that filter will be replaced, otherwise the original is retained.
### Returns
@@ -259,7 +248,7 @@ Add a new plugin for a specified filter.
1. filterName - (*string*) the name of the filter for the plugin.
2. pluginName - (*string*) the name of the plugin.
3. fn - (*function*) the function invoked after the filter is run against an element. See [plugins](#Behavior.Filter:plugins) below.
-4. overwrite - (*boolean*; optional) if *true* and there is already an existing plugin by the given name, that plugin will be replaced, otherwise the original is retained.
+4. overwrite - (*boolean*; optional) if `true` and there is already an existing plugin by the given name, that plugin will be replaced, otherwise the original is retained.
### Returns
@@ -277,7 +266,7 @@ Adds a group of plugins.
### Arguments
1. obj - (*object*) a key/value set of name/functions to be added as plugins.
-2. overwrite - (*boolean*; optional) if *true* and there is already an existing plugin by the given name, that filter will be replaced, otherwise the original is retained.
+2. overwrite - (*boolean*; optional) if `true` and there is already an existing plugin by the given name, that filter will be replaced, otherwise the original is retained.
### Returns
@@ -327,69 +316,189 @@ Adds a group of plugins to the global Behavior namespace.
Class: Behavior.Filter {#Behavior.Filter}
====================================
-Behavior Filters are where you define what to do with elements that are marked with that filter. Elements can have numerous filters defined and filters can do anything with those elements that they like. In general, filters should only alter the element given, though it is possible to have elements that relate to others (for example, an *Accoridon* filter might set up an instance of *Fx.Accordion* using children that are the togglers and sections).
+Behavior Filters are where you define what to do with elements that are marked with that filter. Elements can have numerous filters defined and filters can do anything with those elements that they like. In general, filters should only alter the element given, though it is possible to have elements that relate to others (for example, an *Accoridon* filter might set up an instance of `Fx.Accordion` using children that are the togglers and sections).
-Typically filters allow for configuration using HTML5 data- properties, classes, and element attributes.
+Typically filters allow for configuration using HTML5 data- properties, classes, and element attributes. See the [Behavior.API][] which automates the reading of these properties.
An important rule of filters is that they cannot know about each other or be in any way dependent on each other. When two filters need to be managed differently when both are present, use a [plugin](#Behavior.Filter:plugins) (this should be rare).
-Filters are typically not created with the constructor (i.e. *new Behavior.Filter*) but instead with the [addFilter](#Behavior.addFilter)/[addFilters](#Behavior.addFilters) methods defined on the Behavior instance or the [addGlobalFilter](#Behavior.addGlobalFilter)/[addGlobalFilters](#Behavior.addGlobalFilters) methods on the Behavior namespace.
+Filters are typically not created with the constructor (i.e. `new Behavior.Filter`) but instead with the [addFilter](#Behavior.addFilter)/[addFilters](#Behavior.addFilters) methods defined on the Behavior instance or the [addGlobalFilter](#Behavior.addGlobalFilter)/[addGlobalFilters](#Behavior.addGlobalFilters) methods on the Behavior namespace.
+
+Filters nearly always return instances of classes (this is essentially their purpose). It's not a requirement, but it is generally preferred.
### Example
- Behavior.addGlobalPlugins({
- Accordion: function(element) {
- var toggles = element.getData('toggler-elements') || '.toggle';
- var sections = element.getData('section-elements') || '.target';
- var accordion = new Fx.Accordion(toggles, sections);
- this.markForCleanup(element, function() {
+ Behavior.addGlobalFilters({
+ Accordion: function(element, api) {
+ var togglers = element.getElements(api.get('togglers'));
+ var sections = element.getElements(api.get('sections'));
+ if (togglers.length == 0 || sections.length == 0) api.fail('There are no togglers or sections for this accordion.');
+ if (togglers.length != sections.length) api.warn('There is a mismatch in the number of togglers and sections for this accordion.')
+ var accordion = new Fx.Accordion(togglers, sections);
+ api.onCleanup(function() {
accordion.detach();
});
+ return accorion; //note that the instance is always returned!
+ }
+ });
+
+ /* the matching HTML
+ <div data-filters="Accordion" data-Accordion-togglers=".toggle" data-Accordion-sections=".section">
+ <div class="toggle">Toggle 1</div>
+ <div class="target">This area is controlled by Toggle 1.</div>
+ </div> */
+
+In the example above our filter finds the sections and togglers and validates that there is at least one of each. If there aren't it calls `api.fail` - this stops the filter's execution and Behavior.js catches it and calls its `onError` event (which defaults to `console.error`). It also checks if the number of togglers and the number of sections are equal and calls `api.warn` if they are off. This does *not* top execution; it only fires the `onWarn` event on Behavior (which defaults to `console.warn`).
+
+### Advanced Filters
+
+A simple filter is just a function and a name ("Accordion") and the function that creates accordions given an element and the api object. This is fine, but it's possible to express more complex configurations. Example:
+
+ Behavior.addGlobalFilters({
+ Accordion: {
+ //if your filter does not return an instance of this value Behavior will throw an error
+ //which is caught and logged by default
+ returns: Accordion,
+ require: ['togglers', 'togglers'],
+ //or
+ requireAs: {
+ togglers: String,
+ someNumericalValue: Number,
+ someArrayValue: Array
+ },
+ //you wouldn't define defaults for required values, but this is just an example
+ defaults: {
+ togglers: '.toggler',
+ sections: '.sections',
+ initialDisplayFx: false
+ },
+ //simple example:
+ setup: function(element, API){
+ var togglers = element.getElements(api.get('togglers'));
+ var sections = element.getElements(api.get('sections'));
+ if (togglers.length == 0 || sections.length == 0) api.fail('There are no togglers or sections for this accordion.');
+ if (togglers.length != sections.length) api.warn('There is a mismatch in the number of togglers and sections for this accordion.')
+ var accordion = new Fx.Accordion(togglers, sections,
+ api.getAs({
+ fixedHeight: Number,
+ fixedWidth: Number,
+ display: Number,
+ show: Number,
+ height: Boolean,
+ width: Boolean,
+ opacity: Boolean,
+ alwaysHide: Boolean,
+ trigger: String,
+ initialDisplayFx: Boolean,
+ returnHeightToAuto: Boolean
+ })
+ );
+ api.onCleanup(function() {
+ accordion.detach();
+ });
+ return accorion; //note that the instance is always returned!
+ },
+ //don't instantiate this value until the user mouses over the target element
+ delayUntil: 'mouseover',
+ //OR delay for a specific period
+ delay: 100,
+ //OR let me initialize the function manually
+ initializer: function(element, API){
+ element.addEvent('mouseover', API.runSetup); //same as specifying event
+ //or
+ API.runSetup.delay(100); //same as specifying delay
+ //or something completely esoteric
+ var timer = (function(){
+ if (element.hasClass('foo')){
+ clearInterval(timer);
+ API.runSetup();
+ }
+ }).periodical(100);
+ //or
+ API.addEvent('someBehaviorEvent', API.runSetup);
+ });
}
});
+In the long-form example above, we see that filters can be passed as objects that map to the config option in the Behavior.Filter constructor arguments. (see [Behavior.Filter's constructor](#Behavior.Filter:constructor)) below.
+
### Accessing passed methods
Behavior has a way to [define API methods passed to filters for their use](#Behavior:passMethod). To use these methods, access them in the second argument passed to your filter function:
Behavior.addGlobalPlugins({
- MeasureOnResize: function(element, behaviorAPI) {
- var updater = function(w,h){
- element.set('html', 'the width is ' + w + ' and the height is ' + h);
- };
- behaviorAPI.addEvent('show', updater);
- this.markForCleanup(element, function(){
- behaviorAPI.removeEvent('show', updater);
+ MeasureOnResize: function(element, api) {
+ api.addEvent('resize', updater);
+ api.onCleanup(function(){
+ api.removeEvent('resize', updater);
});
}
});
+ var myBehaviorInstance = new Behavior();
+ myBehaviorInstance.apply(document.body); //applies all filters named in your content
+ //let's assume there's an element with the data-filters property set to MeasureOnResize
+ myBehaviorInstance.fireEvent('resize');
-As you can see in the example above, we add an event whenever the Behavior instance fires it's "show" method. We also clean up that event with the [markForCleanup](#Behavior.Filter:markForCleanup) method.
+As you can see in the example above, we add an event whenever the Behavior instance fires a "resize" method. We also clean up that event with the [markForCleanup](#Behavior.Filter:markForCleanup) method which is passed through the api object as "onCleanup".
-Behavior.Filter Method: markForCleanup {#Behavior:markForCleanup}
+Behavior.Filter constructor {#Behavior.Filter:constructor}
--------------------------------------------------
-Adds a function to invoke when the element referenced is cleaned up by the Behavior instance.
+While is common (and recommended) for filters to be declared using Behavior's [addFilter](#Behavior.addFilter) method it's possible to create a filter on its own.
+
+### Syntax
+
+ new Behavior.Filter(name, filter);
+
+### Arguments
+
+1. name - (*string*) The name of this filter. This is not used directly by the filter, though Behavior instances use it. Stored as `this.name` on the instance of the filter.
+2. filter - (*function* or *object*) Can be a single function or an object with the config options listed below. The function (which must be present either as the argument or as the `.setup` property on the object) expect to be invoked with an element and an instance of [Behavior.API][] passed to it. Filters in general expect this API object to be provided by a Behavior instance which also adds additional methods (see [Behavior.passMethod](#Behavior:passMethod)) for more details.
+
+### Configuration
+
+If the second argument passed to the constructor is an object, the following options are specified:
+
+* setup - (*function*; required) The function to invoke when the filter is applied.
+* delay - (*integer*; optional) If specified, the filter is to be delayed *by the caller* (typically Behavior instances) by this duration.
+* delayUntil (*string*; optional) If specified, the filter is to be deferred until the event is fired upon the element the filter is applied to. This configuration is applied *by the caller*.
+* initializer - (*function*; optional) If specified, the caller (e.g. a Behavior instance) does *not* call the setup function but instead calls this function, passing in the element and the api object. The api object has an additional method, `api.runSetup`, which this initializer can invoke when it pleases (or not at all).
+* require - (*array*) an array of strings (names) of required attributes on the element. If the element does not have these attributes, the filter fails. Note that the actual attribute name is data-filtername-name (example: data-Accordion-togglers); the data-filtername- portion is not specified in this list of required names, just the suffix (in this example, just "togglers").
+* requireAs - (*object*) a list of required attribute names mapped to their types. The types here being MooTools Type objects (String, Number, Function, etc); actual pointers to the actual Type instance (i.e. not a string).
+* defaults - (*object*) a set of name / default value pairs. Note that setting defaults for required properties makes no sense.
+
+Behavior.Filter Method: markForCleanup {#Behavior.Filter:markForCleanup}
+--------------------------------------------------
+
+Adds a function to invoke when the element referenced is cleaned up by the Behavior instance. Note that Behavior passes this method through as "onCleanup" in it's API object.
### Syntax
myBehaviorFilter.markForCleanup(element, fn);
+ //more commonly inside a filter:
+ api.onCleanup(fn); //element is not specified on the api object
+
### Arguments
1. element - The element passed in to your filter function; the element with the data-filter applied to it.
2. fn - (*function*) the function invoked when that element is garbage collected.
-Behavior.Filter Method: cleanup {#Behavior:cleanup}
+Behavior.Filter Method: cleanup {#Behavior.Filter:cleanup}
--------------------------------------------------
-Garbage collects the specific filter instance for a given element.
+Garbage collects the specific filter instance for a given element. This is typically handled by the Behavior instance when you call its [cleanup](#Behavior:cleanup) method.
### Syntax
myBehaviorFilter.cleanup(element);
+ //more commonly
+ myBehaviorInstance.cleanup(container);
+ //here the container can be any element that is being removed from the DOM
+ //all its children that have had filters applied will have their cleanup method run
+
### Arguments
1. element - The element passed in to your filter function; the element with the data-filter applied to it.
@@ -398,23 +507,36 @@ Garbage collects the specific filter instance for a given element.
Filter Plugins {#FilterPlugins}
====================================
-Filter Plugins are identical to regular filters with the exception that they are invoked only when the filter they are altering is invoked and always after that. Filters do not have any guarantee that they will be invoked in any given order, but plugins are always guaranteed to be invoked after the filter they reference.
+Filter Plugins are identical to regular filters with the exception that they are invoked only when the filter they are altering is invoked and always after that. Filters do not have any guarantee that they will be invoked in any given order, but plugins are always guaranteed to be invoked after the filter they reference. More specifically, they are always invoked after all the filters on an element are invoked. If an element has two filters (A and B) and each of these filters have plugins (A1 and B1) the invocation order will be A, B, A1, B1.
### Example
- Behavior.defineGlobalPlugin('Mask', 'AlertOnMask', function(element, behaviorAPI){
- var mask = element.retrieve('Mask'); //get the instance of the Mask class created in the Mask filter
- var aleter = function(){
- alert('the mask is visible!');
+ Behavior.addFilter('Mask', function(element, api){
+ var maskInstance = new Mask(element);
+ //this is silly
+ var events = {
+ mouseover: maskInstance.show.bind(maskInstance),
+ mouseout: maskInstance.hide.bind(maskInstance)
};
- mask.addEvent('show', alerter);
- this.markForCleanup(element, function(){
- mask.removeEvent('show', alerter);
+ element.addEvents(events);
+ api.onCleanup(function(){
+ element.removeEvents(events);
+ });
+ return maskInstance; //note that we return the instance!
+ });
+
+ Behavior.defineGlobalPlugin('Mask', 'AlertOnMask', function(element, api, maskInstance){
+ //also silly
+ var aleter = function(){ alert('the mask is visible!'); };
+ maskInstance.addEvent('show', alerter);
+ api.onCleanup(function(){
+ maskInstance.removeEvent('show', alerter);
});
});
-The above example is guaranteed to always run after the "Mask" filter. You can define a plugin for a plugin just as well; simply name the plugin as the first argument (you could create a plugin for the above example by making a plugin for "AlertOnMask").
+The above example is guaranteed to always run after the "Mask" filter. You can define a plugin for a plugin just as well; simply name the plugin as the first argument (you could create a plugin for the above example by making a plugin for "AlertOnMask"). Plugin setup functions are passed not only the target element and the api object but also the instance returned by the filter they augment.
[Options]: http://mootools.net/docs/core/Class/Class.Extras#Options
[Events]: http://mootools.net/docs/core/Class/Class.Extras#Events
-[Behavior.Filter]: #Behavior.Filter
+[Behavior.Filter]: #Behavior.Filter
+[Behavior.API]: #Behavior.API
36 README.md
View
@@ -2,28 +2,40 @@
Auto-instantiates widgets/classes based on parsed, declarative HTML.
+### Purpose
+
+All well-written web sites / apps that are interactive have the same basic pattern:
+
+![Web app layers](https://github.com/anutron/behavior/raw/master/layers.png)
+
+Each page of the site or app is esoteric. It may have any combination of interactive elements, some of which interact with each other (for example, a form validation controller might interact with an ajax controller to prevent it sending a form that isn't valid). Typically this "glue" code exists in a DomReady statement. It says, get *this* form and instantiate *that* class with *these* options. This code is brittle; if you change either the DOM or the code the state breaks easily. It's not reusable, it only works for a specific page state. It can easily get out of hand.
+
+Behavior attempts to abstract that DomReady code into something you only write once and use often. It's fast and easily customized and extended. Instead of having a DomReady block that, say, finds all the images on a page and turns them into a gallery, and another block that searches the page for all the links on the page and turns them into tool tips, Behavior does a single search for all the elements you've marked. Each element is passed through the filter it names, where a filter is a function (and perhaps some configuration) that you've named. Each of these functions takes that element, reads properties defined on it in a prescribed manner and invokes the appropriate UI component.
+
## Documentation
See markdown files in the *Docs* directory.
+* [Behavior](Docs/Behavior.md)
+* [Behavior.API](Docs/Behavior.API.md)
+* [Element.Data](Docs/Element.Data.md)
+
## Notes
Below are some notes regarding the implementation. The documentation should probably be read first as it gives usage examples.
-* Only one selector is ever run; adding 1,000 filters doesn't affect performance
-* Nodes can have numerous filters
-* Nodes can have an arbitrary number of related properties (*data-foo-value*, *data-bar-value*); this arbitrary quality is cause for some debate, but for now it's proven to be extremely flexible
-* Elements can be retired w/ custom destruction (*markForCleanup*); cleaning up an element also cleans up all the children of that element that have had behaviors applied
-* Behaviors are only ever applied once to an element; if you call *myBehavior.apply(document.body)* a dozen times, the elements with filters will only have those filters applied once (can be forced to override and re-apply).
+* Only one selector is ever run; adding 1,000 filters doesn't affect performance.
+* Nodes can have numerous filters.
+* Nodes can have an arbitrary number of supported options for each filter (`data-filterame-foo="bar"`).
+* Nodes can define options as JSON (this is actually the preferred implementation - `data-filtername-options="<your JSON>"`).
+* Elements can be retired w/ custom destruction; cleaning up an element also cleans up all the children of that element that have had behaviors applied.
+* Behaviors are only ever applied once to an element; if you call `myBehavior.apply(document.body)` a dozen times, the elements with filters will only have those filters applied once (can be forced to override and re-apply).
* Filters are instances of classes that are applied to any number of elements. They are named uniquely.
* There are "global" filters that are registered for all instances of behavior.
-* Instance filters get precedence. This allows for libraries to provide filters (like [http://github.com/anutron/more-behaviors](http://github.com/cloudera/more-behaviors)) but for a specific instance to overwrite it without affecting the global state. (This pattern is in MooTools' *FormValidator* and works pretty well).
-* Filters have "plugins". A requirement for Filters is that they are unaware of each other. They have no guarantee that they will be invoked in any order (the developer writing the HTML expresses their order) or that they will be invoked with others or on their own. In addition to this ignorance, it's entirely likely that in specific environments a developer might want to augment a filter with additional functionality invoked whenever the filter is. This is what plugins are for. Plugins name the filter they augment but otherwise are just filters themselves. It's possible to have plugins for plugins. At the moment, we use plugins for code that is esoteric to our environment ([Hue](http://github.com/cloudera/hue) or [JFrame](http://github.com/cloudera/jframe)) and when we need to make two filters aware of each other (*FilterInput* + *HtmlTable.Zebra*). Note that plugins are always executed after all the filters are, so when writing a plugin that checks for a combination of two filters it is guaranteed that both filters have been applied.
-* Behavior defines a bottleneck for passing environment awareness to filters (*passMethod* / *behaviorAPI*). I wanted to avoid authoring filters that knew too explicitly about the environment they were invoked in. If the filters, for example, had to be able to call a method on *JFrame*, the filter shouldn't have to have a pointer to that instance itself. It would make things brittle; a change in *JFrame* would break any number of unknown filters. By forcing the code that creates the Behavior instance to declare what methods filters can use it makes a more maintainable API. JFrame passes a LOT of methods this way, but at least we know what they are and we can search the filters for where they are used.
+* Instance filters get precedence. This allows for libraries to provide filters (like [http://github.com/anutron/more-behaviors](http://github.com/anutron/more-behaviors)) but for a specific instance to overwrite it without affecting the global state. (This pattern is in MooTools' `FormValidator` and works pretty well).
+* Filters have "plugins". A requirement for Filters is that they are unaware of each other. They have no guarantee that they will be invoked in any order (the developer writing the HTML expresses their order) or that they will be invoked with others or on their own. In addition to this ignorance, it's entirely likely that in specific environments a developer might want to augment a filter with additional functionality invoked whenever the filter is. This is what plugins are for. Plugins name the filter they augment but otherwise are just filters themselves. It's possible to have plugins for plugins. When you need to make two filters aware of each other (`FilterInput` + `HtmlTable.Zebra`). Note that plugins are always executed after all the filters are, so when writing a plugin that checks for a combination of two filters it is guaranteed that both filters have been applied.
+* Behavior defines a bottleneck for passing environment awareness to filters (`passMethod` / `behaviorAPI`). Filters should not know too explicitly about the environment they were invoked in. If the filters, for example, had to be able to call a method on some containing class - one that has created an instance of Behavior for example, the filter shouldn't have to have a pointer to that instance itself. It would make things brittle; a change in that class would break any number of unknown filters. By forcing the code that creates the Behavior instance to declare what methods filters can use it makes a more maintainable API.
## Limitations:
-* Due to the DOM-searching for both creation and destruction, you can't have behavior instance's inside each other.
-* There's a weak notion of "updates" for filters; there's show, hide, resize, but these have vague meanings. For instance, if you have a filter that needs to measure itself, it must do this on show and resize. But if the element that is controlled by the filter changes size and not the container, how does it know? This quality is marginally problematic and inelegant.
-* Right now we require ALL of the global filters for *JFrame* instances. The filters require the things they set up (so *Behavior.FormValidator* requires *FormValidator* from MooTools More). As a result, the amount of code for a *JFrame* is rather large. In theory, we could integrate Behavior w/ *[Depender](http://github.com/anutron/depender)* so that when Behavior finds a filter that isn't defined it requires it from *Depender* before it sets it up. This would greatly reduce the initial footprint of our first app. It would complicate Behavior in that it would require the *Depender* client to work. It probably means we extend/patch Behavior in our environment only if we do this.
-* Currently filter plugins that need to reference an instance created by the filter (a plugin for *FormValidator* that needs to reference the instance of *FormValidator*) must retrieve that instance it from the element. That means either the *FormValidator* class or the behavior filter must store it there. It's not always enforced or available. More generally, if a filter were to create numerous variables the plugin would have to recreate them (imagine a filter that did a DOM search based on a selector in a data property; the plugin would have to perform that search, too, unless the filter were to store the results somewhere, perhaps in the class instance it invoked). A pattern to consider here is to have a filters' invocation to return the instance and behavior to pass what was returned on to plugins.
+* Due to the DOM-searching for both creation and destruction, you can't have behavior instance's inside each other.
27 Source/Behavior.API.js
View
@@ -13,6 +13,7 @@ provides: [Behavior.API]
Behavior.API = new Class({
element: null,
prefix: '',
+ defaults: {},
initialize: function(element, prefix){
this.element = element;
@@ -31,21 +32,23 @@ provides: [Behavior.API]
require: function(/* name[, name, name, etc] */){
for (var i = 0; i < arguments.length; i++){
- if (this.get(arguments[i]) == undefined) throw 'Could not find ' + this.prefix + '-' + arguments[i] + ' option on element.';
+ if (this._getValue(arguments[i]) == undefined) throw 'Could not find ' + this.prefix + '-' + arguments[i] + ' option on element.';
}
+ return this;
},
requireAs: function(returnType, name /* OR {name: returnType, name: returnType, etc}*/){
var val;
if (typeOf(arguments[0]) == 'object'){
for (var objName in arguments[0]){
- val = this.getAs(arguments[0][objName], objName);
- if (val === undefined || val === null) throw "Could not find " + this.prefix + '-' + objName + " option on element or it's type was invalid.";
+ val = this._getValueAs(arguments[0][objName], objName);
+ if (val === undefined || val === null) throw "Could not find " + this.prefix + '-' + objName + " option on element or its type was invalid.";
}
} else {
- val = this.getAs(returnType, name);
- if (val === undefined || val === null) throw "Could not find " + this.prefix + '-' + name + " option on element or it's type was invalid.";
+ val = this._getValueAs(returnType, name);
+ if (val === undefined || val === null) throw "Could not find " + this.prefix + '-' + name + " option on element or its type was invalid.";
}
+ return this;
},
setDefault: function(name, value /* OR {name: value, name: value, etc }*/){
@@ -55,16 +58,24 @@ provides: [Behavior.API]
}
return;
}
- if (this.get(name) == null){
+ this.defaults[name] = value;
+ if (this._getValue(name) == null){
var options = this._getOptions();
options[name] = value;
}
+ return this;
+ },
+
+ refreshAPI: function(){
+ delete this.options;
+ this.setDefault(this.defaults);
+ return;
},
_getObj: function(names){
var obj = {};
names.each(function(name){
- obj[name] = this.get(name);
+ obj[name] = this._getValue(name);
}, this);
return obj;
},
@@ -85,7 +96,7 @@ provides: [Behavior.API]
return options[name];
},
_getValueAs: function(returnType, name, defaultValue){
- var value = this._coerceFromString(returnType, this.get(name));
+ var value = this._coerceFromString(returnType, this._getValue(name));
return instanceOf(value, returnType) ? value : defaultValue;
},
_getValuesAs: function(obj){
43 Source/Behavior.js
View
@@ -28,6 +28,7 @@ provides: [Behavior]
//by default, errors thrown by filters are caught; the onError event is fired.
//set this to *true* to NOT catch these errors to allow them to be handled by the browser.
// breakOnErrors: false,
+ // container: document.body,
//default error behavior when a filter cannot be applied
onError: getLog('error'),
@@ -46,7 +47,11 @@ provides: [Behavior]
applyFilters: this.apply.bind(this),
applyFilter: this.applyFilter.bind(this),
getContentElement: this.getContentElement.bind(this),
- getContainerSize: function(){ return this.getContentElement().getSize(); }.bind(this),
+ getContainerSize: function(){
+ return this.getContentElement().measure(function(){
+ return this.getSize();
+ });
+ }.bind(this),
error: function(){ this.fireEvent('error', arguments); }.bind(this),
fail: function(){
var msg = Array.join(arguments, ' ');
@@ -57,23 +62,24 @@ provides: [Behavior]
}.bind(this)
});
},
-
+
+ getContentElement: function(){
+ return this.options.container || document.body;
+ },
+
//pass a method pointer through to a filter
//by default the methods for add/remove events are passed to the filter
//pointed to this instance of behavior. you could use this to pass along
//other methods to your filters. For example, a method to close a popup
//for filters presented inside popups.
- getContentElement: function(){
- return this.options.container || document.body;
- },
-
passMethod: function(method, fn){
+ if (this.API.prototype[method]) throw 'Cannot overwrite API method ' + method + ' as it already exists';
this.API.implement(method, fn);
return this;
},
passMethods: function(methods){
- this.API.implement(methods);
+ for (method in methods) this.passMethod(method, methods[method]);
return this;
},
@@ -89,12 +95,13 @@ provides: [Behavior]
if (!filter){
this.fireEvent('error', ['There is no filter registered with this name: ', name, element]);
} else {
- if (filter.config.delay !== undefined){
- this._delayFilter(filter.config.delay, element, filter, force);
- } else if(filter.config.delayUntil){
- this._delayFilterUntil(filter.config.delayUntil, element, filter, force);
- } else if(filter.config.initializer){
- this._customInit(filter.config.initializer, element, filter, force);
+ var config = filter.config;
+ if (config.delay !== undefined){
+ this._delayFilter(config.delay, element, filter, force);
+ } else if(config.delayUntil){
+ this._delayFilterUntil(config.delayUntil, element, filter, force);
+ } else if(config.initializer){
+ this._customInit(config.initializer, element, filter, force);
} else {
plugins.extend(this.applyFilter(element, filter, force, true));
}
@@ -179,7 +186,7 @@ provides: [Behavior]
if (filter.config.returns && !instanceOf(result, filter.config.returns)){
throw "Filter " + filter.name + " did not return a valid instance.";
}
- element.store('Behavior:' + filter.name, result);
+ element.store('Behavior Filter result:' + filter.name, result);
//and mark it as having been previously applied
applied[filter.name] = filter;
//apply all the plugins for this filter
@@ -215,7 +222,7 @@ provides: [Behavior]
var applied = getApplied(element);
for (var filter in applied){
applied[filter].cleanup(element);
- element.eliminate('Behavior:' + filter);
+ element.eliminate('Behavior Filter result:' + filter);
delete applied[filter];
}
if (!ignoreChildren) element.getElements('[data-filters]').each(this.cleanup, this);
@@ -295,7 +302,7 @@ provides: [Behavior]
opt2: 2
},
//simple example:
- setup: funciton(element, API){
+ setup: function(element, API){
var kids = element.getElements(API.get('selector'));
//some validation still has to occur here
if (!kids.length) API.fail('there were no child elements found that match ', API.get('selector'));
@@ -323,7 +330,7 @@ provides: [Behavior]
}).periodical(100);
//or
API.addEvent('someBehaviorEvent', API.runSetup);
- }
+ });
*/
},
@@ -400,7 +407,7 @@ provides: [Behavior]
},
getFilterResult: function(name){
- return this.retrieve('Behavior:' + name);
+ return this.retrieve('Behavior Filter result:' + name);
}
});
18 Specs/Behavior/Behavior.API.Specs.js
View
@@ -59,6 +59,18 @@
expect(api.get('two')).toBe(2);
});
+ it('should reset cached values', function(){
+ var clone = target.clone(true, true);
+ var api = new Behavior.API(clone, 'filtername');
+ api.setDefault('fred', 'flintsone');
+ expect(api.get('number')).toBe('0');
+ clone.setData('filtername-number', '5');
+ expect(api.get('number')).toBe('0');
+ api.refreshAPI();
+ expect(api.get('number')).toBe('5');
+ });
+
+
it('should require an option that is present', function(){
var api = new Behavior.API(target, 'filtername');
api.require('number');
@@ -85,14 +97,14 @@
api.requireAs(Number, 'true');
expect(true).toBe(false); //this shouldn't get this far as an error should be thrown
} catch(e) {
- expect(e).toBe('Could not find filtername-true option on element or it\'s type was invalid.');
+ expect(e).toBe('Could not find filtername-true option on element or its type was invalid.');
}
try {
api.requireAs(Boolean, 'number');
expect(true).toBe(false); //this shouldn't get this far as an error should be thrown
} catch(e) {
- expect(e).toBe('Could not find filtername-number option on element or it\'s type was invalid.');
+ expect(e).toBe('Could not find filtername-number option on element or its type was invalid.');
}
try {
@@ -102,7 +114,7 @@
'number': Boolean
});
} catch(e){
- expect(e).toBe('Could not find filtername-number option on element or it\'s type was invalid.');
+ expect(e).toBe('Could not find filtername-number option on element or its type was invalid.');
}
});
26 Specs/Behavior/Behavior.Specs.js
View
@@ -159,7 +159,7 @@
}
if (options.truthy) expect(instanceOf(target.getFilterResult('Require'), SimpleClass)).toBeTruthy();
else expect(target.getFilterResult('Require')).toBeFalsy();
- target.eliminate('Behavior:Require');
+ target.eliminate('Behavior Filter result:Require');
target.removeDataFilter('Require');
behaviorInstance.options.breakOnErrors = false;
@@ -193,7 +193,7 @@
'nine': Array
},
truthy: false,
- catcher: "Could not find Require-nine option on element or it's type was invalid."
+ catcher: "Could not find Require-nine option on element or its type was invalid."
})
);
@@ -330,6 +330,28 @@
});
+ it('should pass a method to a filter via the API', function(){
+ var val = false;
+ behaviorInstance.passMethod('changeVal', function(){
+ val = true;
+ });
+ target.addDataFilter('PassedMethod');
+ Behavior.addGlobalFilter('PassedMethod', function(el, api){
+ api.changeVal();
+ });
+ behaviorInstance.apply(container);
+ expect(val).toBe(true);
+ target.removeDataFilter('PassedMethod');
+ });
+
+ it('should throw an error when attempting to pass a method that is already defined', function(){
+ try {
+ behaviorInstance.passMethod('addEvent', function(){});
+ expect(true).toBe(false); //this shouldn't get this far as an error should be thrown
+ } catch (e) {
+ expect(e).toBe('Cannot overwrite API method addEvent as it already exists');
+ }
+ });
// plugins
BIN  layers.png
View
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Please sign in to comment.
Something went wrong with that request. Please try again.