Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2014 - Mallzee

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

### An AngularJS Directive to mimic iOS UITableView to give a fast unlimited length list if items on mobile using ng-repeat

Scrolling on mobile is a pain. Infinite scrolling and large lists are a massive pain! Which is why there is no perfect solution out there, especially in the Angular world. So we developed our own. The core value of the project is to be as simple to utilise as possible while turning long lists of data into seamless, jank free, scrolling lists on mobile. Which in turn means they run shit hot on the desktop.
Scrolling on mobile is a pain. Infinite scrolling and large lists are a massive pain! Which is why there is no perfect solution out there, especially in the Angular world. So we developed our own. The core value of the project is to be as simple to utilise as possible while turning long lists of data into seamless, jank free, scrolling lists on mobile. Which in turn means they run shit hot on the desktop too!

## Demo
http://angular-ui-table-view.mallzee.com
Expand All @@ -20,7 +20,6 @@ Add the required files to your projects `index.html` file

```HTML
<link rel="stylesheet" href="bower_components/angular-ui-table-view/dist/ui-table-view.css" />
<script src="bower_components/iscroll/build/iscroll-probe.js"></script>
<script src="bower_components/angular-ui-table-view/dist/ui-table-view.js"></script>
```

Expand All @@ -42,13 +41,11 @@ Here's some sample markup to turn your list into a super list

# How does it work?

The mlz-ui-table-view directive watches over your big list of items. It creates a subset of the items based the viewport size (You can override the calculated values with a view object). It then injects the correct data into the correct DOM elements, and moves them into position to create the illusion of a stream of items, without killing the performance of your device and or crashing it all together.
The mlz-ui-table-view directive watches over your big list of items. It creates a subset of the items based the viewport size. You can override the calculated values with a view object. It then injects the correct data from your full list into the correct DOM elements and moves them into position to create the illusion of a stream of items. This is required when displaying large lists to avoid killing the performance of your app, or crashing it all together.

# Why is this different

Currently, this is based off iScroll 5, for now. This has the ability to inject the missing scroll events on mobile devices on momentum scroll. We then use this information to juggle DOM elements rather than deleting and recreating like some other solutions.

DOM elements are limited to the buffer size and are only ever destroyed or created after initialisation when the list becomes smaller than the buffer size, or grows towards the buffer limit. This is what makes the list highly performant. They are moved into the correct place in the list ,using 3d transforms, based on the item index and scroll position and are injected with the correct information from the larger array.
A lot of solutions out there rely on keeping DOM elements to a minimum, but create and destroy them as is necessary, which is expensive. Here, DOM elements are limited to the buffer size and are only ever destroyed or created after initialisation when the list becomes smaller than the buffer size, or grows towards the buffer limit. This is what makes the list highly performant. They are moved into the correct place in the list using 3d transforms based on the item index and elements scope is injected with the correct information from the larger array.

# Attributes

Expand Down
4 changes: 4 additions & 0 deletions bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"test",
"tests"
],
"main": [
"dist/ui-table-view.js",
"dist/ui-table-view.css"
],
"dependencies": {
"angular": "~1.2.0",
"angular-animate": "~1.2.0",
Expand Down
18 changes: 18 additions & 0 deletions dist/ui-table-view.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,21 @@ mlz-ui-table-view {
mlz-ui-table-view .mlz-ui-table-view-wrapper {
position: relative;
}
/* Enable scrollbars on Android (optional) */
::-webkit-scrollbar {
border-left: 1px solid #e5e5e5;
height: 0;
overflow: visible;
width: 5px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
background-clip: padding-box;
min-height: 28px;
border-width: 1px 1px 1px 6px;
width: 5px;
}
::-webkit-scrollbar-corner {
background: transparent;
overflow: visible;
}
16 changes: 12 additions & 4 deletions dist/ui-table-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,8 @@
if (buffer.elements[p]) {
// Scan the buffer for this item. If it exists we should move that item into this
// position and send this block to the bottom to be reused.
for(var k = i; k < buffer.size; k++) {
if (found) {
for(var k = p; k < buffer.size; k++) {
if (buffer.elements[k] && found) {
// Update positions of everything else in the buffer
buffer.elements[k].scope.$coords = { x:x, y:y }
}
Expand Down Expand Up @@ -804,7 +804,10 @@
function setupElement (element) {
var el = getItemElement(element.clone);
el.css({
'webkitTransform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)',
'-webkit-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)',
'-moz-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)',
'-ms-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)',
transform: 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)',
position: 'absolute',
height: element.scope.$height + 'px'
});
Expand All @@ -817,7 +820,12 @@
*/
function repositionElement (element) {
var el = getItemElement(element.clone);
el.css('-webkit-transform', 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)')
el.css({
'-webkit-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)',
'-moz-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)',
'-ms-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)',
'transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)'
});
}

/**
Expand Down
2 changes: 1 addition & 1 deletion dist/ui-table-view.min.js

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions less/ui-table-view.less
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,21 @@ mlz-ui-table-view {
}
}

/* Enable scrollbars on Android (optional) */
::-webkit-scrollbar {
border-left: 1px solid rgb(229, 229, 229);
height: 0;
overflow: visible;
width: 5px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, .2);
background-clip: padding-box;
min-height: 28px;
border-width: 1px 1px 1px 6px;
width: 5px;
}
::-webkit-scrollbar-corner {
background: transparent;
overflow: visible;
}
111 changes: 68 additions & 43 deletions src/ui-table-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,16 @@
SCROLL_DOWN = 'down',
TRIGGER_DISTANCE = 1;

var id = 1, list = [], items = [], itemName = 'item',
var id = 1, list = [], items = [], itemName = 'item', posObj = {},

model = {

},
// Model the main container for the view table
// Model the main container for the view table
container = {
height: 0,
width: 0,
el: undefined
},

// Model the wrapper that will hold the buffer and be scrolled by the container
// Model the wrapper that will hold the buffer and be scrolled by the container
wrapper = {
height: 0,
width: 0,
Expand All @@ -117,7 +114,7 @@

elements,

// Model a row
// Model a row
row = {
height: ROW_HEIGHT,
width: ROW_WIDTH
Expand All @@ -129,8 +126,8 @@
distance: TRIGGER_DISTANCE
},

// Information about the scroll status
// _ indicates the value of that item on the previous tick
// Information about the scroll status
// _ indicates the value of that item on the previous tick
scroll = {
// X-Axis
x: 0, //TODO: Support x axis
Expand Down Expand Up @@ -165,16 +162,6 @@
},
_scroll, // Previous tick data

metadata = {
$$position: 0,
$$visible: true,
$$coords: {
x: 0,
y: 0
},
$$height: 0
},

view = {
top: 0,
bottom: 0,
Expand Down Expand Up @@ -279,6 +266,7 @@

if (attributes.viewParams) {
scope.$watch(attributes.viewParams, function (view) {
id = view.listId || 1;
row.height = view.rowHeight || ROW_HEIGHT;
columns = view.columns || COLUMNS;
buffer.rows = view.rows || BUFFER_ROWS;
Expand All @@ -287,6 +275,14 @@
}, true);
}

/**
* Expose resetPosition function to the controller scope
* TODO: should accept an id parameter to be able to reset the position for different lists
*/
scope.resetPosition = function () {
resetPosition();
}

// Setup trigger functions for the directive
if (attributes.triggerTop) {
triggerTop = function () {
Expand Down Expand Up @@ -399,7 +395,7 @@

// If we have an element cached and it contains the same info, leave it as it is.
if (elements[p] && angular.equals(list[e], elements[p].scope[itemName])) {
elements[p].scope.$coords = { x:x, y:y }
elements[p].scope.$coords = { x:x, y:y };
//$animate.move(elements[p].clone, wrapper.el);
repositionElement(elements[p]);

Expand All @@ -409,8 +405,8 @@
if (elements[p]) {
// Scan the buffer for this item. If it exists we should move that item into this
// position and send this block to the bottom to be reused.
for(var k = i; k < buffer.size; k++) {
if (found) {
for(var k = p; k < buffer.size; k++) {
if (elements[k] && found) {
// Update positions of everything else in the buffer
elements[k].scope.$coords = { x:x, y:y }
}
Expand All @@ -422,7 +418,7 @@
// and move them to the end.
//elements.join(elements.slice(p, k - p));
// Move the found element into the correct place in the buffer elements array
move(buffer.elements, k, p);
move(elements, k, p);
//$animate.move(buffer.elements[p].clone, wrapper.el);
//repositionElement(buffer.elements[p]);
//repositionElement(buffer.elements[k]);
Expand Down Expand Up @@ -568,7 +564,7 @@
function isRenderRequired () {
return(
((scroll.direction === SCROLL_UP && buffer.atEdge !== EDGE_TOP && view.deadZone === false) && (scroll.directionChange || view.ytChange)) ||
((scroll.direction === SCROLL_DOWN && buffer.atEdge !== EDGE_BOTTOM && view.deadZone === false) && (scroll.directionChange || view.ytChange)) || !(view.deadZone !== false && view.deadZoneChange === false)
((scroll.direction === SCROLL_DOWN && buffer.atEdge !== EDGE_BOTTOM && view.deadZone === false) && (scroll.directionChange || view.ytChange)) || !(view.deadZone !== false && view.deadZoneChange === false)
);
}

Expand Down Expand Up @@ -681,7 +677,7 @@
/**
* Perform the scrolling up action by updating the required elements
* @param start
* @param end
* @param distance
*/
function scrollingUp (start, distance) {

Expand Down Expand Up @@ -804,9 +800,14 @@


function setupElement (element) {
var el = getItemElement(element.clone);
var el = getItemElement(element.clone),
transFunc = 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)';

el.css({
'webkitTransform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)',
'-webkit-transform': transFunc,
'-moz-transform': transFunc,
'-ms-transform': transFunc,
transform: transFunc,
position: 'absolute',
height: element.scope.$height + 'px'
});
Expand All @@ -818,8 +819,15 @@
* @param y
*/
function repositionElement (element) {
var el = getItemElement(element.clone);
el.css('-webkit-transform', 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)')
var el = getItemElement(element.clone),
transFunc = 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)';

el.css({
'-webkit-transform': transFunc,
'-moz-transform': transFunc,
'-ms-transform': transFunc,
'transform': transFunc
});
}

/**
Expand Down Expand Up @@ -898,26 +906,42 @@
}
}

function restorePosition() {
if ($window.localStorage.getItem('mlzUITableView.' + id + '.scroll')) {
scroll = JSON.parse($window.localStorage.getItem('mlzUITableView.' + id + '.scroll'));
}
if ($window.localStorage.getItem('mlzUITableView.' + id + '.view')) {
view = JSON.parse($window.localStorage.getItem('mlzUITableView.' + id + '.view'));
}
if ($window.localStorage.getItem('mlzUITableView.' + id + '.buffer')) {
buffer = JSON.parse($window.localStorage.getItem('mlzUITableView.' + id + '.buffer'));
/**
* Check localstorage for a valid scroll-position to restore
*/
function restorePosition () {
var posItem = $window.localStorage.getItem('mlzUITableView.' + id);

if (posItem) {
//TODO: Leaving the view without scrolling saves an empty object which should never be saved
if (posItem !== '{}') {
posObj = JSON.parse(posItem);
scroll = posObj.scroll;
view = posObj.view;
buffer = posObj.buffer;
}

resetPosition();
}
console.log('Restoring', scroll.y);

setupNextTick();
//setScrollPosition(scroll.y);

setScrollPosition(scroll.y);
container.el.prop('scrollTop', scroll.y);
}

function savePosition () {
$window.localStorage.setItem('mlzUITableView.' + id + '.scroll', JSON.stringify(scroll));
$window.localStorage.setItem('mlzUITableView.' + id + '.view', JSON.stringify(view));
$window.localStorage.setItem('mlzUITableView.' + id + '.buffer', JSON.stringify(buffer));
posObj = {
scroll: scroll,
view: view,
buffer: buffer
};
}

function resetPosition () {
if ($window.localStorage.getItem('mlzUITableView.' + id)) {
$window.localStorage.removeItem('mlzUITableView.' + id);
}
}

function cleanup () {
Expand All @@ -932,6 +956,7 @@
}

scope.$on('$destroy', function () {
$window.localStorage.setItem('mlzUITableView.' + id, JSON.stringify(posObj));
cleanup();
});
}
Expand Down
Loading