ExtJs - An ExtJs Ticker Component supporting adding items dynamically

Tickers used to be an essential part of any website during the earlier days of web, but are no longer considered that much useful today, right? Well I atleast thought so until a few days back, when I received a feature request for a scrolling ticker in a corporate intranet app (which uses ExtJs heavily, in fact with a completely ExtJs based UI). And on some inspection, the request seemed to be reasonable.

Those people have lots of news feeds emerging from various parts of the corporation and wanted the feeds to be shown on user's Dashboard and updated dynamically as the feeds change. Fair enough feature request and lets now build an ExtJs Ticker, I said to myself.

There are lots of free Ticker scripts available on the web as you can see, and in fact I had written one such script myself sometime back for my Scrolling Announcement module for DotNetNuke. But I did not want to use any of these (sometimes bulky) scripts and wanted to have a clean solution for ExtJs.

I thought many people might have required such an ExtJs Ticker, and I actually found a couple, but they did not suit my requirements exactly. Both the tickers I encountered allowed scrolling static content, meaning that once they start scrolling, you cannot manipulate the scrolled content dynamically. What I needed was the ability to add items dynamically to the Ticker that are fetched in background through Ajax calls. So, as I was to start writing such a Ticker myself, I found this comment on Sencha forums which gave sample code for a basic ExtJs Ticker. I found the sample promising and decided to use it as the base for creating my custom ExtJs Ticker.

Before seeing and discussing the code, let's first see the Ticker in action (click here to open the demo in new window):

 

 

Try entering any feed url in the textbox and click "Add Url feed" button and notice the feed items appended dynamically to the Ticker with no flicker or affect on existing scroll.

Here is a quick list of features of this ticker (that was obviously based on specs for the app for which this ticker was created in the first place):

  1. Ability to add items dynamically, this was the most important and crucial feature.
  2. Ability to scroll in any direction (left to right, right to left, top to bottom or bottom to top).
    Although my immediate need was strictly a bottom to top scroll, I decided to keep room for scrolling in any direction to avoid rework in future in case a different scroll direction was needed.
  3. Configurable speed - This again was dictated by client needs. Users desired the ability to control scrolling speed on an individual basis.
  4. Pause scrolling on hover - needless to say, client requirement.
  5. Clickable items - clicking on items needed to bring the details up, so individual items needed to be clickable (try clicking an item in the scrolling Ticker above).
  6. Css customizability - the design team dictated this. The ticker provides you classes that would allow you to set css differently based on scrolling direction if needed.
  7. Light-weight - who needs tons of lines of javascript when ExtJs provides so much out of the box.

Based on these specifications, following is the ticker code I came up with (which is what is working in the sample above):

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }Ext.ux.Ticker = Ext.extend(Ext.BoxComponent, { baseCls: 'x-ticker', autoEl: { tag: 'div', cls: 'x-ticker-wrap', children: { tag: 'div', cls: 'x-ticker-body' } }, body: null, constructor: function(config) { Ext.applyIf(config, { direction: 'up', speed: 1, pauseOnHover: true }); if (config.speed < 1) config.speed = 1; else if (config.speed > 20) config.speed = 20; Ext.applyIf(config, { refreshInterval: parseInt(10 / config.speed * 15) }); config.unitIncrement = 1; Ext.ux.Ticker.superclass.constructor.call(this, config); this.addEvents('itemclick'); }, afterRender: function() { this.body = this.el.first('.x-ticker-body'); this.body.addClass(this.direction); this.taskCfg = { interval: this.refreshInterval, scope: this }; var posInfo, body = this.body; switch (this.direction) { case "left": case "right": posInfo = { left: body.getWidth() }; this.taskCfg.run = this.scroll.horz; break; case "up": case "down": posInfo = { top: body.getHeight() }; this.taskCfg.run = this.scroll.vert; break; } posInfo.position = 'relative'; body.setPositioning(posInfo); Ext.ux.Ticker.superclass.afterRender.call(this); if (this.pauseOnHover) { this.el.on('mouseover', this.onMouseOver, this); this.el.on('mouseout', this.onMouseOut, this); this.el.on('click', this.onMouseClick, this); } this.task = Ext.apply({}, this.taskCfg); Ext.TaskMgr.start(this.task); }, add: function(o) { var dom = Ext.DomHelper.createDom(o); this.body.appendChild(Ext.fly(dom).addClass('x-ticker-item').addClass(this.direction)); }, onDestroy: function() { if (this.task) { Ext.TaskMgr.stop(this.task); } Ext.ux.Ticker.superclass.onDestroy.call(this); }, onMouseOver: function() { if (this.task) { Ext.TaskMgr.stop(this.task); delete this.task; } }, onMouseClick: function(e, t, o) { var item = Ext.fly(t).up('.x-ticker-item'); if (item) { this.fireEvent('itemclick', item, e, t, o); } }, onMouseOut: function() { if (!this.task) { this.task = Ext.apply({}, this.taskCfg); Ext.TaskMgr.start(this.task); } }, scroll: { horz: function() { var body = this.body; var bodyLeft = body.getLeft(true); if (this.direction == 'left') { var bodyWidth = body.getWidth(); if (bodyLeft <= -bodyWidth) { bodyLeft = this.el.getWidth(true); } else { bodyLeft -= this.unitIncrement; } } else { var elWidth = this.el.getWidth(true); if (bodyLeft >= elWidth) { bodyLeft = -body.getWidth(true); } else { bodyLeft += this.unitIncrement; } } body.setLeft(bodyLeft); }, vert: function() { var body = this.body; var bodyTop = body.getTop(true); if (this.direction == 'up') { var bodyHeight = body.getHeight(true); if (bodyTop <= -bodyHeight) { bodyTop = this.el.getHeight(true); } else { bodyTop -= this.unitIncrement; } } else { var elHeight = this.el.getHeight(true); if (bodyTop >= elHeight) { bodyTop = -body.getHeight(true); } else { bodyTop += this.unitIncrement; } } body.setTop(bodyTop); } } });{/syntaxhighlighter}

The code basically sets up a custom autoEl for the BoxComponent-derived Ticker, and starts an Ext.Task to scroll the contents regularly.

The code in the comment on the Sencha forum (referenced above) provided a very good foundation for my Ticker requirements and my code maintains its original soul from the comment.

A very interesting point to note above is that the Ticker (Ext.ux.Ticker) is a BoxComponent derived class. Which means it can be added directly as an item to a container (as in the example above where it has been added to 'east' panel of a viewport) or it can be placed directly in yout html somewhere. Lazy instantization (for which you need to asign Ticker an xtype), participating in Layout calls and other ExtJs Component features come natively to the Ticker.

I have tested it with Chrome, Chrome Frame, IE 9 and FF 3, but I think it should work fine with any modern browser.

You can find the javascript code and css for the ticker (providing empty classes to give you an idea on what is available so you can customize the look and feel) as well as the html file for reproducing the above example attached below.

 

AttachmentSize
File extjs-ticker.js4.24 KB
File extjs-ticker.css491 bytes
HTML icon extjs-ticker.html6.59 KB

Comments

Hi, This looks great.. but How about if I want to make the text scroll from left to right?

rahul's picture

Well go ahead and modify it for yourself :)
It should not be much effort to adapt vertical scrolling for horizontal scrolling. 

Right so I'm trying to use it with Ext 4.2.1 and can't make it work... displays this error in firebug console

Error: [Ext.extend] Attempting to extend from a class which has not been loaded on the page.

I already make reference ti the .js and .css

any help? 

rahul's picture

Well this extension is built for Ext 3.x.

How can I use this without news feeds? Suppose I have static text that I need to display as ticker(Right to Left) what are the configs I need to provide?

rahul's picture

Create an empty Ticker as demonstrated in the sample and call add once on it passing your static text as DomHelper specification object.

Perfect. Thanks.

Hi Rahul,

Your user extension is really great. I modified the code a bit to be able to use it with ExtJs4

This is the modified code: 

Ext.define('MyApp.ux.Ticker', {

    extend: 'Ext.Component',

    xtype: 'ticker',

    baseCls: 'x-ticker',

    autoEl: {

        tag: 'div',

        cls: 'x-ticker-wrap',

        children: {

            tag: 'div',

            cls: 'x-ticker-body'

        }

    },

    body: null,

 

 

    constructor: function (config) {

        Ext.applyIf(config, {

            direction: 'left',

            speed: 10,

            pauseOnHover: true

        });

        if (config.speed < 1) config.speed = 1;

        else if (config.speed > 20) config.speed = 20;

 

 

        Ext.applyIf(config, {

            refreshInterval: parseInt(10 / config.speed * 15)

        });

        config.unitIncrement = 1;

 

 

        this.callParent([config]);

    },

 

 

    afterRender: function () {

        this.body = this.el.first('.x-ticker-body');

        this.body.addCls(this.direction);

 

 

        this.taskCfg = {

            interval: this.refreshInterval,

            scope: this

        };

 

 

        var posInfo, body = this.body;

        switch (this.direction) {

            case "left":

            case "right":

                posInfo = { left: body.getWidth() };

                this.taskCfg.run = this.scroll.horz;

                break;

            case "up":

            case "down":

                posInfo = { top: body.getHeight() };

                this.taskCfg.run = this.scroll.vert;

                break;

        }

        posInfo.position = 'relative';

 

 

        body.setPositioning(posInfo);

        DHT.ux.Ticker.superclass.afterRender.call(this);

 

 

        if (this.pauseOnHover) {

            this.el.on('mouseover', this.onMouseOver, this);

            this.el.on('mouseout', this.onMouseOut, this);

            this.el.on('click', this.onMouseClick, this);

        }

 

 

        this.task = Ext.apply({}, this.taskCfg);

        Ext.util.TaskManager.start(this.task);

    },

 

 

    add: function (o) {

        var dom = Ext.DomHelper.createDom(o);        

        this.body.appendChild(Ext.fly(dom).addCls('x-ticker-item').addCls(this.direction));

    },

 

 

    onDestroy: function () {

        if (this.task) {

            Ext.util.TaskManager.stop(this.task);

        }

 

 

        DHT.ux.Ticker.superclass.onDestroy.call(this);

    },

 

 

    onMouseOver: function () {

        if (this.task) {

            Ext.util.TaskManager.stop(this.task);

            delete this.task;

        }

    },

 

 

    onMouseClick: function (e, t, o) {

        var item = Ext.fly(t).up('.x-ticker-item');

        if (item) {

            this.fireEvent('itemclick', item, e, t, o);

        }

    },

 

 

    onMouseOut: function () {

        if (!this.task) {

            this.task = Ext.apply({}, this.taskCfg);

            Ext.util.TaskManager.start(this.task);

        }

    },

 

 

    scroll: {

        horz: function () {

            var body = this.body;

            var bodyLeft = body.getLeft(true);

            if (this.direction == 'left') {

                var bodyWidth = body.getWidth();

                if (bodyLeft <= -bodyWidth) {

                    bodyLeft = this.el.getWidth(true);

                } else {

                    bodyLeft -= this.unitIncrement;

                }

            } else {

                var elWidth = this.el.getWidth(true);

                if (bodyLeft >= elWidth) {

                    bodyLeft = -body.getWidth(true);

                } else {

                    bodyLeft += this.unitIncrement;

                }

            }

            body.setLeft(bodyLeft);

        },

 

 

        vert: function () {

            var body = this.body;

            var bodyTop = body.getTop(true);

            if (this.direction == 'up') {

                var bodyHeight = body.getHeight(true);

                if (bodyTop <= -bodyHeight) {

                    bodyTop = this.el.getHeight(true);

                } else {

                    bodyTop -= this.unitIncrement;

                }

            } else {

                var elHeight = this.el.getHeight(true);

                if (bodyTop >= elHeight) {

                    bodyTop = -body.getHeight(true);

                } else {

                    bodyTop += this.unitIncrement;

                }

            }

            body.setTop(bodyTop);

        }

    }

});

 

And this is the way it is used:

 

border: false,

                    height: 40,

                    width: 256,

                    layout: {

                        type: 'hbox',

                        align: 'stretch'

                    },

                    items: [{

                        width: 200,

                        direction: 'left',

                        xtype: 'ticker',

                        itemId: 'rollerPanel'

                    }],

                    listeners: {

                        afterrender: function (panel) {

                            var ticker = panel.down('ticker');

                            ticker.add({

                                tag: 'div',

                                cls: 'title',

                                html: "Ticker content."

                            });

                        }

                    }

 

Now the problem with this code is the amount of memory it uses. When the text is scrolling, the CPU cycles(in Google Chrome task manager) reaches at 10+ but when I hover the cursor over it, the text stops and the CPU cycles get down to 0. Can you please check what is the issue with the code? Does your original code behaves the same? If yes, how can the code be made more memory efficient?

 

Sorry, in the above snippet

 

border: false,

height: 40,

width: 256,

.

.

.

should be:

xtype: 'panel',

border: false,

height: 40,

width: 256,

.

.

.

rahul's picture

Hi Zafar, it seems the original code also consumes a few CPU cycles while the ticker is running. Unfortunately I am running short on time to take a look at this.

BTW, this is being used in multiple large deployments with thousands of users and nobody has really gotten back to us saying the web app is sluggish.

Thanks Rahul. My app is also running fine. There is not a single report of it being sluggish. I don't know why the reporter of this said "bug" commented "It is consuming a lot of CPU memory"? The ticker is moving DOM elements on the screen at frequent intervals. The ticker screen is getting refreshed regularly, so it is obvious the CPU is going to work more than usual. It's not the sluggishness of the application that is being reported but it is the CPU cycles that is worrying the people. Anyways, thanks a lot.

rahul's picture

Well I can imagine people getting worried. And the correct term to use is "consuming CPU cycles" (not memory). Anyways, I wouldn't worry much about this unless it slows down the web app or the system.