Reply to comment

WP Media UI: States, StateMachine, and Frame

(It seems like I got the AJAX server working, at least somewhat. So I needed to implement the editors. This is really difficult because it's all in the hierarchy, and with keywords like "wp" and "media", it's hard to find the documentation. I have been digging around in the code trying to understand it, and came up with notes, which I'll post occasionally. These are mainly for myself, to review when I need to restart the exploration, but they're posted because someone else might find this useful.)

These are incomplete, and probably incorrect notes.

WP Media UI: States, StateMachine, and Frame

"A frame is a composite view consisting of one or more regions and one or more states."

A frame is a composite view, meaning that it is a view that contains other views.

A frame has "state", which is a way to orchestrate what views are visible. The State and StateMachine fire Events when the current state changes, or any attributes on the State change.

A Frame has the StateMachine class mixed in, so it has a collection of States.

State is a model that stores attributes about the current state. These attributes are used by the subclasses of Frame to determine which regions are visible in a specific state, and to store values in a view.

For example, MediaFrame is a subclass of Frame, and it manages the media library. It comprises five regions. The Select workflow extends MediaFrame and adds handlers to respond to events thrown by these regions.

bindHandlers: function() {
    this.on( 'router:create:browse', this.createRouter, this );
    this.on( 'router:render:browse', this.browseRouter, this ); 
    this.on( 'content:create:browse', this.browseContent, this );
    this.on( 'content:render:upload', this.uploadContent, this );
    this.on( 'toolbar:create:select', this.createSelectToolbar, this );

The "content:create:browse" events means, the content region was created, and the browse mode was activated. The pattern is "region:event:mode". Mode seems to be a hack - a region has a mode, and that determines which view is displayed in the region. A region doesn't need to have a mode.

By the way, Regions fire off two events: create and render. First, if fires create, and a handler in Select creates the view.

But look at the 'content:render:browse' and 'content:render:upload' events: where does the "browse" or "upload" mode come from?

The mode is set in the Library controller. Library extends State (which makes sense, because controllers hold state). The Library controller manages the state in the 'content' region. The Library controller is created by the Select view, and then added to Select's states -- remember, that Select is a Frame, and Frames have the StateMachine controller mixed in.

The Select's initialize() is:

initialize: function() {
    // Call 'initialize' directly on the parent class.
    MediaFrame.prototype.initialize.apply( this, arguments );

    _.defaults( this.options, {
        selection: [],
        library:   {},
        multiple:  false,
        state:    'library'

    this.createStates();  // look here

When createStates() is called, it creates the Library controller:

createStates: function() {
    var options = this.options;

    if ( this.options.states ) {

    // Add the default states.
        // Main states.
            library: options.library ),
            multiple:  options.multiple,
            title:     options.title,
            priority:  20

So when the Select view is initialized, the Library controller is also created and a reference to it is saved in the view's StateMachine. Later, when browseContent() is called, it retreives state information from the Library controller instance, and that renders the AttachmentBrowser view:

browseContent: function( contentRegion ) {
    var state = this.state();


    // Browse our library of attachments.
    contentRegion.view = new{
        controller: this,
        collection: state.get('library'),
        selection:  state.get('selection'),
        model:      state,
        sortable:   state.get('sortable'),
        search:     state.get('searchable'),
        filters:    state.get('filterable'),
        date:       state.get('date'),
        display:    state.has('display') ? state.get('display') : state.get('displaySettings'),
        dragInfo:   state.get('dragInfo'),

        idealColumnWidth: state.get('idealColumnWidth'),
        suggestedWidth:   state.get('suggestedWidth'),
        suggestedHeight:  state.get('suggestedHeight'),

        AttachmentView: state.get('AttachmentView')

(This feels like coupling to me.)

When the mode is "upload", Select displays UploaderInline:

uploadContent: function() {
    this.$el.removeClass( 'hide-toolbar' );
    this.content.set( new{
        controller: this
    }) );

So the Select frame and Library controller work in tandem to display two different views: AttachemsntBrwoser and UploaderInline.

In addition the view juggling above, Select also has a selection.

Select's selection

A Selection is subclass of Attachements. Attachments is a collection of "Attachment"s. An Attachment is a Model. So, Select has a collection to hold the selection. Here's the code to create the selection in Select:

createSelection: function() {
    var selection = this.options.selection;

    if ( ! (selection instanceof ) {
        this.options.selection = new selection, {
            multiple: this.options.multiple

    this._selection = {
        attachments: new,
        difference: []

Note that it's stored in this.options. That was weird, so I did a grep on "this.options.selection" and found it used in some views, like the Attachment view. The Attachment view seems to manage the manipulation of selection lists.

Library's selection

In the "browse" mode in Select, an AttachmentsBrowser is created and it has a property 'selection':

        selection:  state.get('selection'),

Remember that state is the Library controller. Up in the Library controller, the code to create the selection is:

initialize: function() {
    var selection = this.get('selection'),

    if ( ! this.get('library') ) {
        this.set( 'library', );

    if ( ! ( selection instanceof ) ) {
        props = selection;

        if ( ! props ) {
            props = this.get('library').props.toJSON();
            props = _.omit( props, 'orderby', 'query' );

        this.set( 'selection', new null, {
            multiple: this.get('multiple'),
            props: props
        }) );


So it's similar code, but the selection is kept in the Library controller's state.

It looks like these two instances of Selection are separate. The Select's selection is a global value, and the Library controller's selection is local to the library. This feels weird, but I'll assume there's a good reason. (Or a bad reason.)

BTW, this.options?

this.options.selection is defined up in the initialize(). It's a little hard to spot because it uses the _.defaults() function:

    _.defaults( this.options, {
        selection: [],
        library:   {},
        multiple:  false,
        state:    'library'

But where is this.options set? It might be in Backbone.History. That's the only place I saw "this.options =". So this is still a mystery to me. The only thing I see is that this.options is like a global between different views.

Additionally, the only place I found Backbone.history.start was in views/frame/manage.js.


The content of this field is kept private and will not be shown publicly.
  • Lines and paragraphs break automatically.

More information about formatting options

1 + 0 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.