読者です 読者をやめる 読者になる 読者になる

Challenge Java EE !

Java EEを中心に趣味や仕事における開発メモを書いています。

JSFで無限スクロール(Infinite Scroll)

JavaScript JSF PrimeFaces

あけましておめでとうございます。2014年も開発を中心に、自分なりにコツコツと頑張っていこうと思います(^^)

冬休み、やっと少しまとまった時間が取れたので、今さらながらにJavaScriptやjQueryを触っています。
半分遊びですが、今後もWeb系の開発が続くと思うので無駄にはならないだろうと(^^;

あと自分で遊びのWebシステムを作ろうと思っていて(多分Java EE7ベース)、「こんなことやりたい」といったイメージの具体化を少しずつしています。

無限スクロール

で、今やりたいことの1つに無限スクロールがあります。

TwitterやFacebookで画面下にスクロールするとデータが読み込まれるアレです。
最近、動画サービスのHuluに入ったのですが、画像で同じようなことがされていました。

別にJSFにこだわるわけではないのですが、JSFだとどんな風に実現するんだ?と気になって(^^;

イメージとしては以下のようにパネルのコンポーネントが並んでいる画面で下にスクロールすると新しいパネルが表示されていく、というそんな様な画面です。

f:id:kikutaro777:20140101161953p:plain

jQueryだと

jQueryのプラグインだと色々あるみたいです(^^;以下サイトでまとまってました。

jQueryを触り始めて思ったのですが、プラグインがたくさんありすぎて、どれがマッチするのか探すの結構大変そうな気が…(^^;時間があると楽しいの間違いないですが、納期ある仕事だと悩ましそうな。

JSFだと

JSFでも普通にjQueryのプラグインとうまく組み合わせて作ることもできそうです。
実際、以下のサイトはそんな感じでした。

ただ、この例ではPrimeFacesのremoteCommandコンポーネント(クライアントサイドのJSから管理対象ビーン(Backing Bean)のメソッドを呼ぶためのコンポ)を使っているので純粋にjQueryだけでとは言えないのですが(^^;

PrimeFaces ExtensionsのWaypointコンポ

PrimeFacesのデモを何気にみていたら、Extensionsに無限スクロールのサンプルがあって、コンポーネントもありました。(なぜ上記のサンプルではPrimeFaces使ってるのにこれを使わないかは不明です…)

Waypointコンポーネント
http://fractalsoft.net/primeext-showcase-mojarra/views/waypoint.jsf

中身はjQueryのWaypointプラグインを利用してると思われます(サンプルのコード的にも)

で、これを使って簡単に最初のイメージのものを作ってみました。

画面表示時こんな感じでパネルがワラワラと並んでます。
f:id:kikutaro777:20140101172735j:plain

スクロールして画面の最下端に触れるとイベントが発火して「すたーと」ダイアログ表示とともに追加のパネルデータを読み込みます(読み込む前の時点では以下図のようにパネル50まで存在)
f:id:kikutaro777:20140101172831j:plain

パネルの読み込み完了のイベントで「えんど」ダイアログが表示されます。
f:id:kikutaro777:20140101172918j:plain

パネルがちゃんと増えてるー。
f:id:kikutaro777:20140101173040j:plain

そして再度スクロールして画面下に到達すると同じことの繰り返しです。
f:id:kikutaro777:20140101173254j:plain

500個くらいまでいってみた(^^;
f:id:kikutaro777:20140101173325j:plain

無限スクロールできた(^^)

コード

管理対象ビーン(Backing Bean)に画面に表示されるパネルの元データとしてListを定義します。この例ではサンプルを簡単にするためStringにしています。

初期表示時に文字列を50個、リストに入れます。またリストに文字列を追加するメソッドも定義しておきます。

package jp.co.hoge.pfsample.ext.waypoint;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.faces.view.ViewScoped;
import javax.inject.Named;
import lombok.Getter;
import lombok.Setter;

@Named(value = "wayPointPanelBean")
@ViewScoped
public class WayPointPanelBean implements Serializable{

    @Getter @Setter
    private List<String> strList;
    
    private int strListMax = 0;
    
    @PostConstruct
    public void init(){
        //画面表示時に文字列50個をリストに格納
        strList = new ArrayList<>();
        for(int i = 1; i <= 50; i++){
            strList.add(Integer.toString(i));
        }
    }
    
    public void loadMorePanel(){
        //文字列データを10個リストに追加する処理
        strListMax = strList.size();
        for(int i = strListMax + 1; i <= strListMax + 10; i++){
            strList.add(Integer.toString(i));
        }
    }
}

画面側は以下のような感じで、少しややこしい感じです。

<?xml version='1.0' encoding='UTF-8' ?> 
<!DOCTYPE html>
<html lang="ja"
    xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:p="http://primefaces.org/ui"
      xmlns:pe="http://primefaces.org/ui/extensions">
    <h:head>
        <title>Waypoint</title>
    </h:head>
    <h:body>
        <h:form prependId="true">
            <p:dataGrid id="dgPanel" var="pnl" value="#{wayPointPanelBean.strList}">
                <p:panel id="pnl" header="#{pnl}">Name:hogehoge#{pnl}</p:panel>
            </p:dataGrid>
            
            <p:remoteCommand id="remote" name="loadMore" update="dgPanel" process="@this"
                             actionListener="#{wayPointPanelBean.loadMorePanel()}"
                             oncomplete="handleLoadEnd();">
            </p:remoteCommand>

            <pe:waypoint id="way" offset="function(){return $.waypoints('viewportHeight') - $(this).outerHeight()}" widgetVar="widgetWay">
                <pe:javascript event="reached" execute="handleLoadStart(ext);" /> 
            </pe:waypoint>

            <script type="text/javascript">
                function handleLoadStart(ext){
                    if(ext.direction === "down"){
                        alert("すたーと");
                        PF('widgetWay').remove();
                        loadMore();
                    }
                }
                
                function handleLoadEnd(){
                    alert("えんど");
                    PF('widgetWay').register();
                }
            </script>
        </h:form>
    </h:body>
</html>

流れ的には

  1. waypointコンポを定義してスクロールで検知する対象を決めます
  2. 1の条件を満たした場合にキックするJavaScriptをjavascriptコンポで紐づけます
  3. 2で呼び出されるJavaScriptの処理を定義します
  4. 3の中で管理対象ビーン(Backing Bean)のメソッドを呼ぶためにremoteCommandコンポを定義してactionListenerでメソッドを紐づけます

こんな感じです。1では通常、for属性でコンポのIDを指定するか、入れ子にwaypointを定義します。上記ではどちらの定義でもないのですが、forがnullの場合は親に対するものとなるため、上記例はFormが対象となります

JavaScriptの中でwaypointを外したり付けたりしてるのは、データを増やしたタイミングで再度イベント実行されて無限ループされるのを防ぐためです。

offsetが最初いまいちイメージできなくてマゴマゴしたのですが、以下jQuery Waypointsのドキュメントやサンプルが参考になりました。
$.waypoints('viewportHeight')はざっくり言うと内部で表示されている縦幅なんですね。
http://imakewebthings.com/jquery-waypoints/#docs

わかりにくかったので図にしてみました。
上記のwaypointはfor属性でコンポーネント指定していないので、親であるformが指定されています。なので、formのoffsetがマイナス画面でみえない分=最下端となってます。
f:id:kikutaro777:20140101211633j:plain

慣れるとわりと自由に使えそう…かな(^^;

環境

  • Windows 8 Pro 64bit
  • JDK8
  • NetBeans 7.4
  • GlassFish 4.0
  • PrimeFaces 4.0
  • PrimeFaces Extensions 1.1.0
にほんブログ村 IT技術ブログへ
にほんブログ村
にほんブログ村 IT技術ブログ Javaへ
にほんブログ村