MillionMunkeys.net

 

PiMunkey Documentation -> Pi Object Reference ->

Listener & Filter Functions

Listeners

Listeners are fired whenever the set() function is called. They can be either "global listeners" or "property listeners". Global Listeners are fired whenever anything changes on a Pi Object. They are most commonly used with Pi Objects acting as Arrays, but can also be used with Pi Objects acting as structs. Property Listeners, in contrast, are only fired when one particular property changes on the object. The possibilities are almost limitless on how to use a listener. You can use them to update other values when a key value changes, or to override or rollback a change when certain conditions occur. When using "global listeners" on a Pi Array, you can ensure that certain actions happen, regardless of where in your code an item is added to the Array. Some other coder on your team could create an entirely new page that adds objects to your Pi Object, and you don't have to worry about them remembering to also include your special processing code. It will automatically execute.

For more details and examples, see the sections below on "addListener" and "removeListener".

Filters

Filters are fired whenever the get() function is called. As with listeners, they can be either "global filters" or "property filters". Global Filters intercept any request made on a Pi Object. Property filters intercept requests for only one particular property on a Pi Object. You can use filters mask or hide information, or to add additional information based upon the requestor. You can also use them to create "virtual properties," i.e. add a filter for a property that doesn't actually exists. If someone requests that property, your filter method can instead calculate an answer and return it to the requestor. And as with listeners, you setup your filters on your objects and then it doesn't matter who uses your objects. Someone else on your team could create new code that requests a property or an object, and they won't have to remember to do any special processing or conditional code. They just request the property and use the result.

For more details and examples, see the sections below on "addFilter" and "removeFilter".

Unanticipated Extendability

The extra benefit of listeners and filters is the unanticipated tangle. Perhaps you're two weeks from delivery and the client says that they forgot that they need an audit trail. Anytime anything changes on a request, they want to log it to a file. There's no telling how many places in your code might edit the object. With Pi Objects, though, it's no problem. Just add a global listener to your collection, which listens to anything the object has added to it, and write to the log file whenever any property on any item is modified.

This is the tragic flaw of "event-driven" methodology. If the event doesn't exist yet, you have to go through every place in your code where the object is edited and fire this new "edit" event. With Property-Invocation, everything is a potential plugin point, with no additional work and little or no additional overhead in execution time.

Methods

  • addFilter: Adds a filter function to either the entire object as a whole, or a specific property within the object. NOTE: You may add a filter for a property even if the property doesn't exist yet. It will still be fired whenever that property is requested from the Pi Object, and regardless of whether the property actually exists or not.

    Usage:

    ColdFusion or Javascript:
    1. addFilter (string property, object object, string method_name) : Use three arguments to add a "property filter" that fires whenever the specified property is requested from the Pi Object.
      • string property : The first argument is the name of a property within the Pi Object. NOTE: You may add a filter to a property even if the property doesn't exist yet. It will still be fired whenever that property is requested from the Pi Object, and regardless of whether the property actually exists or not.
      • object object : The second argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do.
      • string method_name: The third argument must be the name of a method within the object specified in the second argument.
    2. addFilter (object object, stringmethod_name) : Use two arguments to add a "global filter" that fires whenever any item is requested from the Pi Object. This format is particularly useful when using Pi Objects as arrays.
      • object object : The first argument must be a valid object. It can be a Pi Object, but it does not need to be; Any object will do.
      • string method_name: The second argument must be the name of a method within the object specified in the first argument.
    Javascript only:
    1. addFilter (string property, [object scope,] function method) : Use this function with a property name as the first argument to add a "property filter" that fires whenever the specified property is requested from the Pi Object.
      • string property : The first argument is the name of a property within the Pi Object. NOTE: You may add a filter to a property even if the property doesn't exist yet. It will still be fired whenever that property is requested from the Pi Object, and regardless of whether the property actually exists or not.
      • object scope : [optional] The second optional argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do. If a scope is not provided, the object to which the filter is being attached is used as the scope.
      • function method : The third argument must be a reference to a function. This function can be a stand-alone function, or a method on an object. This function does not have to belong to the object specified in the second argument, but it can.
    2. addFilter ([object scope,] function method) : Use this function without the property name to add a "global filter" that fires whenever any item is requested from the Pi Object. This format is particularly useful when using Pi Objects as arrays.
      • object scope : [optional] The first optional argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do. If a scope is not provided, the object to which the filter is being attached is used as the scope.
      • function method : The second argument must be a reference to a function. This function can be a stand-alone function, or a method on an object. This function does not have to belong to the object specified in the first argument, but it can.

    Filter Method Arguments:

    1. object: A reference to the Pi Object.
    2. property: The name of the property being requested. NOTE: This argument is always passed to the filter, regardless of whether the filter is a "global filter" or a "property filter".
    3. value: The value of the property being returned. If the property does not exist, this value will be the empty string.

    Filter Method Result:

    If you do not return a value from your filter method, then the "value" argument passed to your method will be passed back to the original caller unmodified. If you do return a value from your filter method, however, you will override the value being passed into your filter method. This will not change the value of the item in the Pi Object, but it will change the value returned to the original caller.

    CAUTION: If you edit the value of the property in the Pi Object, but do not return the new value from the filter function, then the original caller will still receive the original value.

    Examples:

    In this first example we're going to add a global filter to an array of requests in a workflow. Anytime a request is retrieved we want to assign that request to the user so that other people don't try to claim it.

    ColdFusion:
    // Array Filter Setup Code
    <cfset requestManager.addFilter(AccessController, "setOwnerId") />
    // Global Filter Method:
    <cfcomponent hint="Manages Access and Permissions to Objects">
         ...
         <cffunction name="setOwnerId">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="value" />
              <cfset var request = Arguments.value />
              <cfset request.set("ownerId", GetAuthUser()) />
         </cffunction>
         ...
    </cfcomponent>

    Javascript:
    // Global Filter Method
    AccessController = new PiObject();
    AccessController.setOwnerID = function(object, property, value) {
         value.set( "ownerId", user.get("id") );
    }
    // Array Filter Setup Code
    requestManager.addFilter( AccessController.setOwnerId ); // single argument sets the scope of the function call to requestManager.
    requestManager.addFilter( AccessController, AccessController.setOwnerId ); // two arguments set the scope of the function call to AccessController.

    In this second example we're going to add a filter to non-existant "user_status" property. Whenever someone requests this property, we're going to instead show them the user's first name, last name, and the number of requests that they are currently processing.

    ColdFusion:

    <cfcomponent hint="User Object" extends="MillionMunkeys.PiMunkey_1_5.PiComponent">
         // Property Filter Setup Code
         <cfset this.addFilter("user_status", this, "getCurrentStatus") />
         ...
         // Property Filter Method
         <cffunction name="getCurrentStatus">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="value" />
              <cfreturn "#this.get('first_name')# #this.get('last_name')# (#this.get('myRequests').getLength()#)" />
         </cffunction>
         ...
    </cfcomponent>

    Javascript:
    // The following example assigns one of its own functions as a filter on itself.
    User = PiObject.extend();
    User.prototype.init = function(config) {
         User.parent.init.apply(this,arguments);
         this.addFilter("user_status", User.prototype.getCurrentStatus);
    }
    User.prototype.getCurrentStatus = function(object, property, value) {
         return this.get('first_name') + " " + this.get('last_name') + " (" + this.get('myRequests').getLength() + ")";
    }

  • addListener: Adds a listener function to either the entire object as a whole, or a specific property within the object. NOTE: You may add a listener to a property even if the property doesn't exist yet. It will be fired when the property is added to the object.

    Usage:

    ColdFusion or Javascript:
    1. addListener (string property, object object, string method_name [, boolean applyToExisting]) : Use three arguments (with an optional boolean fourth argument) to add a "property listener" that fires whenever the specified property is modified on the Pi Object.
      • string property : The first argument is the name of a property within the Pi Object. NOTE: You may add a listener to a property even if the property doesn't exist yet. It will be fired when the property is added to the object.
      • object object : The second argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do.
      • string method_name: The third argument must be the name of a method within the object specified in the second argument.
      • boolean applyToExisting : [default = true] The optional fourth argument determines whether the method should be immediately fired after being added. If the Pi Object already contains the specified property, the listener function will be run.
    2. addListener (object object, string method_name [, boolean applyToExisting]) : Use two arguments (with an optional boolean third argument) to add a "global listener" that fires whenever any property is modified on the Pi Object. This format is particularly useful when using Pi Objects as arrays.
      • object object : The first argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do.
      • string method_name: The second argument must be the name of a method within the object specified in the first argument.
      • boolean applyToExisting : [default = true] The optional third argument determines whether the method should be immediately fired after being added. If the Pi Object is not empty, the listener function will be fired once for each item in the object.
    Javascript only:
    1. addListener (string property, [object scope,] function method [, boolean applyToExisting]) : Use this function with a property name as the first argument to add a "property listener" that fires whenever the specified property is modified on the Pi Object.
      • string property : The first argument is the name of a property within the Pi Object. NOTE: You may add a listener to a property even if the property doesn't exist yet. It will be fired when the property is added to the object.
      • object scope : [optional] The second optional argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do. If a scope is not provided, the object to which the filter is being attached is used as the default scope.
      • function method : The second or third argument must be a reference to a function. This function can be a stand-alone function, or a method on an object. This function does not have to belong to the object specified as the scope, but it can.
      • boolean applyToExisting : [default = true] The optional final argument determines whether the method should be immediately fired after being added. If the Pi Object already contains the specified property, the listener function will be run, otherwise it.
    2. addListener ([object scope,] function method [, boolean applyToExisting]) : Use this function without the property name to add a "global listener" that fires whenever any property is modified on the Pi Object. This format is particularly useful when using Pi Objects as arrays.
      • object scope : [optional] The first optional argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do. If a scope is not provided, the object to which the filter is being attached is used as the scope.
      • function method : The first or second argument must be a reference to a function. This function can be a stand-alone function, or a method on an object. This function does not have to belong to the object specified in the first argument, but it can.
      • boolean applyToExisting : [default = true] The optional final argument determines whether the method should be immediately fired after being added. If the Pi Object is not empty, the listener function will be fired once for each item in the object.

    Listener Method Arguments:

    1. object: A reference to the object being modified.
    2. property: The name of the property being modified. NOTE: This argument is always passed to the listener, regardless of whether the listener is a "global listener" or a "property listener".
    3. oldValue: The value of the property before it was modified. If the property did not previously exist, this value will be the empty string.
    4. newValue: The value of the property after it was modified. If the property was removed, the value will be the empty string.

    Listener Method Result:

    If you do not return a value from within your listener method, then the "newValue" argument passed to your method will be stored in the Pi Object. If you do return a value from within your listener method, however, you will override the value being saved.

    NOTE: The value of the property has already been set by the time it reaches your listener method. If you directly edit the value of the property in the Pi Object, but do not return a value from your method, there's no need to worry. Your edit will NOT be overwritten after exiting your method.

    Examples:

    In this first example we're going to add a global listener to an array of pages. Anytime a new page is added, we want to dynamically generate a unique URL for the page based on its page ID. We're also going to run this method against any pages already in the array.

    ColdFusion:
    // Global Listener Method:
    <cfcomponent extends="MillionMunkeys.PiMunkey_1_5.PiObject" hint="TrafficMunkey">
         // Array Listener Setup Code:
         <cfset pages = newMunkey() />
         <cfset pages.addListener(this, "addURLVariable", true) />
         ...
         <cffunction name="addURLVariable">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="oldValue" />
              <cfargument name="newValue" />
              <cfset var newPage = Arguments.newValue />
              <cfif not newPage.exists("url") and newPage.exists("pageId")>
                   <cfset newPage.set("url", "#this.get('baseURL')#?#this.get('urlVariable')#=#newPage.get('pageId')#") />
              </cfif>
         </cffunction>
         ...
    </cfcomponent>

    Javascript:
    // As part of the TrafficMunkey definition.
    TrafficMunkey = PiObject.extend();
    TrafficMunkey.prototype.init = function(config) {
         TraficMunkey.parent.init.apply(this,arguments);
         this.set('pages', new PiObject()).addListener(this, this.addURLVariable);
    }
    TrafficMunkey.prototype.addURLVariable = function(pages, property, oldPage, newPage) {
         if (!newPage.exists("url") && newPage.exists("pageId"))
              newPage.set("url", this.get('baseURL') + "?" + this.get('urlVariable') + "=" + newPage.get('pageId');
    }

    In this second example we're going to add a listener to a single page object. If its page ID is ever changed, we want to update its URL with a new unique URL, based on the new page ID. We also want remove its old ID from the page lookup list, and register it under its new ID. However, if the ID was deleted, then we don't want to register this page anymore, just remove it from the page lookup entirely.

    ColdFusion:
    // Property Listener Setup Code
    <cfset page.addListener("pageId", TrafficMunkey, "addURLVariable", true) />
    ...
    <cfcomponent extends="MillionMunkeys.PiMunkey_1_5.PiObject" hint="TrafficMunkey">
         ...
         // Property Listener Method
         <cffunction name="addURLVariable">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="oldValue" />
              <cfargument name="newValue" />
              <cfset var pageLookup = this.get("pageLookup") />
              <cfset pageLookup.remove(Arguments.oldValue) />
              <cfif Arguments.newValue neq "">
                   <cfset Arguments.object.set("url", "#this.get('baseURL')#?#this.get('urlVariable')#=#Arguments.newValue#") />
                   <cfset pageLookup.set(Arguments.newValue, Arguments.object) />
              <cfelse>
                   <cfset Arguments.object.remove("url") />
              </cfif>
         </cffunction>
         ...
    </cfcomponent>

    Javascript:
    // Property Listener Setup Code
    page.addListener("pageId", TrafficMunkey, TrafficMunkey.addURLVariable, true);
    ...
    TrafficMunkey = PiObject.extend();
    TrafficMunkey.prototype.addURLVariable = function(page,property,oldID,newID) {
         var pageLookup = this.get("pageLookup");
         pageLookup.remove(oldID) />
         if (newID != "")
              pageLookup.set(newID, object.set( { "url" : this.get('baseURL') + "?" + this.get('urlVariable') + "=" + newID } ) );
         else
              object.remove("url");
    }

    In this third example we're going to add a global listener to a single page object. If anything changes, we want to mark it so we can save it to the database later.

    ColdFusion:
    // Global Listener Setup Code
    <cfset page.addListener(DataAccessLayer, "isDirty", false) />
    // Global Listener Method
    <cfcomponent hint="Data Access Layer">
         ...
         <cffunction name="isDirty">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="oldValue" />
              <cfargument name="newValue" />
              <cfset Arguments.object.set("isDirty", true) />
         </cffunction>
         ...
    </cfcomponent>

    Javascript:
    // Global Listener Setup Code
    page.addListener(function (object,property,oldValue,newValue) {
         object.set("isDirty", true);
    }, false);

  • each: Iterates over all of the items in a Pi Object, or until the function returns a value. The "each" function works much like a filter, but is run on demand, instead of in response to a "get" operation. It works really well when processing all of the elements in a PiObject, or when searching for an item in a collection, replacing the need for using for-loops or iterators.

    Usage:

    ColdFusion or Javascript:
    • each (object object, string method_name)
      • object object : The first argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do.
      • string method_name: The second argument must be the name of a method within the object specified in the first argument.
    Javascript only:
    • each ([object scope,] function method)
      • object scope : [optional] The first optional argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do. If a scope is not provided, the object to which the filter is being attached is used as the scope.
      • function method : The second argument must be a reference to a function. This function can be a stand-alone function, or a method on an object. This function does not have to belong to the object specified in the first argument, but it can.

    Iteration Method Arguments:

    1. object: A reference to the Pi Object.
    2. property: The unique identifier (property name) for each item in the object.
    3. value: The item stored in the object.

    Iteration Method Result:

    • No result: Use the "each" function without returning a value to loop over all of the items in the Pi Object. For example, you can use the "each" function for collecting information, or making modifications to all of the items in a Pi Object.
    • With result: You can also use the "each" function to search a Pi Object. When you find the item that you're looking for, return a value and the looping will exit.

    Examples:

    In this first example we're going to iterate over a Pi Object and collect information from each item. The way this data is collected is implemented differently when working with ColdFusion and Javascript. In ColdFusion, the "this" keyword is tied to the first argument, so you must collect your information through that object. Javascript gives the concept of a "closure" that can be used to collect information.

    ColdFusion:
    <cfcomponent extends="MillionMunkeys.PiMunkey_1_5.PiComponent" hint="pages">
         <cfset pageTitles="" />
         <cfset this.addFilter("titles", this, "getPageTitles") />
         <cffunction name="getPageTitles">
              <cfset pageTitles = "" />
              <cfset this.each(this, "getTitles") />
              <cfreturn pageTitles />
         </cffunction>
         <cffunction name="getTitles">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="value" />
              <cfset pageTitles = ListAppend(pageTitles, Arguments.value.get("title")) />
         </cffunction>
    </cfcomponent>

    Javascript:
    // Define closure variable
    var pageTitles = [];
    // Iterator function
    pages.each( function(pages, index, page) {
         pageTitles.push( page.get("title") );
    }
    pageTitles = pageTitles.join(",");

    In this second example we're going to search a Pi Object for a particular item.

    ColdFusion:
    <cfcomponent extends="MillionMunkeys.PiMunkey_1_5.PiComponent" hint="users">
        <cffunction name="findUser">
              <cfargument name="last_name" />
              <cfset this.set("search_string", Arguments.last_name) />
              <cfreturn this.each(this, "findByLastName") />
         </cffunction>
         <cffunction name="findByLastName">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="value" />
              <cfif Arguments.value.get("last_name") eq this.get("search_string")>
                   <cfreturn Arguments.value />
              </cfif>
         </cffunction>
    </cfcomponent>

    Javascript:
    modules = new PiObject();
    // Find position based on y position
    modules.findPosition = function(modules, originalIndex, oldModule, newModule) {
         var newIndex = modules.each( function(modules, index, module) {
              if (module.get("y") > newModule.get("y")) {
                   return index;
              }
         }
         // Match index to module's position on the y-axis.
         modules.move( originalIndex, newIndex );
    }
    modules.addListener(modules.findPosition);

  • removeFilter: Removes a filter function from the Pi Object. (NOTE: If the filter is not actually listening to this Pi Object, then this function call will just be ignored.)

    Usage:

    ColdFusion:

    1. removeFilter (string property, object object, string method_name) : Use three arguments to remove a "property filter".
      • string property : The first argument is the name of a property within the Pi Object. NOTE: You may add a filter to a property even if the property doesn't exist yet. It will still be fired whenever that property is requested from the Pi Object, and regardless of whether the property actually exists or not.
      • object object : The second argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do.
      • string method_name: The third argument must be the name of a method within the object specified in the second argument.
    2. removeFilter (object object, stringmethod_name) : Use two arguments to remove a "global filter".
      • object object : The first argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do.
      • string method_name: The second argument must be the name of a method within the object specified in the first argument.

    Javascript:

    1. removeFilter (string property, [object scope,] function method) : Use three arguments to remove a "property filter".
      • string property : The first argument is the name of a property within the Pi Object. NOTE: You may add a filter to a property even if the property doesn't exist yet. It will still be fired whenever that property is requested from the Pi Object, and regardless of whether the property actually exists or not.
      • object scope : [optional] The second optional argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do. If a scope is not provided, the object to which the filter is being attached is used as the scope.
      • function method : The third argument must be a reference to a function. This function can be a stand-alone function, or a method on an object. This function does not have to belong to the object specified in the second argument, but it can.
    2. removeFilter ([object scope,] function method) : Use two arguments to remove a "global filter".
      • object scope : [optional] The first optional argument must be a valid object. It can be a Pi Object itself, but it does not need to be. Any object will do. If a scope is not provided, the object to which the filter is being attached is used as the scope.
      • function method : The second argument must be a reference to a function. This function can be a stand-alone function, or a method on an object. This function does not have to belong to the object specified in the first argument, but it can.

    Examples:

    ColdFusion:
    <cfset requestManager.removeFilter(AccessController, "setOwnerId") />
    <cfset this.removeFilter("user_status", this, "getCurrentStatus") />

    Javascript:
    requestManager.removeFilter(AccessController.setOwnerId);
    this.removeFilter("user_status", User.prototype.getCurrentStatus);

    CAUTION: To remove a filter, you must provide exactly the same arguments as when the filter was added. For example, if you add a filter with a scope but try to remove it without specifying the scope, you will be unsuccessful.

  • removeListener: Removes a listener function from the Pi Object. (NOTE: If the listener is not actually listening to this Pi Object, then this function call will just be ignored.)

    Usage:

    ColdFusion or Javascript:

    1. removeListener (string property, object object, string method_name) : Use three arguments to remove a "property listener".
      • string property : The first argument is the name of a property within the Pi Object.
      • object object : The second argument must be a valid object.
      • string method_name: The third argument must be the name of a method within the object specified in the second argument.
    2. removeListener (object object, stringmethod_name) : Use two arguments to remove a "global listener".
      • object object : The first argument must be a valid object.
      • string method_name: The second argument must be the name of a method within the object specified in the first argument.

    Javascript only:

    1. removeListener (string property, [object scope,] function method) : Use three arguments to remove a "property listener".
      • string property : The first argument is the name of a property within the Pi Object.
      • object scope : [optional] The second optional argument must be a valid object. If a scope is not provided, the object to which the filter is being attached is used as the default scope.
      • function method : The second or third argument must be a reference to a function.
    2. removeListener ([object scope,] function method) : Use two arguments to remove a "global listener".
      • object scope : [optional] The first optional argument must be a valid object. If a scope is not provided, the object to which the filter is being attached is used as the scope.
      • function method : The first or second argument must be a reference to a function.

    Examples:

    These first two calls will clean up the listeners added during the examples in the addListener section of this documentation.

    ColdFusion:
    <cfset pages.removeListener(TrafficMunkey, "addURLVariable") />
    <cfset page.removeListener("url", TrafficMunkey, "addURLVariable") />

    Javascript:
    pages.removeListener(TrafficMunkey, TrafficMunkey.addURLVariable);
    page.removeListener("url", TrafficMunkey, TrafficMunkey.addURLVariable);

    This next example assumes that IDs will not change, and so once it's done it's job it will remove itself. In other words, this method will only ever be executed once per object.

    ColdFusion:
    // Property Listener Setup Code:
    <cfset page.addListener("pageId", TrafficMunkey, "addURLVariable", true) />
    ...
    // Property Listener Method:
    <cfcomponent>
         ...
         <cffunction name="addURLVariable">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="oldValue" />
              <cfargument name="newValue" />
              <cfif Arguments.newValue neq "">
                   <cfset Arguments.object.set("url", "#this.get('baseURL')#?#this.get('urlVariable')#=#Arguments.newValue#") />
                   <cfset Arguments.object.removeListener("pageId", this, "addURLVariable") />
              </cfif>
         </cffunction>
         ...
    </cfcomponent>

    Javascript:
    page.addListener("pageId", TrafficMunkey, TrafficMunkey.addURLVariable);
    ...
    TrafficMunkey.prototype.addURLVariable = fuction(object,property,oldValue,newValue) {
         object.set("url", this.get('baseURL') + "?" + this.get('urlVariable') + "=" + newValue );
         object.removeListener("pageId", this, this.addURLVariable);
    }

 

Follow Pete on Twitter

Copyright ©2010, MillionMunkeys® LLC, All Rights Reserved