Problem

An Ext.PagingToolbar allows paging over a large amount of data in chunks of predefined size. Adding it to a grid is pretty straightforward:


  bbar: new Ext.PagingToolbar({
    pageSize: 10,
    store: store,
    displayInfo: true
  })

and it works perfectly with fixed size grids, however, when grid is placed in a resizable window, a possibility of changing pageSize parameters becomes desired.

PageSliderResizer

PageSliderResizer is a plugin which uses a slider to get desired pageSize value.

Ext.namespace('Ext.ux.plugins');

Ext.ux.plugins.PageSliderResizer = Ext.extend(Object, {

  pageSizes: [5, 10, 15, 20, 25, 30, 50, 75, 100, 200, 300, 500],
  tipText: 'Showing <b>{0}</b> records per page.',

  constructor: function(config){
    Ext.apply(this, config);
    Ext.ux.plugins.PageSliderResizer.superclass.constructor.call(this, config);
  },

  init : function(pagingToolbar){

	var ps = this.pageSizes;
    var sv = 0;
    Ext.each(this.pageSizes, function(ps, i) {
      if (ps==pagingToolbar.pageSize) {
        sv = i;
        return;
      }
    });

    var tt = this.tipText;
    var slider = new Ext.Slider({
      width: 115,
      value: sv,
      minValue: 0,
      maxValue: ps.length-1,
      plugins: new Ext.ux.SliderTip({
        getText : function(slider){return String.format(tt, ps[slider.getValue()]);}
      }),
      listeners: {
        changecomplete: function(s, v){
          pagingToolbar.pageSize = ps[v];
          var rowIndex = 0;
          var gp = pagingToolbar.findParentBy (
            function (ct, cmp) {return (ct instanceof Ext.grid.GridPanel) ? true : false;}
          );
          var sm = gp.getSelectionModel();
          if (undefined != sm && sm.hasSelection()) {
            if (sm instanceof Ext.grid.RowSelectionModel) {
              rowIndex = gp.store.indexOf( sm.getSelected() ) ;
            } else if (sm instanceof Ext.grid.CellSelectionModel) {
              rowIndex = sm.getSelectedCell()[0] ;
            }
          }
          rowIndex += pagingToolbar.cursor;
          pagingToolbar.doLoad(Math.floor(rowIndex/pagingToolbar.pageSize)*pagingToolbar.pageSize);
        }
      }
    });

    var inputIndex = pagingToolbar.items.indexOf(pagingToolbar.refresh);
    pagingToolbar.insert(++inputIndex,'-');
    pagingToolbar.insert(++inputIndex, slider);
    pagingToolbar.on({
      beforedestroy: function() {
        slider.destroy();
      }
    });

  }
});

When plugged in :

Grid with a PageSliderResizer

PageComboResizer

PageComboResizer is a plugin which uses a comboBox to get desired pageSize value.

Ext.namespace('Ext.ux.plugins');

Ext.ux.plugins.PageComboResizer = Ext.extend(Object, {

  pageSizes: [5, 10, 15, 20, 25, 30, 50, 75, 100, 200, 300, 500],
  prefixText: 'Showing ',
  postfixText: 'records per page.',

  constructor: function(config){
    Ext.apply(this, config);
    Ext.ux.plugins.PageComboResizer.superclass.constructor.call(this, config);
  },

  init : function(pagingToolbar) {
    var ps = this.pageSizes;
    var combo = new Ext.form.ComboBox({
      typeAhead: true,
      triggerAction: 'all',
      lazyRender:true,
      mode: 'local',
      width:45,
      store: ps,
      listeners: {
        select: function(c, r, i){
          pagingToolbar.pageSize = ps[i];
          var rowIndex = 0;
          var gp = pagingToolbar.findParentBy (
            function (ct, cmp) {return (ct instanceof Ext.grid.GridPanel) ? true : false;}
          );
          var sm = gp.getSelectionModel();
          if (undefined != sm && sm.hasSelection()) {
            if (sm instanceof Ext.grid.RowSelectionModel) {
              rowIndex = gp.store.indexOf( sm.getSelected() ) ;
            } else if (sm instanceof Ext.grid.CellSelectionModel) {
              rowIndex = sm.getSelectedCell()[0] ;
            }
          }
          rowIndex += pagingToolbar.cursor;
          pagingToolbar.doLoad(Math.floor(rowIndex/pagingToolbar.pageSize)*pagingToolbar.pageSize);
        }
      }
    });

    Ext.iterate(this.pageSizes, function(ps) {
      if (ps==pagingToolbar.pageSize) {
        combo.setValue (ps);
        return;
      }
    });

    var inputIndex = pagingToolbar.items.indexOf(pagingToolbar.refresh);
    pagingToolbar.insert(++inputIndex,'-');
    pagingToolbar.insert(++inputIndex, this.prefixText);
    pagingToolbar.insert(++inputIndex, combo);
    pagingToolbar.insert(++inputIndex, this.postfixText);
    pagingToolbar.on({
      beforedestroy: function(){
        combo.destroy();
      }
    });

  }
});

When plugged in :

PageResizer with comboBox

PageCycleResizer

PageCycleResizeris a plugin which uses a cycleButton to get desired pageSize value.

Ext.namespace('Ext.ux.plugins');

Ext.ux.plugins.PageCycleResizer = Ext.extend(Object, {

  pageSizes: [5, 10, 15, 20, 25, 30, 50, 75, 100, 200, 300, 500],
  prependText: 'Showing',
  appendText: ' records per page',
  iconCls: 'icon-app-add-delete',

  constructor: function(config){
    Ext.apply(this, config);
    Ext.ux.plugins.PageCycleResizer.superclass.constructor.call(this, config);
  },

  init : function(pagingToolbar){

    var at = this.appendText; var ic = this.iconCls;
    var items=[];
    Ext.iterate(this.pageSizes, function(ps) {
      items.push({
        text: " " + ps + at,
        value: ps,
        iconCls:ic,
        checked: pagingToolbar.pageSize==ps ? true : false
      });
    });

    var pt = this.prependText;
    var button = new Ext.CycleButton({
      showText: true,
      prependText: pt,
      items: items,
      listeners: {
        change: function(button, item) {
          pagingToolbar.pageSize = item.value;
          var rowIndex = 0;
          var gp = pagingToolbar.findParentBy (
            function (ct, cmp) {return (ct instanceof Ext.grid.GridPanel) ? true : false;}
          );
          var sm = gp.getSelectionModel();
          if (undefined != sm && sm.hasSelection()) {
            if (sm instanceof Ext.grid.RowSelectionModel) {
              rowIndex = gp.store.indexOf( sm.getSelected() ) ;
            } else if (sm instanceof Ext.grid.CellSelectionModel) {
              rowIndex = sm.getSelectedCell()[0] ;
            }
          }
          rowIndex += pagingToolbar.cursor;
          pagingToolbar.doLoad(Math.floor(rowIndex/pagingToolbar.pageSize)*pagingToolbar.pageSize);
        }
      }
    });

    var inputIndex = pagingToolbar.items.indexOf(pagingToolbar.refresh);
    pagingToolbar.insert(++inputIndex,'-');
    pagingToolbar.insert(++inputIndex, button);
    pagingToolbar.on({
      beforedestroy: function() {
        button.destroy();
      }
    });
  }
});

When plugged in :

PageResizer with a CycleButton

all PageResizers are PagingToolbar plugins, so in order to use it :

  bbar: new Ext.PagingToolbar({
    pageSize: 15,
    store: store,
    displayInfo: true,
    plugins: [
      new Ext.ux.plugins.PageCycleResizer({pageSizes: [5, 10, 15, 20, 30, 50]})
    ]
  })

There is one silent assumption made – initial pageSize parameter is one of the pageSizes elements.
After page size changes a page containing first selected row or cell is displayed. If there is no selection made, a page containing a current top record is displayed.

A zip file with html/js used to create samples above.

December 25th, 2009Ext JS Integration with Spring

Problem

Create Ext JS grid able to read data in XML format. Provide XML data from Spring controller. Bind it all together.

To demonstrate an integration between Ext JS and Spring frameworks I am going to use one of Ext JS grid samples – Progress Bar Pager. This grid displays Dow Jones components using Ext.data.ArrayReader which is one of the three, built in Ext.data.DataReader implementation. Since data will be served from a server, an ArrayReader has to be replaced with a XMLReader. Ext JS provides a helper class, XMLStore, which is automatically configured with an XMLReader, so I am going to use it.

DJIA Grid

Ext.onReady(function() {

  // example of custom renderer function
  function change(val) {
    if (val > 0) {
      return '<span style="color:green;">' + val + '</span>';
    } else if (val < 0) {
      return '<span style="color:red;">' + val + '</span>';
    }
      return val;
  }

  // example of custom renderer function
  function pctChange(val) {
    if (val > 0) {
      return '<span style="color:green;">' + val + '%</span>';
    } else if (val < 0) {
      return '<span style="color:red;">' + val + '%</span>';
    }
    return val;
  }

  // create the data store
  var xmlStockStore = new Ext.data.XmlStore({
    autoDestroy : true,
    url : '/zone/rest/djia',
    record : 'component',
    idPath : 'symbol',
    totalProperty : 'totalRecords',
    fields : [
      {name : 'name'},
      {name : 'price', type : 'float'},
      {name : 'valChange', type : 'float'},
      {name : 'pctChange', type : 'float'},
      {name : 'lastUpdate', type : 'date', dateFormat : 'Y-m-d H:i:s.u T'}
    ]
  });

  // create the Grid
  var grid = new Ext.grid.GridPanel({
    store : xmlStockStore,
    columns : [
      {id : 'company', header : "Company", width : 160, sortable : true, dataIndex : 'name'},
      {header : "Price", width : 75, sortable : true, renderer : 'usMoney', dataIndex : 'price'},
      {header : "Change", width : 75, sortable : true, renderer : change, dataIndex : 'valChange'},
      {header : "% Change", width : 75, sortable : true, renderer : pctChange, dataIndex : 'pctChange'},
      {header : "Last Updated", width : 95, sortable : true, renderer : Ext.util.Format.dateRenderer('d-m-Y H:i'), dataIndex : 'lastUpdate'}
    ],
    stripeRows : true,
    autoExpandColumn : 'company',
    height : 320,
    width : 545,
    frame : true,
    title : 'DJIA',

    bbar : new Ext.PagingToolbar({
      pageSize : 10,
      store : xmlStockStore,
      displayInfo : true,
      displayMsg : 'Displaying item {0} - {1} of {2}',
      emptyMsg : "No item to display",

      plugins: new Ext.ux.ProgressBarPager()
    })
  });

  grid.render('grid-example');

  xmlStockStore.load({params : {start : 0, limit : 10}});
});

The grid above is configured to consume an xml of the form:

<?xml version="1.0" encoding="UTF-8"?>
<djia>
  <totalRecords>30</totalRecords>
  <components>
    <component>
      <symbol>MMM</symbol>
      <name>3M Co </name>
      <price>60.46</price>
      <valChange>0.12</valChange>
      <pctChange>0.2</pctChange>
      <lastUpdate>2009-12-24 15:00:00.0 CET</lastUpdate>
    </component>
  </components>
</djia>

So the last thing to do is to configure Spring to deliver an XML in a required form.

Thanks to Spring 3.0 REST support and ContentNegotiatingViewResolver providing an XML content is an extremally easy task. First, Spring configuration:

web.xml

	<servlet>
		<servlet-name>SpringDispatcher</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value/>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>SpringDispatcher</servlet-name>
		<url-pattern>/rest/*</url-pattern>
	</servlet-mapping>

applicationContext.xml

  <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver" p:order="1">
    <property name="mediaTypes">
      <map>
        <entry key="xml" value="application/xml"/>
      </map>
    </property>
    <property name="defaultViews">
      <bean class="org.springframework.web.servlet.view.xml.MarshallingView">
        <property name="marshaller">
          <bean class="org.springframework.oxm.xstream.XStreamMarshaller"
            p:autodetectAnnotations="true"
          />
        </property>
      </bean>
    </property>
  </bean>

As seen above, I am going to use an XStream library to serialize objects to XML. XStream is easy configurable with annotations, so the single DJIA component:

Component

import java.util.Date;

import com.thoughtworks.xstream.annotations.XStreamAlias;

@XStreamAlias("component")
public class Component {

  private String symbol;
  private String name;
  private Double price;
  @XStreamAlias("valChange")
  private Double valueChange;
  @XStreamAlias("pctChange")
  private Double percentChange;
  private Date lastUpdate;

  public Component () {
    super ();
  }

  public Component(String symbol, String name, Double price, Double valueChange, Double percentChange, Date lastUpdate) {
    super();
    this.symbol = symbol;
    this.name = name;
    this.price = price;
    this.valueChange = valueChange;
    this.percentChange = percentChange;
    this.lastUpdate = lastUpdate;
  }

  public String getSymbol() {
    return symbol;
  }
  public String getName() {
    return name;
  }
  public Double getPrice() {
    return price;
  }
  public Double getValueChange() {
    return valueChange;
  }
  public Double getPercentChange() {
    return percentChange;
  }
  public Date getLastUpdate() {
    return lastUpdate;
  }

  public void setSymbol(String symbol) {
    this.symbol = symbol;
  }
  public void setName(String name) {
    this.name = name;
  }
  public void setPrice(Double price) {
    this.price = price;
  }
  public void setValueChange(Double valueChange) {
    this.valueChange = valueChange;
  }
  public void setPercentChange(Double percentChange) {
    this.percentChange = percentChange;
  }
  public void setLastUpdate(Date lastUpdate) {
    this.lastUpdate = lastUpdate;
  }
}

and all of the components:

IndexDTO

import java.util.LinkedList;

import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.zone.domain.model.Component;

@XStreamAlias("djia")
public class IndexDTO {

  private int totalRecords;
  private LinkedList <Component> components;

  public void setTotalRecords(int totalRecords) {
    this.totalRecords = totalRecords;
  }
  public void setComponents(LinkedList <Component> components) {
    this.components = components;
  }

  public int getTotalRecords() {
    return totalRecords;
  }
  public LinkedList <Component> getComponents() {
    return components;
  }

}

IndexDTO is a helper class, which will be eventually marshalled and sent to browser, totalRecords variable is optional, but grid PagingToolbar will not work without it.

Finally, the controller:

IndexController

import java.util.LinkedList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.zone.domain.model.Component;
import com.zone.infrastructure.repository.DJIARepository;
import com.zone.web.grid.IndexDTO;

@Controller
public class IndexController {

  @Autowired
  DJIARepository djiaRepository;

  @RequestMapping(value = "/djia")
  public void getComponents(@RequestParam("start") int start, @RequestParam("limit") int limit, Model model) {

    List <Component> components= (List<Component>) djiaRepository.findAll();
    IndexDTO indexDTO = new IndexDTO ();

    indexDTO.setTotalRecords(components.size());
    indexDTO.setComponents(new LinkedList<Component> (components.subList(start, start+limit > components.size() ? components.size() : start+limit)));

    model.addAttribute("indexDTO", indexDTO);
  }

}

DJIARepository is a class holding all DJIA components.

Conclusion

It is actuall all, what is necessary, to have an Ext JS grid populated with data coming from Java based back end. Of course, all hard work is done by Spring and Xstream with some configuration and programming left to the application developer.


© 2007 Toss a coin | iKon Wordpress Theme by Windows Vista Administration | Powered by Wordpress