ExtJs and iScroll - Scrolling Ext containers on Touch devices (iPad, iPhone etc.) using iScroll 4

I yesterday did a very interesting thing, integrating ExtJs with the excellent iScroll 4 script from Matteo Spinelli that would make my ExtJs containers intuitively scrollable on Touch devices (especially iPad) giving them a more native scrolling feel.

You might be knowing already that mobile webkit (on iPhone, iPad, Android) does not provide a native way to scroll content inside a fixed width/height element. In simpler terms, this means overflow: auto or overflow: scroll does not work on mobile browsers as you are used to seeing them on PCs. When a element's contents exceed its visible area on touch devices, scrollbars do not appear for any value of overflow css property you specify. Instead mobile users are left with having to use the extremely unfriendly 2-finger scroll to scroll contents on mobile browsers.

And Matt's script addresses exactly this issue by attaching custom scrollbars to such "overflown" html elements. It works great stand-alone but I was tasked to find out an easy non-breaking approach to integrate iScroll into an existing large web app that leverages ExtJs extensively, preferably in a way that should not require changes to component config specifications.

Before discussing how I approached it, let's first have a sneak-peak of an example demonstrating the behavior of what I did. Below you will find 3 "iScrolled" ExtJs components, a Panel, a GridPanel and a TreePanel. Try clicking any component and dragging it up or down to see how scrolling behaves. You would see the best results when viewing this example on a Touch device (preferable iPad or another tab device). Click here to open the demo in new window:

 

 

Now iScroll is a pretty easy library to use. For each html element to which you want to have scrollbars attached when it overflows, you need to wrap the element in another html element having "overflow: auto" and register this parent element with iscroll in a single line, as easy as that (please check the official iScroll page for more details on how to use iScroll and various options available).

So I thought well let's hook into the rendering process for an ExtJs Container and register the container's content area with iScroll if the Container is set to be auto-scrolled (autoScroll: true).

So after some test and trial iterations, I came up with the following code:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }Ext.override(Ext.Panel, { afterRender: Ext.Panel.prototype.afterRender.createSequence(function() { if (this.getXType() == 'panel') { this._getIScrollElement = function() { return (this.el.child('.x-panel-body', true)); } } //Uncomment below to use iScroll only on mobile devices but use regular scrolling on PCs. if (this.autoScroll /*&& Ext.isMobileDevice*/) { if (this._getIScrollElement) { this._updateIScroll(); this.on('afterlayout', this._updateIScroll); } } }), _ensureIScroll: function() { if (!this.iScroll) { var el = this._getIScrollElement(); if (el.children.length > 0) { this.iScroll = new iScroll(el); this.iScrollTask = new Ext.util.DelayedTask(this._refreshIScroll, this); } } }, _updateIScroll: function() { this._ensureIScroll(); if (this.iScroll) { this.iScrollTask.delay(1000); } }, _refreshIScroll: function() { this.iScroll.refresh(); //Refresh one more time. this.iScrollTask.delay(1000); } }); Ext.override(Ext.tree.TreePanel, { _getIScrollElement: function() { return (this.el.child('.x-panel-body', true)); } }); Ext.override(Ext.grid.GridPanel, { _getIScrollElement: function() { return (this.el.child('.x-grid3-scroller', true)); }, afterRender: Ext.grid.GridPanel.prototype.afterRender.createSequence(function() { //TODO: need to hook into more events and to update iScroll. this.view.on('refresh', this._updateIScroll, this); }) });{/syntaxhighlighter}

It starts with defining afterRender sequence method for an Ext Panel. If the Panel is set to be autoScrolled (autoScroll: true), its scroll element (the main content div housing the Panel's content) is registered with iScroll to make it scrollable on touch devices.

Every derived class of Ext.Panel is expected to provide a method called _getIScrollElement, which should either return the id of the content region for the Panel, or the dom reference for the content region (notice it should be the raw dom reference, not encapsulated in Ext.Element).

If a Panel derived class does not provide the _getIScrollElement method, instances of that class are not registered with iScroll and hence are not "iScrollable". In the above code, Panel, GridPanel and TreePanel classes have been overridden to provide an appropriate return value by adding the _getIScrollElement method to these classes. You can easily override other Panel derived classes as required and return suitable content element that should be "iScrolled".

This was the easy part, the major challenge was deciding when to call refresh method on the iScroll associated to the Panel's content. As you can read in iScroll's documentation, iScroll cannot currently detect changes in html content for the element which has been registered. But refresh method should be called each time a registered element's html changes to enable iScroll to re-calculate the scrollbar dimensions and scrollable content.

In the above code, I have hooked into afterlayout listener to refresh iScroll for Panels. Additionally, GridPanel View's refresh listener is also used to refresh the iScroll for that GridPanel. But this is not enough, you probably need to register additional listeners from which you should call _updateIScroll method to keep the iScroll bars synced with content changes (e.g. you would need to call this in record added/removed listeners for Grids, node added/removed listeners for Trees, resize listeners for all components etc).

I assembled the above code very quickly for this blog post, I think some enhancements can be made to it (e.g. providing a config option preventIScroll, which if true, iScroll is not registered for the component even if autoScroll is set to true). More work needs to be done in finding out a comprehensive set of listeners for each component that should be subscribed to call the _updateIScroll method. The above is "fresh" code that has been just baked. I would try to keep it updated as I improve it for my application based on usability testing. But hopefully you can see the idea how you can leverage iScroll to providing native-feel scrolling on touch devices for your Ext containers without having to change config options for each component independently.

For demonstration purpose, I have left iScroll enabled in the above example on PCs too. But on PCs, you might not find iScroll intuitive. As mentioned, you would be able to test this example better from a touch device (preferably a pad).

 

Comments

Great job on this !  You saved my life with this post. i got it to work but it doesnt work for all grids. 

is there a way to make any grid on the page have an iscroll scrollbar? do you happen to have an example?

I'm not very advanced in ext so I really appreciate your help with this 

Irena

rahul's picture

Hi Irena, thanks for your feedback :)
When I used it, it worked fine for all my grids (since then I have switched to using Sencha Touch instead of ExtJs with iScroll for mobiles). The code itself should be able to scroll all grids without any problem.

So I would not be able to help further unless you can show me a live example somewhere where the code does not work for you.

Hi....

I am doing one app in Sencha touch where I have to scroll PDF file inside the iframe. Do you have any solution for sencha touch scrolling issue...

Thanks
Namrata

rahul's picture

Its not a Sencha Touch scrolling issue Namrata, but iOS one. Unfortunately there's no direct way to work-around that (in my apps where I needed to, I have resorted to custom Google Docs like preview frameworks instead).

Rahul, Thanks for your response. I was able to figure it out... Thanks again for great job with this post - it was very helpful

Irena

rahul's picture

Hi Irena, do you mean you are able to scroll a PDF document inside an iframe using iScroll now?

Could you comment on how to get this working on ExtJS 4?

I'm having problems doing the same thing

rahul's picture

Hi Dan, I think you would continue sequencing Container's afterRender method making necessary api changes consistent with ExtJs 4 changes (e.g. using Ext.Function.createSequence). Unfortunately I do not have time to research myself, so that is all I can say at this point. I can try to help on some specific issue if you face one adapting the code in this blog post for Ext 4.

This seems to work in ExtJS 4, except I don't get the nice looking scrollbars (which I think are webkit extensions.. right?)... I'll be happy if you could help me with this little bit of getting the visual look of the webkit srollbars.

Here's the code at any rate:

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }Ext.override(Ext.panel.Panel, { afterRender: Ext.Function.createSequence(Ext.Panel.prototype.afterRender, function() { if (this.getXType() == 'panel') { this._getIScrollElement = function() { return (this.el.child('.x-panel-body', true)); } } //Uncomment below to use iScroll only on mobile devices but use regular scrolling on PCs. if (this.viewConfig && this.viewConfig.autoScroll && Ext.isMobileDevice) { if (this._getIScrollElement) { this._updateIScroll(); this.on('afterlayout', this._updateIScroll); } } }), _ensureIScroll: function() { if (!this.iScroll) { var el = this._getIScrollElement(); if (el && el.children.length > 0) { this.iScroll = new iScroll(el); this.iScrollTask = new Ext.util.DelayedTask(this._refreshIScroll, this); } } }, _updateIScroll: function() { this._ensureIScroll(); if (this.iScroll) { this.iScrollTask.delay(1000); } }, _refreshIScroll: function() { this.iScroll.refresh(); //Refresh one more time. this.iScrollTask.delay(1000); } }); Ext.override(Ext.tree.Panel, { _getIScrollElement: function() { return (this.el.child('.x-panel-body', true)); } }); Ext.override(Ext.grid.Panel, { _getIScrollElement: function() { return (this.el.child('.x-grid-body', true)); }, afterRender: Ext.Function.createSequence(Ext.grid.Panel.prototype.afterRender, function() { //TODO: need to hook into more events and to update iScroll. if (this.viewConfig && this.viewConfig.autoScroll && Ext.isMobileDevice) { this.view.on('refresh', this._updateIScroll, this); } }) });{/syntaxhighlighter}

rahul's picture

Hi Dan, Nopes they are not webkit scrollbars. Those scrollbars are actually custom <div>s rendered and managed by iScroll. So the best you can do is to get a designer style those scrollbars to imitate web-kit scrollbars as closely as possible.

I tested your code for ExtJs 4, but it doesn't seem to work with the Grid. The grid in extjs4 is rendering the content when it comes into the visible portion of the view. This is different from the old grid in Extjs 3, and general panels.  Did you find a solution for this?

rahul's picture

Hi Svein, I have not used the code with Ext4 and probably won't do in the near term (as mentioned in the comment above, I would instead use Sencha Touch on mobile devices).

But you should be able to get this to work using View/GridPanel events/overrides in Ext4, or in the extreme case, manually calculating bounds for the GridPanel in Ext4.

Actually I was testing the code that Dan posted. It seems to almost work.
By the way Sencha Touch seems to lack a grid, and the Touch demos on Sencha's page was not working great on my Android phone. Seems to me that ST should rather be an extension to ext4 than a supplement.

rahul's picture

Hi Svein, yes Touch does not have a native grid, but you can easily get one by using templates and a DataView (in fact my clients were so happy with the results they did not realize it was not a grid).

You need to understand ST is neither supposed to be an extension or supplement to Ext. It's a stand-alone javascript framework targeted at mobile devices like Ext is primarily targeted at PCs. They share some of the common codebase but apart from that, they are for most purposes independent. Touch 2 is shaping up really nice. You also need to consider Ext4 has been under development for many years but Touch is relatively newer, hopefully it will mature overtime.

hello i tried this with extjs 4.0.7 grid, in an ipad 1 ios 5.1 and it does nothing with scrolling problem.

rahul's picture

Hi Manel, my original code was written for Ext 3, so you might want to take a cue from that and try adapting it for Ext4.

I am using the updated code form Dan Shechter, the post says it is working for 4.x.

rahul's picture

Well things have changed in 4.x lineup, Dan's code might have broken with the later versions of Ext 4.x.

  1. Hi,

  2. I got the following error when running the code with ExtJS 4.0.7. What version of ExtJS is working for this code? 

  3. Thanks!
  4. -Gwowen

  5. Uncaught TypeError: Cannot call method 'substring' of undefined ext-all.js:1879
    1. Ext.ClassManager.parseNamespaceext-all.js:1879
    2. Ext.ClassManager.getext-all.js:1938
    3. Ext.ClassManager.instantiateext-all.js:2028
    4. Ext.ClassManager.instantiateByAliasext-all.js:2024
    5. (anonymous function)ext-all.js:898
    6. Ext.define.statics.createext-all.js:10791
    7. Ext.define.getLayoutext-all.js:14607
    8. Ext.define.doLayoutext-all.js:14611
    9. Ext.define.add
rahul's picture

Hi Gwowen, this is for ExtJs 3.x, won't work with Ext 4.x.

Find a solution to ExtJS4? 

Please show me who came to apply for iScroll ExtJs4.

rahul's picture

Hi Nikolay, I haven't tried using this approach with Ext4, and probably I won't ever. You can try using and adapting code that others have posted above.

hi

Can you please upgrade your demo to allow when u click in something it dynamically load content with scrollbar assigned to iT?

I test and play with it over hours but it not work, if you can fix it for dynamic content it would be great.

wait for your answer

thank you

rahul's picture

Hi Ali, I am too hooked up and won't be able to create the requested sample. However I believe it should be very easy. After you have updated content dynamically to your container (which could be any of Tree/Panel/Grid), please call _refreshIScroll manually on the container to ensure iscroll can calculate new bounds for updated content.

hi, you know what there is 2 problem.

1 problem is that I assign a template to pannel and use .body attribute to update it, but when I debug in template we have an HTML but in body it is an object!

when I chekc content of .body.dom it is a scroller wapper there and in content it is HTML of me!!! but I can't access it

another problem it beacuse of assigning that wrong assignin first time when I load page it work and show scrolls but I think it disturb the structure and after these beacuse template is damaged it not work more and I give this error in fiebug:

that[dir + "ScrollbarWrapper"].parentNode is null 
...t[dir + 'ScrollbarWrapper'].parentNode.removeChild(that[dir + 'ScrollbarWrapper'...

Sorry for my weak english grammar!
I hope we here solve this problem

thank you

rahul's picture

Hi Ali, I am running way short on time to be able to try reproducing and debugging your specific issues. But you need to learn to maintain references to your ExtJs component objects in a way that they are easily accessible wherever you need and then use the same to perform operations on those components. Or if they have unique ids, then use Ext.getCmp and perform operations.

I can only add that I have successfully used this approach in scenarios where component markup was updated multiple times from results returned from Ajax calls (provided I manually refreshed iScroll state on the component as mentioned in my previous comment).

hi again,

thankyou for your help.can we have your manual refresh of iScroll code to see how you fix it!

it help us alot if u bring your code, or a sample code that it or at least how u manually refresh iScroll on component.

your the best.

rahul's picture

Hi Ali, after you have updated the component with content fetched dynamically, please use this on the component:

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }cmp._updateIScroll();{/syntaxhighlighter}

"cmp" is assumed to be your component reference.

Correct me if I'm wrong, but you don't need any special scripts to get scrolling within containers to work. a two-finger swipe will scroll within containers naitvely on an iPad. I have this working on my ExtJS 3 applications with no extra code.

rahul's picture

Hi Adam, yes a two-finger scroll works. But that is way too non-friendly, plus in complex and highly nested layouts, two finger scroll works non-intuitively very often.

iScroll enables a single finger scroll with a native scroll like feel.