• If you are citizen of an European Union member nation, you may not use this service unless you are at least 16 years old.

  • You already know Dokkio is an AI-powered assistant to organize & manage your digital files & messages. Very soon, Dokkio will support Outlook as well as One Drive. Check it out today!



Page history last edited by Jörn Zaefferer 12 years, 5 months ago

type: utility

release: 0.0

status: in development

documentation: N/A

demo: N/A, though used by all grid editing demos     



  • Add methods to access relevant array properties? Would make it possible (in theorey) to provide implementations of $.observable that wrap other objects, like a Backbone.Collection or SproutCore.ArrayController
    • needs actual testing to figure out what accessors need to be supported 



1 - Description:


When objects or collections change in one of various ways, events have to be triggered to inform any observers of the change.  Observers in this context could be:

  • one or more UI controls displaying the data
  • custom application logic 
  • stores persisting the data (e.g. back to the server)
  • etc.


$.observable provides the API to trigger those events and implements the necessary operations for plain objects and arrays.



2 - Visual Design:






3 - Functional Specification:


To create an observable instance, pass an object or array to $.observable:


$.observable( object ).property( "name", "Fred" );
$.observable( array ).insert({
  name: "Peter"

There are (currently) no options, just methods and events.


Methods for observable objects:

  • property(String key)
    • Retrieve the property for that key
    • Supports dot-notation 
  • property(String key, Object value)
    • Set a property to a new value.
    • Supports dot-notation  
  • property(Object keyValues
    • key-value pairs to set, keys can use dot-notation 
  • removeProperty(String key)
    • delete a property 


Methods for observable arrays:

  • insert(index, object) 
    • object can be a single object or an array 
  • insert(object) 
    • same as insert(index, object), where the index is at the end of the array 
  • remove(index)
    • Remove the object at the given index. 
  • remove(index, howMany)
    • Remove the number of object at the given index.
  • remove(object)
    • Remove the given object or array of objects.
  • replaceAll(newItems)
    • Replace the contents of the array with the give items. 


Events for observable objects:

  • change(event, { newValues, oldValues }) 
    • Triggered after one or more properties changed.
    • Parameters:
      • newValues: Current, changed value(s), with path as key and value as value. If a property is removed, its not present here.
      • oldValues: Value(s) before the change, same format as newValues. If a property is newly added, its not present here.
    • Examples
      • myCustomer.firstName = "Fred";  // Simple field change
        $(myCustomer).triggerHandler("change", { oldValues: { firstName: "Bob" }, newValues: {firstName: "Fred" }}); 
        myCustomer.address.zipCode = "98052";  // Nested field change
        $(myCustomer).triggerHandler("change", { oldValues: { "address.zipCode": "98002" }, newValues: { "adress.zipCode": "98052" }});


Events for observable arrays:

  • insert(event, { index, items })
    • Triggered after one or more items have been inserted at a specific position.
    • Parameters:
      • index:  The 0-based position where the new item(s) are located.
      • items:  An array of one or more items that have been inserted.
    • Examples:
      • // Insert at a specific position
        myCustomers.splice(positionToInsert, 0, customerToInsert);
        $(myCustomers).triggerHandler("insert", { index: positionToInsert, items: [ customerToInsert ] });
      • // Push onto the end of a collection
        $(myCustomers).triggerHandler("insert", { index: myCustomers.length - 1, items: [ customerToInsert ] }); 
  • remove(event, { items: [ { index, item }, ... ] })
    • Triggered after one or more items have been removed from a specific position.
    • Parameters: 
      • items: An array of one or more items, each with an index and item property.
        • index: The 0-based position of the item before the removal
        • item: The removed item
    • Examples:
      • // Remove from a specific position
        var customersToRemove = myCustomers.slice(positionToRemove, positionToRemove + 1);
        myCustomers.splice(positionToRemove, 1);
        $(myCustomers).triggerHandler("remove", { items: [ index: positionToRemove, item: customersToRemove ] });
      • // Remove from end of a collection
        var customersToRemove = [ myCustomers.pop() ];
        $(myCustomers).triggerHandler("remove", { items: [ index: myCustomers.length, customersToRemove ] }); 
  • replaceAll(event, { oldItems, newItems })
    • Triggered when replacing the content of the array with different content.
    • Parameters:
      • oldItems: Content of the array before the event
      • newItems: Content of the array after the event 
    • Examples:
      • var event = { oldItems: myCustomers.slice(0), newItems: newItems };
        Array.prototype.splice.apply( myCustomers, [ 0, myCustomers.length ].concat( newItems ) );
        $(myCustomers).triggerHandler("replaceAll", event);   


Not supported

  • Events aren't cancelable. This is just about observing changes, not preventing those changes.
  • Batch updates: Events are triggered for each change. Its up to the listener to take care of effective strategies for rendering in batches. Or make sure the client batches updates on the API level, changing properties or adding/removing items in one batch. Recommended strategy: debouncing.
  • A change event on arrays for containing objects. Bind to change events on each object.




4 - Markup & Style:





5 - Latest version of plugin:





6 - Open issues being discussed


  • Do we need a flag (on object or passed to each method) to supress events? Needs a usecase.
    • Brad to prototype begin()/end() methods that would trigger separate events that a client can listen to, then suspend storing or rerendering within, and do that once the end-method is called. 
      • "Bracketing" proposal (discussed with Richard and Corey; under consideration by Jörn):

        • New $.observable.makeEdits API (on $.observable singleton)
          • $.observable.makeEdits(function() {
                 $.observable(someItem).property("someProperty", "foo");
          • Optionally used by app/UI code to make multiple observable edits. 
            • Scenarios:
              • Multi-select rows in a grid, then delete
              • Multi-select "expense report" rows in a grid, then set "Approved" check-box for all
              • Helper methods like (once proposed) $.observable(myArray).replaceAll(newItems) can be achieved by:
                • $.observable.makeEdits(function() {
                       $.observable(myArray).remove(0, myArray.length);
            • Data stores can use a single $.ajax/POST for all edits bracketed with a $.observable.makeEdits call 
        • Corresponding new editing event
          • $( $.observable ).triggerHandler("editBracket", { begin: true });
          • $( $.observable ).triggerHandler("editBracket", { begin: false });
          • Raised by:
            • (Primary scenario) $.observable.makeEdits(editFunction) before/after editFunction is called
            • Data stores when they make multiple model changes to reflect, for instance, a completed $.ajax request 
          • Advanced UI controls can (optionally) bind to "editBracket" to do optimal rerendering:
            • On begin:true, the UI control will begin taking note of edited objects (inserted/removed/updated) by listening to array and object change events they care about
            • On begin:false, the UI control will rerender only those rows/list-items that have changed (no multiply-rerendered rows/list-items)
        • Notes
          • $.observable.makeEdits API and "editBracket" event are on $.observable singleton.  Why?
            • Avoids introducing another principal/concept into the Data Model
              • For instance, JSViews is only aware of $.observable (and not $.dataSource, not $.eventBracketerThingy)
            • The (managable) downside is that listeners on "editBracket" might find no relevant edits in a given bracket
              • ...and triggered events will go to listeners who ultimately do nothing useful 
          • Should we allow nested $.observable.makeEdits?
            • Could be.  Listeners on "editBracket" will likely ref-count begins/ends and do their work on the outermost "end". 
  • Change 'remove' event to trigger before mutation? Yehuda and Tom suggested we have the remove event occur before its mutation, otherwise you have no opportunity to do cleanup on those elements. For example, if you have a dropdown with 18 items and you get a remove event for index:3 and howMany: 3. If the mutation occurs and then the event, you've lost your link between the item in the array and the same-index item in the dropdown. In discussion with Scott, Jörn, and Richard, no use case was found where both beforeRemove and afterRemove are needed, beforeRemove seems sufficient. It also doesn't make sense that it be cancelable, as there's no use case for that, so one suggestion is keep the name 'remove' but simply change its semantics, having it trigger before the mutation.
  • Add indirection method for array member getting. Another suggestion Yehuda and Tom made is that we add a method for getting array members by index, something like .get(index) or .getAt(index). The default implementation would be return arr[index] but by having a method that should always be used, a non-array object could actually be used underneath. This is already accounted for at the object level, since .property is a getter and setter. 






Comments (0)

You don't have permission to comment on this page.