0

I've some ExtJS 4 MVC apps that share certain custom view components. A shared component has its own namespace and is often sufficiently complex (e.g. with a large tree of descendant components that must be managed) that it warrants its own controller. I want this 'component specific controller' to be properly encapsulated (private) within the component (i.e. nothing outside the component should have to know or care that the component is using an embedded controller). Of course, it should be possible for an app to have multiple instances of the component (each encapsulating a separate instance of its component controller).

To this end, I've developed the following mixin (this version for ExtJS 4.1), but I'm keen to know if anyone has solved the same problem more elegantly:

/**
 * A mixin that provides 'controller' style facilities to view components.
 * 
 * This is for 'view' components that also serve as the 'controller' for the component. 
 * In such situations where an embedded controller is required we would have preferred to use a separate 
 * Ext.app.Controller derived controller but it looks like Ext.app.Controllers are global things,
 * i.e. their refs can't be scoped to within the component (allowing multiple instances on the component to be used,
 * each with their own controller each with its own set of refs).
 * 
 * Usage:
 *   - Declare a 'refs' config just as in an Ext.app.Controller.
 *   - Call this.setupControllerRefs() from your initComponent() template function.
 *   - Call this.control(String/Object selectors, Object listeners) just as you would in a Ext.app.Controller.init()
 *     template function.
 *   - Any events fired from within a Window need special treatment, because Ext creates Windows as floated 
 *     (top-level) components, so our usual selector scoping technique doesn't work. The trick is to give each Window
 *     an itemId prefixed with this component's itemId, e.g. itemId: me.itemId + '-lookup-window'
 *     Then, in the 'this.control({...})' block, define selectors as necessary that begin with "#{thisItemId}-", e.g. 
 *     '#{thisItemId}-lookup-window aux-filter-criteria': ...
 *   
 * It is also recommended to keep the 'view' aspect of the component minimal. If there is a significant proportion of
 * view code, push it down into a new component class. Ideally, the component/controller should be just a Container.
 */
Ext.define('Acme.CmpController', {

  setupControllerRefs: function() {
    var me = this,
      refs = me.refs;

    // Copied from Ext.app.Controller.ref
    refs = Ext.Array.from(refs);
    Ext.Array.each(refs, function(info) {
        var ref = info.ref,
            fn = 'get' + Ext.String.capitalize(ref);
        if (!me[fn]) {
            me[fn] = Ext.Function.pass(me.getRef, [ref, info], me);
        }
    });
  },
  /** @private  (copied from Ext.app.Controller.ref) */
  getRef: function(ref, info, config) {
      this.refCache = this.refCache || {};
      info = info || {};
      config = config || {};

      Ext.apply(info, config);

      if (info.forceCreate) {
          return Ext.ComponentManager.create(info, 'component');
      }

      var me = this,
          selector = info.selector,
          cached = me.refCache[ref];

      if (!cached) {
          //me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0];
          /**** ACME ****/ me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector, this)[0];
          if (!cached && info.autoCreate) {
              me.refCache[ref] = cached = Ext.ComponentManager.create(info, 'component');
          }
          if (cached) {
              cached.on('beforedestroy', function() {
                  me.refCache[ref] = null;
              });
          }
      }

      return cached;
  },

  control: function(selectors, listeners) {
    var me = this,
      selectorPrefix,
      thisIemIdPrefix = '#{thisItemId}',
      newSelectors = {};

    if (listeners)
      throw "Support for the optional 'listeners' param (which we had thought was rarely used) has not yet been coded.";

    // Since there could be multiple instances of the controller/component, each selector needs to be 
    // prefixed with something that scopes the query to within this component. Ensure each instance has 
    // an itemId, and use this as the basis for scoped selectors.
    me.itemId = me.itemId || me.id;
    if (!me.itemId)
      throw "We assume the component will always have an 'id' by the time control() is called.";
    selectorPrefix = '#' + me.itemId + ' ';
    Ext.Object.each(selectors, function(selector, listeners) {
      if (selector.indexOf(thisIemIdPrefix) === 0)
        selector = '#' + me.itemId + selector.substring(thisIemIdPrefix.length);
      else
        selector = selectorPrefix + selector;
      newSelectors[selector] = listeners;
    });
    selectors = newSelectors;

    // Real Controllers use the EventBus, so let's do likewise.
    // Note: this depends on a hacked EventBus ctor. See ext-fixes.js
    Ext.app.EventBus.theInstance.control(selectors, listeners, me);
  }
});

As referred to in that last comment, ExtJS must be patched as follows:

Ext.override(Ext.app.EventBus, {
    /**
     * Our CmpController mixin needs to get a handle on the EventBus, as created by the Ext.app.Application instance. Analysis
     * of the ExtJS source code shows that only one instance of EventBus gets created (assuming there's never more than one
     * Ext.app.Application per app). So we hack the ctor to store a reference to itself as a static 'theInstance' property.
     */
    constructor: function() {
        this.callOverridden();
        /**** ACME ****/ this.self.theInstance = this;
    },

    /**
     * Had to patch this routine on the line labelled **** ACME ****. Events intercepted by Pv were being received by the Pv 
     * instance first created by appPv. appPv created a new Pv instance every time a 'to.AcmeViewer.View' message is received.
     * Even though the old Pv had isDestroyed:true, the routine below was dispatching the event to it.
     * 
     * It's possible this surprising behaviour is not unconnected with our (mis?)use of EventBus in Acme.CmpController.
     * 
     * This patched function is from ExtJS 4.1.1
     */
    dispatch: function(ev, target, args) {
        var bus = this.bus,
            selectors = bus[ev],
            selector, controllers, id, events, event, i, ln;

        if (selectors) {
            // Loop over all the selectors that are bound to this event
            for (selector in selectors) {
                // Check if the target matches the selector
                if (selectors.hasOwnProperty(selector) && target.is(selector)) {
                    // Loop over all the controllers that are bound to this selector
                    controllers = selectors[selector];
                    for (id in controllers) {
                        if (controllers.hasOwnProperty(id)) {
                            // Loop over all the events that are bound to this selector on this controller
                            events = controllers[id];
                            for (i = 0, ln = events.length; i < ln; i++) {
                                event = events[i];
                                /**** ACME ****/ if (!event.observable.isDestroyed)
                                // Fire the event!
                                if (event.fire.apply(event, Array.prototype.slice.call(args, 1)) === false) {
                                    return false;
                                }
                            }
                        }
                    }
                }
            }
        }
        return true;
    }
});

Although I'm currently on ExtJS 4.1 I'm also interested to hear about solutions that depend on 4.2, as this may help motivate me to migrate.

David Easley
  • 1,347
  • 1
  • 16
  • 24
  • 1
    While this doesn't really address your question for 4.x, View Controllers for components have been implemented in the 5.x beta if you'd like to try that out. – Evan Trimboli May 01 '14 at 19:03
  • That's good to hear, Evan, but my product owner won't sanction an upgrade to version 5 for some while yet (must be well proven). But thanks to your comment I can now phrase the question differently: How can I approximate ExtJS 5 ViewController in ExtJS 4? – David Easley May 02 '14 at 10:01
  • By upgrading.... There's really no equivalent currently. – Evan Trimboli May 02 '14 at 13:23

0 Answers0