My Heart Bleeds for You!

April 10, 2014 Leave a comment

Fig Leaf Software’s CTO, Dave Watts, wrote up a great blog post about the Heart Bleed vulnerability.

Check it out here.

Categories: Security

Generate an Excel File from a Tree Panel / Tree Grid!

April 4, 2014 3 comments

Recently I was tasked with building an application whereby the user could export report data contained within a  treegrid to Microsoft Excel. Under normal circumstances, I would have used a server-side approach using ColdFusion’s robust functionality. However, in this particular case, we were using .NET and frankly, I wanted the middleware developer on the project to stay focused on building the core .NET CRUD webservices that were required for the project.

treegrid

Here’s the first-pass at a solution, which I implemented as an override to the tree panel control. Calling it’s rather quite simple. Just invoke the tree panel’s downloadExcelXml() method.

 {
   xtype: 'button',
   flex: 1,
   text: 'Download to Excel',
   handler: function(b, e) {
     b.up('treepanel').downloadExcelXml();
   }
 }

And here’s the conversion of the tree store data to an Excel spreadshet…

Ext.define('MyApp.view.override.TreePanel', {
	override: 'Ext.tree.Panel',
	requires: 'Ext.form.action.StandardSubmit',

	/*
        Kick off process
    */

	downloadExcelXml: function(includeHidden, title) {

		if (!title) title = this.title;

		var vExportContent = this.getExcelXml(includeHidden, title);

		var location = 'data:application/vnd.ms-excel;base64,' + Base64.encode(vExportContent);

		/* 
          dynamically create and anchor tag to force download with suggested filename 
          note: download attribute is Google Chrome specific
        */

		if (Ext.isChrome || Ext.isGecko || Ext.isSafari) { // local download
			var gridEl = this.getEl();

			var el = Ext.DomHelper.append(gridEl, {
				tag: "a",
				download: title + "-" + Ext.Date.format(new Date(), 'Y-m-d Hi') + '.xls',
				href: location
			});

			el.click();

			Ext.fly(el).destroy();

		} else { // remote download

			var form = this.down('form#uploadForm');
			if (form) {
				form.destroy();
			}
			form = this.add({
				xtype: 'form',
				itemId: 'uploadForm',
				hidden: true,
				standardSubmit: true,
				url: 'http://webapps.figleaf.com/dataservices/Excel.cfc?method=echo&mimetype=application/vnd.ms-excel&filename=' + escape(title + ".xls"),
				items: [{
					xtype: 'hiddenfield',
					name: 'data',
					value: vExportContent
				}]
			});

			form.getForm().submit();
		}
	},

	/*

        Welcome to XML Hell
        See: http://msdn.microsoft.com/en-us/library/office/aa140066(v=office.10).aspx
        for more details

    */
	getExcelXml: function(includeHidden, title) {

		var theTitle = title || this.title;

		var worksheet = this.createWorksheet(includeHidden, theTitle);
		var totalWidth = this.columns.length;

		return ''.concat(
			'<?xml version="1.0"?>',
			'<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html="http://www.w3.org/TR/REC-html40">',
			'<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office"><Title>' + theTitle + '</Title></DocumentProperties>',
			'<OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office"><AllowPNG/></OfficeDocumentSettings>',
			'<ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">',
			'<WindowHeight>' + worksheet.height + '</WindowHeight>',
			'<WindowWidth>' + worksheet.width + '</WindowWidth>',
			'<ProtectStructure>False</ProtectStructure>',
			'<ProtectWindows>False</ProtectWindows>',
			'</ExcelWorkbook>',

			'<Styles>',

			'<Style ss:ID="Default" ss:Name="Normal">',
			'<Alignment ss:Vertical="Bottom"/>',
			'<Borders/>',
			'<Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="12" ss:Color="#000000"/>',
			'<Interior/>',
			'<NumberFormat/>',
			'<Protection/>',
			'</Style>',

			'<Style ss:ID="title">',
			'<Borders />',
			'<Font ss:Bold="1" ss:Size="18" />',
			'<Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1" />',
			'<NumberFormat ss:Format="@" />',
			'</Style>',

			'<Style ss:ID="headercell">',
			'<Font ss:Bold="1" ss:Size="10" />',
			'<Alignment ss:Horizontal="Center" ss:WrapText="1" />',
			'<Interior ss:Color="#A3C9F1" ss:Pattern="Solid" />',
			'</Style>',


			'<Style ss:ID="even">',
			'<Interior ss:Color="#CCFFFF" ss:Pattern="Solid" />',
			'</Style>',


			'<Style ss:ID="evendate" ss:Parent="even">',
			'<NumberFormat ss:Format="yyyy-mm-dd" />',
			'</Style>',

			'<Style ss:ID="evenint" ss:Parent="even">',
			'<Numberformat ss:Format="0" />',
			'</Style>',

			'<Style ss:ID="evenfloat" ss:Parent="even">',
			'<Numberformat ss:Format="0.00" />',
			'</Style>',

			'<Style ss:ID="odd">',
			'<Interior ss:Color="#CCCCFF" ss:Pattern="Solid" />',
			'</Style>',

			'<Style ss:ID="groupSeparator">',
			'<Interior ss:Color="#D3D3D3" ss:Pattern="Solid" />',
			'</Style>',

			'<Style ss:ID="odddate" ss:Parent="odd">',
			'<NumberFormat ss:Format="yyyy-mm-dd" />',
			'</Style>',

			'<Style ss:ID="oddint" ss:Parent="odd">',
			'<NumberFormat Format="0" />',
			'</Style>',

			'<Style ss:ID="oddfloat" ss:Parent="odd">',
			'<NumberFormat Format="0.00" />',
			'</Style>',

			'<Style ss:ID="indent1even" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="1"/>',
			'</Style>',

			'<Style ss:ID="indent2even" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="2"/>',
			'</Style>',

			'<Style ss:ID="indent3even" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="3"/>',
			'</Style>',

			'<Style ss:ID="indent4even" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="4"/>',
			'</Style>',

			'<Style ss:ID="indent1odd" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="1"/>',
			'</Style>',

			'<Style ss:ID="indent2odd" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="2"/>',
			'</Style>',

			'<Style ss:ID="indent3odd" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="3"/>',
			'</Style>',

			'<Style ss:ID="indent4odd" ss:Parent="odd">',
			'<Alignment ss:Horizontal="Left" ss:Vertical="Bottom" ss:Indent="4"/>',
			'</Style>',


			'</Styles>',
			worksheet.xml,
			'</Workbook>'
		);
	},


	getData: function() {

		var pnl = this;
		var cols = [];
		var maxDepth = 0;

		// get columns

		for (var i = 0; i < pnl.columns.length; i++) {
			cols.push({
				dataIndex: pnl.columns[i].dataIndex,
				title: pnl.columns[i].text,
				xtype: pnl.columns[i].xtype
			});
		}

		var aResult = [];
		var rootNode = pnl.getRootNode();

		rootNode.cascadeBy(function(node) {
			var rec = {};
			for (var j = 0; j < cols.length; j++) {
				rec[cols[j].dataIndex] = node.get(cols[j].dataIndex);
			}
			rec.depth = node.getDepth();
			if (rec.depth > maxDepth) {
				maxDepth = rec.depth;
			}
			aResult.push(rec);
		}, this);


		return {
			cols: cols,
			maxDepth: maxDepth,
			data: aResult
		}


	},



	createWorksheet: function(includeHidden, theTitle) {
		// Calculate cell data types and extra class names which affect formatting

		var data = this.getData();

		var cellType = [];
		var cellTypeClass = [];

		var totalWidthInPixels = 0;
		var colXml = '';
		var headerXml = '';
		var visibleColumnCountReduction = 0;

		for (var i = 0; i < data.cols.length; i++) {
			colXml += '<Column ss:AutoFitWidth="1"/>';
			headerXml += '<Cell ss:StyleID="headercell">' + '<Data ss:Type="String">' + data.cols[i].title + '</Data>' + '<NamedCell ss:Name="Print_Titles"></NamedCell></Cell>';
			switch (data.cols[i].xtype) {
				case "numbercolumn":
					cellType.push("Number");
					cellTypeClass.push("int");
					break;
				case "booleancolumn":
					cellType.push("String");
					cellTypeClass.push("");
					break;
				case "datecolumn":
					cellType.push("DateTime");
					cellTypeClass.push("date");
					break;
				default:
					cellType.push("String");
					cellTypeClass.push("");
					break;
			}
		}


		var visibleColumnCount = data.cols.length;

		var result = {
			height: 9000,
			// width: Math.floor(totalWidthInPixels * 30) + 50
			width: 1000
		};

		// Generate worksheet header details.

		// determine number of rows
		var numGridRows = data.data.length + 1;



		// create header for worksheet
		var t = ''.concat(
			'<Worksheet ss:Name="' + theTitle + '">',

			'<Names>',
			'<NamedRange ss:Name="Print_Titles" ss:RefersTo="=\'' + theTitle + '\'!R1:R2">',
			'</NamedRange></Names>',

			'<Table ss:ExpandedColumnCount="' + (visibleColumnCount + 2),
			'" ss:ExpandedRowCount="' + numGridRows + '" x:FullColumns="1" x:FullRows="1" ss:DefaultColumnWidth="65" ss:DefaultRowHeight="15">',
			colXml,
			'<Row ss:Height="38">',
			'<Cell ss:MergeAcross="' + (visibleColumnCount - 1) + '" ss:StyleID="title">',
			'<Data ss:Type="String" xmlns:html="http://www.w3.org/TR/REC-html40">',
			'<html:b>' + theTitle + '</html:b></Data><NamedCell ss:Name="Print_Titles">',
			'</NamedCell></Cell>',
			'</Row>',
			'<Row ss:AutoFitHeight="1">',
			headerXml,
			'</Row>'
		);

		// Generate the data rows from the data in the Store
		var groupVal = "",
			cellClass = null,
			v = null;
		
		for (var i = 1; i < data.data.length; i++) {

			cellClass = (i & 1) ? 'odd' : 'even';
			t += '<Row>';
			

			for (var j = 0; j < data.cols.length; j++) {
				v = data.data[i][data.cols[j].dataIndex];

				if (j == 0 && data.data[i].depth > 1) {
					// first col might be indented
				    t += '<Cell ss:StyleID="indent' + data.data[i].depth + cellClass + '"><Data ss:Type="' + cellType[j] + '">';
				} else {
					t += '<Cell ss:StyleID="' + cellClass + cellTypeClass[j] + '"><Data ss:Type="' + cellType[j] + '">';
				}

				
				if (cellType[j] == 'DateTime') {
					t += Ext.Date.format(v, 'Y-m-d');
				} else {
					t += v;
				}
				t += '</Data></Cell>';


			}
			t += '</Row>';
		}

		result.xml = t.concat(
			'</Table>',
			'<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">',
			'<PageLayoutZoom>0</PageLayoutZoom>',
			'<Selected/>',
			'<Panes>',
			'<Pane>',
			'<Number>3</Number>',
			'<ActiveRow>2</ActiveRow>',
			'</Pane>',
			'</Panes>',
			'<ProtectObjects>False</ProtectObjects>',
			'<ProtectScenarios>False</ProtectScenarios>',
			'</WorksheetOptions>',
			'</Worksheet>'
		);

		return result;
	}
});

The server-side code used to echo the generated spreadsheet back to the browser and force a “download” operation is the following:

<cfcomponent>

 <cffunction name="echo" access="remote" returntype="void">
  
  <cfargument name="mimetype" type="string" required="no" default="text/html">
  <cfargument name="filename" type="string" required="yes">
  <cfargument name="data" type="string" required="no" default="">

  <cfif isdefined("form.data")>
   <cfset arguments.data = form.data>
  </cfif>

  <cfheader name="Content-Disposition" value="attachment; filename=#arguments.filename#">
  
  <cfcontent type="#arguments.mimetype#"><cfoutput>#arguments.data#</cfoutput>

 </cffunction>

</cfcomponent>

Note that the ColdFusion-based “echo” webservice is available for evaluation/testing purposes only. No warranty or level of service is expressed or implied.

You can play around with the code on Sencha Fiddle:
https://fiddle.sencha.com/#fiddle/4pr

Go check it out!

Categories: Ext JS 4

Developing an Ext JS 4 Checkbox Group with “Other (please specify)” option

March 31, 2014 Leave a comment

I’m currently working on a dynamic forms/workflow solution for one of our clients. One of the requirements is to allow the user to not only choose from a set of values that are represented as a series of checkboxes, but also be able to specify their own custom value if necessary.

Image

In order to meet this requirement I subclassed the Ext.form.CheckboxGroup and it’s related layout – Ext.layout.container.CheckboxGroup

Defining a custom layout

Making room for the additional checkbox was a relatively straightforward affair. We just added an additional row to the layout class’ render template as illustrated on line 15 of the following source listing :

Ext.define('Ext.ux.layout.container.CheckboxOther', {
    extend: 'Ext.layout.container.CheckboxGroup',
    alias: ['layout.checkboxgroupother'],

    renderTpl: [
        '<table id="{ownerId}-innerCt" class="' + Ext.plainTableCls + '" cellpadding="0"',
            'role="presentation" style="{tableStyle}">',
            '<tbody role="presentation"><tr role="presentation">',
            '<tpl for="columns">',
                '<td class="{parent.colCls}" valign="top" style="{style}" role="presentation">',
                    '{% this.renderColumn(out,parent,xindex-1) %}',
                '</td>',
            '</tpl>',
        '</tr>',
        '<tr><td colspan="{[values.columnCount]}" class="{colCls}" valign="top" style="{style}" role="presentation"></td></tr>',
        '</tbody>',
        '</table>'
    ]

});

Extending the Ext.form.CheckboxGroup class

The final step was to extend the Ext.form.CheckboxGroup class in order to inject the “Other” textfield. This involved:

  • Rendering the textfield into the layout template (line 34)
  • Using the Ext.util.TextMetrics class to determine the pixel width of the text field prompt and then setting its labelWidth property accordingly (lines 30-31)
  • Overriding the getValue(), setValue(), getSubmitData(), and getModelData() fields to handle the textfield input.
Ext.define('Ext.ux.form.CheckboxGroupOther', {
	extend: 'Ext.form.CheckboxGroup',
	alias: 'widget.checkboxgroupother',
	requires: ['Ext.ux.layout.container.CheckboxGroupOther'],
	layout: 'checkboxgroupother',
	
	config: {
		otherBoxLabel: 'Other :',
		otherName: ''
	},

	// private

	textfield: null,

	initComponent: function() {

		// prevent checkboxes from submitting
		if (this.items) {
			for (var i=0; i<this.items.length; i++) {
				this.items[i].submitValue = false;
			}
		}
		this.callParent(arguments);
	},

	afterRender: function() {

		this.callParent(arguments);
		var tm = Ext.create('Ext.util.TextMetrics');
		var labelWidth = tm.getWidth(this.getOtherBoxLabel()) + 5;
		var colspanEl = Ext.get(this.getEl().select('td [colspan=' + this.columns + ']').elements[0]);

		this.textfield = Ext.widget('textfield', {
			renderTo: colspanEl,
			labelWidth: labelWidth,
			margin: '10 0 0 0',
			width: '100%',
			fieldLabel: this.getOtherBoxLabel(),
			name: this.getOtherName(),
                        style: {
			   'table-layout' : 'auto !important'
			},
			listeners: {
				change: {
					fn: this.otherBoxChange,
					scope: this
				}
			}
		});

	},

	isDirty: function() {

		if (this.textfield.isDirty()) {
			return true;
		} else {
			return this.callParent(arguments);
		}
	},

	setReadOnly: function(readOnly) {

		this.textfield.setReadOnly(readOnly);
		this.callParent(arguments);

	},

	reset: function() {
		this.textfield.reset();
		this.callParent(arguments);
	},

	otherBoxChange: function(cmp) {
		this.validate();
	},

	resetOriginalValue: function() {
		this.textfield.resetOriginalValue();
		this.callParent(arguments);
	},

	getErrors: function() {

		var errors = [];
		if (!this.allowBlank && Ext.isEmpty(this.getChecked()) && Ext.isEmpty(this.textfield.getValue())) {
			errors.push(this.blankText);
		}
		return errors;
	},


	// setValue syntax:
	//
	// setValue({rb: ['1','2', '3', 'Other: Foo']})
	//
	//

	setValue: function(value) {

		var cb = null;

		if (!Ext.isArray(value)) {
			value = [value];
		}

		for (var i = 0; i < this.items.items.length; i++) {

			cb = this.items.items[i];

			if (Ext.Array.contains(value, cb.inputValue)) {
				cb.setValue(true);
			} else {
				cb.setValue(false);
			}

		}

		for (var i = 0; i < value.length; i++) {
			if (Ext.isString(value[i]) && value[i].indexOf('Other: ') == 0) {
				this.textfield.setValue(value[i].substr(7));
			}
		}

	},


	getValue: function() {

		var values = [];

		for (var i = 0; i < this.items.items.length; i++) {
			var cb = this.items.items[i];

			if (cb.isCheckbox && cb.getValue()) {
				values.push(cb.inputValue)
			}
		}

		if (this.textfield) {
			var tfValue = this.textfield.getValue();
			if (!Ext.isEmpty(tfValue)) {
				values.push('Other: ' + tfValue);
			}
		}

		if (values.length == 1) {
			values = values[0];
		}

		return values;
	},


	getSubmitData: function() {

		var val = new Object();
		val[this.name] = this.getValue();

		return val;

	},

	getModelData: function() {

		var val = new Object();
		val[this.name] = this.getValue();

		return val;
	}
});

What are you waiting for?

Categories: Ext JS 4

Integrating Ext JS 4 and the TinyMCE 4 Rich Text WYSIWYG Editor

March 30, 2014 3 comments

TinyMCE is generally considered to be one of the best WYSIWYG browser-based editors that’s currently available. Not only does it have a ton of native features, but it also has an extensible plugin architecture that enables developers to add additional functionality with relative. One of it’s best features is the ability for a user to expand the editor to full-screen, thereby enabling a much better user experience. It also has a copy/paste plugin that can filter out nasty extraneous Microsoft Word markup.

TinyMCE is open-source and has LGPL and Commercial licenses available.

Image

Oleg Schildt was gracious enough to develop an Ext extension for TinyMCE 4, which we improved by adding the following features:

  • Making the class a lot more developer-friendly by adding config attributes to load plugins and automatically instantiate associated buttons
  • Added ICE version control plugin support (depicted above)
  • Automatically load TinyMCE from the CDN if not present
  • Added autoFocus param to prevent automatic focus on instantiation
  • Added dynamic show/hide of header & footer on focus / blur

We also created a custom a Sencha Architect user extension to support it, as well as bundled all of the source code into an Ext JS 4 “package” for easy deployment to your Sencha Cmd-based apps.

A few geeky technical points:

In order for a Sencha Architect user extension to operate properly, the Ext class must not be reliant on external javascripts. Therefore, we threw a little kludge into our codebase that “tricks” Architect into thinking that we’re instantiating a simple textarea:


// architect design canvas runs pages with
// a url starting with "ionp"

if (location.href.indexOf('ionp') == 0) {  

    // kludge for sencha architect canvas

    Ext.define('Ext.ux.form.TinyMceTextArea', {
        extend: 'Ext.form.field.TextArea',
        alias: 'widget.tinymce'
    });

} else {

    // actually load the class

    Ext.define('Ext.ux.form.TinyMceTextArea', {

        extend: 'Ext.form.field.TextArea',
        alias: 'widget.tinymce'

        // define the "guts" of the class here
    });
}

In order to make the component more “developer-friendly”, we used Ext.loader.loadScript() to load the TinyMCE and ice plugin scripts automatically. Note that ICE requires jQuery (for now):

if (!window['tinymce']) {
    Ext.Loader.loadScript({
        url: '//tinymce.cachefly.net/4.0/tinymce.min.js',
        onLoad: function() {
            if (window.loadIce) {
                Ext.Loader.loadScript({
                    url: '//code.jquery.com/jquery-1.9.1.min.js',
                    onLoad: function() {
                        Ext.Loader.loadScript({
                            url: '//code.jquery.com/jquery-migrate-1.0.0.js'
                        });
                    }
                });
                Ext.Loader.loadScript({
                    url: 'packages/TinyMCE/resources/ice/ice-master.min.js'
                });
                Ext.Loader.loadScript({
                    url: 'packages/TinyMCE/resources/tinymce/plugins/ice/plugin.js'
                });
            }
        },
        scope: this
    });
}

And finally, we packaged everything (including the Sencha Architect plugin) into a Sencha package, which enables you to combine your component’s source code, related design assets, and (optionally) Sencha Architect plugin into neatly organized folder structure. Note that in the following screenshot, you can configure the TinyMCE editor plugins and other attributes by filling out a simple configuration form.

Sencha Architect Integration

 

So what are you waiting for?

Categories: Ext JS 4

Generate an Excel File from an Ext JS 4 Grid or Store (Version 1.1)

March 19, 2014 Leave a comment

As illustrated by the following screenshot, I’ve tweaked my Excel.js script that converts a Sencha Touch grid/store to an Excel spreadsheet so that it now supports grouping.

group

 
Test and download the code by clicking here.
View the original blog post by clicking here.

Categories: Ext JS 4

9 Chrome Plugins to help you debug your web apps and GSD

March 7, 2014 1 comment

I’m personally obsessed with GSD (getting stuff done) and find the following (free) developer tools to be invaluable in this regard. Check ‘em out, and if you have any other suggestions, please list them in the comments!

POSTMAN

Postman is a free Google Chrome App that enables you to easily test your REST API’s.

Image

Features include:

  • Ability to create requests quickly
  • Simulate HTTP requests with file upload support
  • Auto-Formatting of JSON and XML responses
  • Basic an OAuth Helpers
  • Share collections between developers
  • Create unit tests by adding “Jetpacks” ($9.95 US)

CLEAR CACHE

Clear Cache is a free Google Chrome App that enables you to reliably clear your browser cache at the click of a button.

Image

APACHE RIPPLE

Apache Ripple™ is a web based mobile environment simulator designed to enable rapid development of mobile web applications for various web application frameworks, such as Apache Cordova™ and BlackBerry® WebWorks™. It can be paired with current web based mobile development workflows to decrease time spent developing and testing on real devices and/or simulators.

Image

It is free software, licensed under the Apache License, Version 2.0.

Features include:

  • Disabling cross-domain security restrictions
  • Accelerometer simulation
  • Geolocation simulation

JSON EDITOR ONLINE

JSON Editor is a Chrome plugin for viewing, editing, and formatting JSON. It shows your data in an editable treeview and in a code editor.

JSONEditor

Features include:

  • View and edit JSON side by side in treeview and a code editor.
  • Edit, add, move, remove, and duplicate fields and values.
  • Change type of values.
  • Sort arrays and objects.
  • Colorized values, color depends of the value type.
  • Search & highlight text in the treeview.
  • Undo and redo all actions.
  • Load and save files and urls.
  • Format, compact, and inspect JSON in the code editor.

REGEXP TESTER

This free Chrome plugin enables you to quickly and easily verify the validity of your javascript regular expressions.

regexp

ICOMOON

Icomoon enables you to rapidly select and encode icons as a custom font. This is particularly useful  if you’re developing custom themes for Sencha Touch and Ext JS 4.21+ applications.

icomoon

SPEEDTRACER

Speed Tracer by Google is a tool to help you identify and fix performance problems in your web applications. It visualizes metrics that are taken from low level instrumentation points inside of the browser and analyzes them as your application runs.

speedtracer

If you’re obsessed with Javascript RIA application performance, check it out!

CHROMEVOX

If you want to verify that your Javascript-based RIA (Ext JS 4) is accessible for the vision impaired, check out this free Chrome plugin.

APP INSPECTOR FOR SENCHA

App Inspector for Sencha™ is an extension for Google Chrome DevTools which makes debugging Sencha Ext JS and Touch applications easier than ever before!

App Inspector for Sencha™ helps you to inspect your component tree, data stores, events, and layouts. It provides similar functionality to Illuminations for Developers.

appinspector

Miscellaneous Ext JS 4 Tips and Tricks, Part I

February 28, 2014 Leave a comment

Adding Spacing Between Action Column Icons

Our usability folks have been complaining that the default spacing (0px) between action item buttons could result in a user mistakenly clicking the wrong button.

actioncolumnitemspacing

Add this to your CSS:

.x-grid-cell-inner-action-col img {
  margin-right: 5px;
 }

Adding Tooltips to Combo Box Selections

toolkit1

Override the getInnerTpl() method as illustrated below. Don’t forget to escape embedded quotes in the output!


{
  xtype: 'combobox',
  listConfig: {
    getInnerTpl: function() {
      return "<div data-qtip='{[values.description.replace(\"'\",\"\'\")]}'>{name}</div>"; 
    }
  },
  itemId: 'toolkitfilter',
  fieldLabel: 'Toolkits',
  name: 'toolkit',
  displayField: 'name',
  forceSelection: true,
  multiSelect: true,
  queryMode: 'local',
  store: 'Toolkits',
  valueField: 'id'
}

Adding Tooltips to Form Fields

Because users need a lot of help…

Add tooltips to form fields by attaching a tooltip in the afterrender event as illustrated below:

{
  xtype: 'textfield',
  fieldLabel: 'Enter your name',
  allowBlank: false,
  anchor: '100%',
  listeners: {
   afterrender: function(component) {
     Ext.create('Ext.tip.ToolTip', {
                   target: component.getEl(),
                   html: "Enter your lastname, firstname please. This is a required field."
     });
   }
  }
}

You can try out this example at https://fiddle.sencha.com/#fiddle/3vs and check out the full application at http://www.naccho.org/toolbox

Happy coding!

Categories: Ext JS 4
Follow

Get every new post delivered to your Inbox.

Join 532 other followers