Zowel hobby als werk: Webdevelopment. Zowel front-end als back-end.
Asp, PHP, MySQL, html,css en javascript.

Howto: Infinite scroll in Jquery

Door onok op vrijdag 12 oktober 2012 21:35 - Reacties (13)
Categorie: Webdevelopment, Views: 6.310

Ik heb al een tijdje niet meer geblogd, dus het wordt wel weer een keer tijd :)
Deze keer een blokje in de categorie webdevelopment. Heb je niks met javascript, dan zou ik deze vooral skippen :)

Ik wil een uitleg gaan geven over hoe je op een webpagina een zogeheten "infinite scroll" maakt. Wat bedoel ik daarmee: Stel je hebt een pagina met een hoop content. Bijvoorbeeld veel reacties op een nieuwsitem. Een leuk effect is dan om dit in stukjes in je pagina te laden, ipv. alles in 1x. Dit zorgt ervoor dat je pagina sneller aanvoelt. Bovendien kan het serverload schelen. Bekendste voorbeelden zijn Facebook en Twitter. Tweets en updates worden niet allemaal in 1x geladen, dit zijn er immers veel teveel. Er worden er pakweg 20-50 geladen, en vervolgens als je naar beneden scrollt, wordt de volgende batch (ofwel pagina) geladen en direct eronder getoond.

Er zijn wel plugins voor, maar ik vind het steeds vaker leuk (en vooral leerzaam) om zulk soort dingetjes zelf te maken. In dit geval bleek het zelfs best simpel :)
In deze How-to gaat het voornamelijk om de javascript (JQuery), de achterliggende server(php,asp, whatever) code mag je zelf uitzoeken ;)

Wat gaan we precies maken?
Ga er vanuit dat je een simpel HTML element hebt, waarin je de content laad. Het makkelijkst is een divje.

HTML:
1
<div id="mycontent"></div>


Het werkt het makkelijkst om deze div een vaste hoogte en breedte geven. Belangrijker is om er "overflow: auto;" aan toe te voegen.
In je CSS ziet het er dus zo uit:

Cascading Stylesheet:
1
#mycontent{width: 800px; height: 600px; overflow: auto;}


Ik ga verder niet teveel op de styling in, dit kun je zelf aanpassen aan je wensen. Zelf pas ik de hoogte en breedte met javascript aan zodat dit m'n beeld uitvult. Maar dat is voor dit voorbeeld niet belangrijk.

Normaal vul je deze div met de html, je items / reacties. We gaan er in dit voorbeeld even vanuit dat dit een list is (<ul>) met een hele hoop list-items (<li></li>). Maar het kan het zo goed een lijst met div's, img's of een table zijn.

Deze div gaan we nu met ajax calls vullen. We gaan 10 items per keer laden. Je kunt dit aantal zelf aanpassen. Als je kleine stukjes tekst hebt (Tweets bijvoorbeeld) is een groter aantal beter, maar je kunt het ook met foto's doen, en dan is 5 of 10 wel een mooi aantal. Experimenteer gewoon en kijk wat het beste past bij jouw situatie.

Voordat we naar de javascript gaan, moet je dus eerst aan de serverkant ervoor zorgen dat je van je lijst stukjes kunt laden, ipv. alles in 1x. Ik ga er even vanuit dat er PHP gebruikt wordt.

Aan de serverkant
Ik verwacht dat de meesten wel weten hoe je een lijst met items in meerdere pagina's kunt opdelen, maar voor degenen die het niet weten even een korte uitleg.
Stel, je hebt het volgende stukje php code om de lijst te laden:

PHP:
1
2
3
4
5
6
7
8
9
10
<div id="mycontent"></div><ul>
<?php

$result=mysql_query("SELECT * FROM mytable");
while ($row=mysql_fetch_array($result)){
    echo '<li>'.$row['itemname'].'</li>';
}

?>
</ul></div>


We hebben een querystring variable nodig om te bepalen welke "pagina" er geladen moet worden. Deze noemen we simpelweg 'p'. Deze gaan we straks dus ook in javascript gebruiken.

PHP:
1
$p = intval($_GET['p']);


De PHP functie intval zorgt gelijk voor de input validatie. Ik weet nu 100% zeker dat $p altijd een integer is. Die hoef ik straks dus niet meer te escapen.
Vervolgens kunnen we het stukje LIMIT van de mysql query gaan opbouwen:

PHP:
1
2
$p=$p<1 ? 1 : $p;
$limit=($p*10)-10 . ",10";


Korte uitleg: Als $p kleiner is dan 1, wordt deze 1. Ik begint bij het tellen van pagina's altijd bij 1 en niet bij 0, dus 0 wordt 1. Dat hoeft niet per se, maar vind ik zelf makkelijker. Vervolgens wordt het $limit stukje opgebouwd (start,aantal) wat je aan je mysql query kan plakken. Je kunt hier verder nog een stukje validatie bijmaken dat er voor zorgt dat $p niet groter kan zijn dan het totaal aantal pagina's, maar ga ik verder nu niet op in.
Vervolgens moet je alle HTML code weghalen die je maar 1x nodig hebt en dus niet bij iedere pagina. Het resultaat is als volgt:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
<?php

$p = intval($_GET['p']);
$p=$p<1 ? 1 : $p;
$limit=($p*10)-10 . ",10";

$result=mysql_query("SELECT * FROM mytable LIMIT ".$limit);
while ($row=mysql_fetch_array($result)){
    echo '<li>'.$row['itemname'].'</li>';
}

?>


Met de bovenstaande code worden er steeds maar 10 items geladen. De querystring variabele p bepaalt of dit de eerste 10 items zijn, de 2e 10 items, enz.

Zo, heb ik toch een stuk servercode gedaan. Krijg je cadeau :P

De javascript

Dan nu aan de client kant. We passen onze HTML aan zodat we het <ul> element en het <div> element hebben. Die hebben we immers weggehaald bij het stukje php-code dat aangeroepen wordt tijdens de AJAX calls. De HTML wordt dus:

JavaScript:
1
<div id="mycontent"><ul></ul></div>


Dan komt nu de magie. In Jquery hebben we de functie .Scroll() nodig. Deze wordt bij iedere scroll-actie aangeroepen. Dit kan een muiswiel-scroll actie zijn, maar ook het schuiven met de balken, of javascript-scroll actie werken. In javascript:

code:
1
2
3
$("#mycontent").scroll(function(){
// functie komt hier
});



Ik kwam er echter al snel achter dat het event heel vaak achter elkaar fired - oftewel: de functie wordt heel vaak achter elkaar aangeroepen als je een keer scrollt. En we willen straks niet dat er 10-tallen ajax-requests achter elkaar worden uitgevoerd. Om dit op te lossen gebruiken gaan we een techniek gebruiken die ook wel throttling wordt genoemd. We gebruiken de timer functies van de browser om ervoor te zorgen dat een functie maar 1x wordt uitgevoegd ipv. heel vaak. Dat kan bijvoorbeeld zo:

JavaScript:
1
2
3
4
5
6
7
var mouseScrollTimeout;
$("#mycontent").scroll(function(){
    window.clearTimeout(mouseScrollTimeout);
    mouseScrollTimeout = window.setTimeout(function(){  
        // functie komt hier
    },30);
});


Dit stukje zorgt ervoor dat mijn functie niet vaker dan 30 miliseconden wordt uitgevoerd. Dit getal lijkt laag, maar in dit voorbeeld ga ik zelfs dubbele throttling gebruiken, namelijk ook nog een keer bij de daadwerkelijke ajax-call. De exacte timing is iets waar je zelf mee moet expirimenteren in de browser. De waardes uit het voorbeeld zijn ook de waardes die ik in m'n eigen projectje gebruik.

De volgende stap is kijken wanneer we precies een ajax call moeten doen om meer content te laden. We willen dat immers pas doen als iemand bijna aan de onderkant van de lijst is, en niet bij elke scroll actie. Er zijn meerdere manieren om dit te doen, maar ik doe het als volgt: Ik kijk eerst hoe hoog alle elementen die al in de lijst zitten bij elkaar opgeteld zijn. Vervolgens gebruik ik de JQuery functie .scrollTop() om te kijken hoever er al naar beneden gescrollt is. De hoogte van de elementen bij elkaar optellen gaat als volgt (en ik heb er even een aparte functie voor gemaakt voor de overzichtelijkheid):

JavaScript:
1
2
3
4
5
6
7
function GetListHeight(){
    var h=0;
    $("#mycontent li").each(function(){
        h+=$(this).height();
    });
    return h;
}


Je selecteert alle <li> items, gebruikt de functie .height() om de hoogte in pixels uit te lezen, en je telt ze bij elkaar op voor alle items in de lijst. Simple as that. Je kunt immers niet $("#mycontent").height() gebruiken, omdat hier in ons voorbeeld altijd 600 uitkomt. Als je het op deze manier doet gelden wel een paar voorwaarden: de <li> items moeten allemaal block elementen zijn (display: block;), en ze mogen geen padding of margin hebben. De functie .width() berekend namelijk de hoogte zonder margins en paddings. Omdat je dit waarschijnlijk wel wil hebben kun je 2 dingen doen: Of je stopt nieuwe blok elementen (bijvoorbeeld een div) in elk <li> element, die je vervolgens de juiste paddings en margins geeft (dus dit in je php code doen: <li><div>itemnaam</div></li>), of je past de bovenstaande GetListHeight() functie aan zodat van elk <li>-element de paddings bij het totaal opgeteld worden. De eerste manier is makkelijker.

Goed, we kunnen nu de hoogte van de lijst berekenen, en we weten hoever we naar beneden gescrolld zijn. Door de totale hoogte van de scroll-top af te trekken, weten we hoever we van de onderkant van de lijst afzitten. En dat getal hebben we nodig. Als dit getal onder een bepaalde waarde zit, gaan we de ajax-call doen en meer items toevoegen aan onze lijst.

JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
var mouseScrollTimeout;
$("#mycontent").scroll(function(){
    window.clearTimeout(mouseScrollTimeout);
    mouseScrollTimeout = window.setTimeout(function(){  
        if (GetListHeight()-$("#mycontent").scrollTop()<1500){
            var page=$("#mycontent li").length/10+1;
            $.get("http://mijnurl.nl/index.php?p="+page,function(data){
                $("#mycontent ul").append(data);
            });
        }
    },30);
});


Het getal 1500 is ook weer iets waarmee je moet expirimenteren. Het hangt ook af van de hoogte van je $("#mycontent") element. Verder zie je dat ik op regel 6 bepaal welke pagina er precies moet worden geladen, simpelweg door het aantal <li> items te tellen, dit delen door 10 (we doen immers 10 items per "pagina"), en +1 voor de volgende pagina. De response die je krijgt is als het goed is de html van de volgende 10 items van de lijst, en deze voeg je simpelweg toe aan de lijst met .append().

Er is nog een kleine toevoeging nodig, want met de bovenstaande code krijg je nog steeds veel te veel ajax calls, en ook meerdere calls tegelijk. En vooral dat laatste moeten we niet hebben, want je zult zien dat je dan items dubbel te zien krijgt. Om dit op te lossen gebruik ik een 2e throttle, zoals ik al eerder zei.

JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var loadMoreLoading=0;
var mouseScrollTimeout;
$("#mycontent").scroll(function(){
    window.clearTimeout(mouseScrollTimeout);
    mouseScrollTimeout = window.setTimeout(function(){  
        if (GetListHeight()-$("#mycontent ").scrollTop()<1500){
            if (!loadMoreLoading){
                loadMoreLoading=1;
                var page=$("#mycontent li").length/10+1;
                $.get("http://mijnurl.nl/index.php?p="+page,function(data){
                    $("#mycontent ul").append(data);
                    setTimeout(function(){loadMoreLoading=0;},1000);
                });
            }
        }
    },30);
});


Dit is een beetje dirty code, maar het principe is duidelijk denk ik. Ik gebruik een javascript variabele loadMoreLoading om aan te geven of er een ajax call bezig is. Ook wordt deze variabele pas weer na 1 seconde na een afgeronde ajax-call teruggezet. Het resultaat is dat er geen nieuwe ajax calls gedaan worden als er al 1 bezig is, en dat er niet meer dan 1x per seconde een nieuwe call gedaan mag worden.

In de globale opbouw van je pagina moet je er natuurlijk eerst voor zorgen dat de eerste 10 items worden geladen, anders moet een gebruiker eerst in een leeg vak scrollen :+ maar ik denk dat het idee wel duidelijk is. Je moet er ook nog voor zorgen dat er een detectie is wanneer je aan het einde van de lijst bent (als er dus niet meer items zijn). Maar dat laat ik aan jezelf over ;)

Dit was m'n eerste HOWTO, dus als ik dingen kan verbeteren hoor ik het graag. Als de reacties positief zijn zullen er meer volgen ;)

HTML5 multiple file uploader (flash is kut?)

Door onok op woensdag 13 april 2011 17:32 - Reacties (19)
Categorie: Webdevelopment, Views: 3.548

Ik heb zelf een CMSje dat ik voor meerdere websites gebruik (bijvoorbeeld www.x-life.nl en wollan.nl). In het beheergedeelte zit een file uploader waarmee je in 1 keer meerdere files tegelijk kunt uploaden, namelijk SWFUpload. Dit is een multiple-file uploader die werkt met flash.

Ik weet eigenlijk niet eens welke versie ik momenteel gebruik, maar de uploader is inmiddels weer stuk. Vermoedelijk dus door een flash-update. Het is al de 2e keer dat flash een update heeft uitgerold die het SWF component onbruikbaar maakt. Het componentje wordt gelukkig actief bijgehouden, en er is dan ook weer een nieuwe versie beschikbaar. Maar ik heb geen zin om telkens al m'n sites te updaten, als flash weer eens een update uitrolt die alles stuk maakt.

De oplossing: een HTML5 uploader. Javascript zal nooit met een update komen die webpagina's onklaar maakt. Ja ok, IE kan er wel wat van :P Maar in principe blijft je html, css en javasript code jarenlang bruikbaar.
De bonus is dat m'n CMS straks volledig compatible is met de iPad :P

Ik zal eerst uitleggen waarom je uberhaubt flash nodig hebt/had. Een simpele ajax uploader is vrij makkelijk te maken/te vinden. Maar 2 stukjes functionaliteit waar ik veel waarde aan hecht zijn:
1. Het in 1x kunnen selecteren van meerdere files, en
2. De upload voortgang kunnen bijhouden.
En dit red je niet met html, css en javascript.

Met een standaard <input type="file"> element kun je namelijk maar 1 file tegelijk selecteren en dat is vrij vervelend als je bijvoorbeeld een fotoalbum wilt uploaden. Met behulp van een flash component kon je dit afvangen. Het dialoogje "selecteer file(s)" word dan door flash verzorgd en niet door de browser. Met de opkomst van HTML5 is er op dit gebied een hele hoop functionaliteit bijgekomen. Met het simpele attribuut multiple="true" kun je namelijk wel meerdere files selecteren met je standaard file input. En updates aan de XMLHTTP objecten in de browser zorgen ervoor dat je feedback kunt krijgen over de voortgang van je upload. Vervolgens maak je gewoon met css3 je mooie voortgangs balkjes.

Het hele probleem van HTML5 kan in 2 woorden samengevat worden: Internet Explorer. Versie 8 kan HTML5 namelijk niet eens spellen. Laat staan de oudere versies 7 en 6. Gelukkig is Microsoft een beetje wakker geworden (mag wel na al die jaren, toch?) en hebben ze beperkte! HTML5 ondersteuning toegevoegd aan Internet Explorer 9. IE9 ondersteund dan ook multiple="true". Eindelijk!

In de JQuery plugin directory zijn al een hele boel HTML5 uploaders te vinden. Ik heb echter besloten om er zelf eentje te maken omdat degenen die ik heb gevonden steeds net niet naar m'n zin waren. Alstie volwassen is zal ik m wel posten.

Moraal van dit verhaal: Flash is kudt?