ExtJS 6 : Multiline cell within a grid

In one app, some messages were displayed into a grid, quite a common stuff.
However, long messages were stripped and I needed to display the whole message.

Here is the approach:
1) Add the following style to your css/scss

.multiline-row {
  overflow: auto !important;
  white-space: normal !important;
  text-overflow: ellipsis;
  display: block;
}

2) Define a rendered for the multiline cell. For example, here is my column declaration ( I prefer to use a span instead of a div )

, columns: [
        {
            xtype: 'gridcolumn', align: 'left'
            , dataIndex: 'message_content', flex: 3, text: 'Msg'
            , renderer: function (value, metaData, record, rowIndex) {
                return '' + value + '';
        }
]

And that’s pretty all!

ExtJS 5/6 : FileField/FileUploadFile : reading data

Introduction

I have a “filefield” or “fileuploadfield” in ExtJS, and I’d like to read directly its content directly into the browser, when user selects a file.

It is useful when you’d like to display an image for example, as soon as the user selects it.

How To Proceed

First of all, you need to declare the “filefield” with a listener on “change” event. For example:

{
    xtype: 'filefield'
    ,fieldLabel: "File: "
    ,labelWidth: 70
    ,anchor: '100%'
    ,buttonOnly: true // hide text field below button
    ,buttonText: "Select a picture"
    ,listeners: {
        'change':  'onMediaChange'
    }
}

and in the Controller, the handler:

onMediaChange: function(widget, value, eOpts ) {
  var files = event.target.files
      self = this; // the controller
  if (!files || files.length == 0) return; // make sure we got something
  // Let's read content as file
  var reader = new FileReader();
  reader.onload = function (e) {
    // image content is in e.target.result
    // we can then put it into img.src, for example
    self.lookupReference('myImg').setSrc(e.target.result);
  };
  reader.readAsDataURL(files[0]);
}

and that’s it!

ExtJS 5/6 : Alert Message (kind of Android Toast/Bootstrap Alert)

In ExtJS KitchenSink example, Sencha provides/uses an alert message, sliding from top, then sliding out.

This kind of temporary message is always useful to popup some alerts/informations for example.

After having a look on Sencha Ext.example.msg, I made couple of improvements using Bootstrap 3.x alert’s ideas.

So, this “temporay message box” slides from top, stays “delays” seconds, then slides out. There are 5 different classes (default, success, info, danger, warning) like in Bootstrap alerts (color are roughly identical). Here is a look of each:

ExtJS Alert/Toast messages with different classes
ExtJS Alert/Toast messages with different classes

The code (js and scss) is available on GitHub: My GitHub Repo

ExtJS 5/6 : Grid’s RowEditing Locale & Options

Within a Grid, RowEditing plugin is convenient to edit values directly. However, Locale (l10n, i18n) is  (sometimes) a mess with ExtJS 5 or 6, and that’s still the case with RowEditing.

However, one very simple way to solve this is to override button labels/messages while declaring/configuring the plugin:

Ext.define('MyApp.view.DataGrid',{
    extend: 'Ext.grid.Panel'
    ,requires: [
        'Ext.grid.plugin.RowEditing'
    ]
    ,plugins: [
        {
            ptype: 'rowediting',
            clicksToEdit: 2,  
            disabled: false, // disable RowEditing, tgrough binding for example
            saveBtnText: i18n('global.update'), // label for update/save btn
            cancelBtnText: i18n('global.cancel') // label for cancel btn
            errorsText: 'Errors', // errors text dirtyText: 'You need to commit or cancel...' // if record is dirty 
         } 
    ] /* ... */ });

And that’s all !!!  You could notice that I also declare “disabled” field as I bind it to enable/disable RowEditing plugin on the fly (for example, based on user profile).

For example, in the following screenshot, buttons are in french:

ExtJS Grid Row Editing in French
ExtJS Grid Row Editing in French

ExtJS 6 – Router improvements

Introduction

With ExtJS 5.x, I was not convinced by the “Router” part of the framework. It was quite a mess, and implemented through Mixins. I remembered having a deep look on Saki example.

In ExtJS 6, the Router implementation is really a good step ahead, and remembers me the one I used in AngularJs. Sencha has made a detailed article over there.

Quick Summary

  1. Put the default route in your Application declaration. Example:
    defaultToken : 'home', // default route
  2. Defined routes with your Controller :
    routes : { 
      'users': 'onUsers',
      'user/:id' : 'onUser'
    },
    onUsers : function() {
      //...
    }
    onUser : function(id) {
      //...
    }
  3. Update/Change route within Controller (here “this” is the controller)
    this.redirectTo('user/1234', true); // redirect

You can also setup some masks on route parameters and have BeforeRoute function (see Sencha doc for more details).

Multiple Routes

Finally, one point : ExtJs 6 supports multiple routes (routes pipe), with a route separator “|” (which could be changed): in that case, each route is handled sequentially by its order of appearance.

Reference: Sencha’s ExtJS 6 : Router

Conclusion

In ExtJS 6, Router is in line with AngularJS’ one and is a big step ahead since ExtJS5.x.

ExtJS : Accessing Application from anywhere

In ExtJS, “Application” is handling and containing a lot of stuff. Among other things, useful getters are:

  • getStore(‘store_name’) : retrieve a store
  • getRoutes()  : get Routes

How to retrieve current Application?

In fact, is is really simple. Let’s imagine that your App namespace is ‘MyApp’; then it can be retrieve using:

MyApp.getApplication()

Update (March 13th): you can also use

MyApp.app

ExtJS 5/6 : localization / i18n / l10n

 

This post is an update of previous one : https://abarre.wordpress.com/2015/09/29/extjs-5-and-localization/

Project files are now hosted on GitHub : GitHub “extjs-i8n-lang” Project

Approach

This approach is very simple:

  1. Create a ‘Lang’ class
  2. Add languages string based on a ‘yaml/json’ tree approach
  3. Create some shortcuts
  4. Add you ‘Lang’ class in ExtJS’s Application requires section
  5. Use it

The ‘Lang’ class template

Ext.define('Lang',{
    singleton : true

    ,config : {
        currentLang : null,
        defaultLang : 'us'
    }

    ,getStrings: function(lang) {
        if (!lang || !Ext.isDefined(this.strings[lang]))
            lang=this.getDefaultLang();
        if (Ext.isString(this.strings[lang])) // got an aliase
            lang = this.strings[lang];
        return Ext.isDefined(this.strings[lang])
                ? this.strings[lang]
                : { };
    }

    ,t : function(label, lang) {
        var path = label.toLowerCase().split('.'),
            p = this.getStrings(lang); // strings
        for (var i = 0; i<path.length; i++) {
            if (Ext.isDefined( p[ path[i] ]))
                p = p[ path[i] ];
            else return '<<' + label + '>>';
        }
        return p;
    }

    ,constructor : function(config) {
        this.initConfig(config);
        if (!this.getCurrentLang())
            this.setCurrentLang( this.getDefaultLang());
    }

    strings: {
        'us_US': {
            'hello_world': 'Hello World!',
            'map' : 'Map'
        },
        'fr_FR': {
            'hello_world': 'Bonjour Le Monde!',
            'map' : 'Carte'
        },
        'es_ES': {
            'hello_world': 'Ola el Mundo!',
            'map' : 'Carto.'
        }
    }
});
var i18n = function(label, lang) { return Lang.t(label, lang); };

Usage

i18n('hello_world')
i18n('map')

ExtJS5 and Leaflet : Getting Started. Example with a “Desktop” window

Here is couple of lines on using Leaflet with ExtJS. The Leaflet widget I’ve been using is based on a GitHub project by  kazmiekr (@see Leaflet UX). Many thanks to that !!!

Setup

The setup is VERY easy. The only requirements are to add in index.html all the leaflet common requirements.

https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.css
src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.js

The Leaflet Map Widget

As I wrote it above, the code is widely based on kazmiekr work (@see Leaflet UX). Couple of improvements:

  • Multiple tiles providers support,
  • Use of OpenStreetMap instead of CloudMade as default map,
  • initMap function, called at the end of “afterrender” listener.
Ext.define('Ext.ux.LeafletMapView', {
    extend: 'Ext.Component'
    ,alias: 'widget.leafletmapview'

/* // Optional: add an additionnal controller 
   // with alias: controller.leaflet-map
,requires: [
 'Ext.ux.LeafletMapController'
 ]
   ,controller: 'leaflet-map'
*/
    ,config:{
        initialLocation: null,
        initialZoomLevel: null,
        map: null,
        layerControl: null,
        useCurrentLocation: false

// Description of tiles providers
        ,tiles: {
            osm: {
                title: 'OSM'
                ,url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
                ,maxZoom: 18
                ,attribution: "OpenStreetmap"
            }
            ,mapbox : {
                title: 'MapBox v4'
                ,url: 'http://{s}.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}.png?access_token={key}'
                ,maxZoom: 18
                ,key: "MAPBOX API KEY"
                ,attribution: '&copy; GGL/OSM'
            }
        }
        ,defaultTile: 'osm' // default tile provider
        ,initMap: function(map) {} // called afterRender
    }


// Will be called by afterrender => add tiles selector
    ,initTiles: function(map) {
        var tiles = {}, list = this.getTiles();
        for(var i in list) {
            var t = list[i]
                ,tile = L.tileLayer(t.url, t);
            tiles[t.title] = tile;
            if (i == this.getDefaultTile()) // Add Default map..
                tile.addTo(map);
        }

        // Add Map Control
        var layerControl = L.control.layers(tiles, {});
        layerControl.addTo(map);
        this.layerControl = layerControl;
    }

// afterrender is called the 1st time the widget is created
    ,afterRender: function(t, eOpts){
        this.callParent(arguments); 
        var leafletRef = window.L;
        if (leafletRef == null){
            this.update("No leaflet library loaded");
        } else {
            var map = L.map(this.getId());
            this.setMap(map);
            this.initTiles(map);

            var initialLocation = this.getInitialLocation();
            var initialZoomLevel = this.getInitialZoomLevel();
            if (initialLocation && initialZoomLevel){
                map.setView(initialLocation, initialZoomLevel);
            } else {
                map.fitWorld();
            }
            if (this.getUseCurrentLocation() == true){
                map.locate({
                    setView: true
                });
            }

            if (Ext.isFunction(this.initMap)) {
                this.initMap(Ext.isDefined(this.scope) ? this.scope : this, map);
            }
        }
    }

    /** Called on Element resize => invalidate map **/
    ,onResize: function(w, h, oW, oH){
        this.callParent(arguments);
        var map = this.getMap();
        if (map) map.invalidateSize();
    }
});

Example : a Desktop window (with a Tree Panel on left)

Ext.define('Desktop.map.GlobalMapWindow', {
    extend: 'Ext.ux.desktop.Module',
    requires: [
        'Ext.panel.Panel'
        ,'Ext.ux.LeafletMapView'
        ,'Ext.tree.Panel'
    ]

    ,id: 'panel-map-global'
    ,title: "Window Title"
    ,iconCls: 'fa fa-globe' // font-awesome icon

    ,init : function(){
        this.launcher = { // start menu shortcut
            text: this.title
            ,iconCls: this.iconCls
        }
    }

    ,config : {
        mapWidget: null
        ,treeWidget : null
    }
/**
 * Create the Window
 * @returns {*}
 */
,createWindow : function(){
    //GLC.Config.log.log(this.id, 'createWindow');
    var me = this,
        app = this.app
        ,desktop = app.getDesktop()
        ,win = desktop.getWindow( this.id );
    if(!win || win.isDestroyed){
        // reset internal object cache
        this.mapWidget = null;
        this.treeWidget = null;
        // create window
        win = desktop.createWindow({
            id: this.id,
            title: this.title,
            width: 740,
            height: 480,
            iconCls: this.iconCls,
            animCollapse: false,
            border: false,
            constrainHeader: true,
            layout: {
                type: 'border'
            }
   ,items: [
    {  // Left Tree Panel
        xtype: 'treepanel'
        ,region: 'west'
        ,title: null
        ,itemId: 'treelayers'
        ,rootVisible: true
        ,split: true
        ,collapsible: true
        ,collapsed: false
        ,width: 200
        //,fields: ['name', 'description']
        ///*
        ,columns: []
        ,listeners: { }
    } // eo TreePanel

   ,{ // The Map
                    xtype: 'leafletmapview'
                    ,region: 'center'
                    ,flex:2
                    ,useCurrentLocation: true
                    ,itemId: 'map'
                    /* // in case I need an additionnal
                       //afterrender function
                    ,initMap: me.initMap
                    ,scope: me
                    */
                }
            ]
        });
    }
    //console.log(win);
    return win;
}

/**
 * Get the Leaflet.Map component with LeafletMap Widget
 * @returns Leaflet.Map
 */
,getMap: function() { // retrieve the map
    if (!this.mapWidget) this.mapWidget = Ext.ComponentQuery.query("#" + this.id + " > #map")[0];
    return this.mapWidget.getMap();
}

/**
 * Get the PanelTree component
 * @returns Ext.tree.Panel
 */
,getTreeWidget: function() {
    if (!this.treeWidget) this.treeWidget = Ext.ComponentQuery.query("#" + this.id + " > #treelayers")[0];
    return this.treeWidget;
}

}); // eo Desktop.map.GlobalMapWindow

 

 

ExtJS 5 : get a store from anywhere in your app

In the app I’m working on, I have multiple stores, and I need to get an access to any of them from anywhere.

There are multiple approaches: store (!) the Stores’list in a global variable or in an App variable, …

In fact, there is a much more simple way. When you declare your store, setup a “storeId” parameter.

Ext.create('Ext.data.Store',
{
  model: 'My.Model',
  storeId: 'my-model-store'
  [...]
}

Then, to easily access this store from anywhere:

var store = Ext.data.StoreManager.lookup('my-model-store');

Let’s be honest : it is nothing more than a global variable and you can achieve the same easily; anyway, it conveniently rocks!