0
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Qiitaのタグから記事を表示する小さなSPA

タグをクリックすると、Qiita API v2 /api/v2/tags/:tag_id/items で記事を取得して表示する小さなSPA。Craft-UIKit による簡単な実装例です。

A Pen

See the Pen qiita_small by Ray Kitajima (@raykitajima) on CodePen.

※ 未認証のAPIコール数の上限に直ぐに到達する気がするので下のコードをコピペでローカルで試してみて下さい。
※ Qiita上で browser back-forward も機能するんですね。よくできてる。
※ 0.5x にすると全体がみえる感じ

クラスとページのレイアウト

components.jpg

クラスとページレイアウトの関連図。

レイアウトコード

PageController.js
viewDidLoad(callback){
    this.appendView(new Header());
    this.tags = new Tags({delegate:this});
    this.items = new Items({delegate:this});
    let container = new Container();
    container.loadView();
    container.appendView(this.tags);
    container.appendView(this.items);
    this.appendView(container);
    this.appendView(new Footer());
}

モバイルアプリ風のレイアウトコード。

クラスの概略

クラス 役割
PageController RootViewController / ヒストリーの管理
Header ヘッダー(装飾)
Container TagsとItemsを保持するレイアウト用ラッパー
Tags タグのリスト / 一覧はハードコード
クリックで Items にタグ付けされた記事一覧を表示
Items タグに関連した記事を表示するスペース
Footer フッター(装飾)

中身を触ってみる

PageController は RootViewController なので Context から取得できます。
例えば console から手動でタグをセレクトするのはこんな感じ。

window.Craft.Core.Context.getRootViewController().selectTag('Python')

※ CodePenでは動かないですのでローカルでお試し下さい

コード

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Qiita small</title>
    <script src="https://unpkg.com/@craftkit/craft-uikit/dist/craft-uikit.min.js"></script>

    <script>
        var default_tags = [{"followers_count":49144,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/65275b3d3423ddf27cec968034cca8850e4ca97b/medium.jpg?1490807160","id":"JavaScript","items_count":22217},{"followers_count":41399,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/bcef5ec765c6eb84e5ec3300ce1ff16850bb3e14/medium.jpg?1512821048","id":"Python","items_count":22130},{"followers_count":27643,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/0337fbcbff62fb8fa5d0b8be5c3b47d1115d91fc/medium.jpg?1418548649","id":"Ruby","items_count":18847},{"followers_count":30315,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/1420ca7652bf7c28bdab51be6f5bcd4640df27b1/medium.jpg?1478446216","id":"PHP","items_count":13911},{"followers_count":16569,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/6ae023831e95ee12e287c04656a8d128e6834bce/medium.jpg?1489110713","id":"Rails","items_count":12673},{"followers_count":23188,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/f8da947d9c7f46fd4b061b2df9538615f2a587b6/medium.jpg?1498197309","id":"iOS","items_count":12514},{"followers_count":29890,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/f577d33e0031b68387fcf0168098b070101a48ab/medium.jpg?1475471164","id":"Android","items_count":11001},{"followers_count":5051,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/e4c5b2934424684d712ca939b6d82a4da7722844/medium.jpg?1479386754","id":"AWS","items_count":10679},{"followers_count":32090,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/1bfaf60121121d7dec866c83d4c4453347ec93e2/medium.jpg?1436171387","id":"Java","items_count":10426},{"followers_count":6172,"icon_url":"https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/8924010780db484a83145542a3e49c6c2084ecb7/medium.jpg?1401738498","id":"Swift","items_count":10344}];

        var App = {
            didBootApplication : function(){
                let rootViewController = new PageController();
                Craft.Core.Context.setRootViewController(rootViewController);
                rootViewController.bringup();
            }
        };
        var Util = {
            escape : function(str){
                return str.replace(/</g,'&lt;').replace(/>/g,'&gt;');
            }
        };
        window.onload = function(){
            Craft.Core.Defaults.ALLOW_COMPONENT_SHORTCUT = true;
            Craft.Core.Bootstrap.boot(App);
        };

        class PageController extends Craft.UI.DefaultRootViewController {
            constructor(){
                super();
                this.tags = '';
                this.items = '';
            }
            viewDidLoad(callback){
                this.appendView(new Header());
                this.tags = new Tags({delegate:this});
                this.items = new Items({delegate:this});
                let container = new Container();
                container.loadView();
                container.appendView(this.tags);
                container.appendView(this.items);
                this.appendView(container);
                this.appendView(new Footer());
            }
            resolveRoutingRequest(path,event){
                if( !path ){ path = ''; }
                let match = path.match(/(\w*)/);
                let tag = match[1];
                if( tag ){ this.selectTag(tag,event); }
            }
            selectTag(tag,event){
                if( !event ){
                    // the argument `event` is a popstate event object
                    // you should update history if it is not passed.
                    this.pushState({state:{tag:tag},path:'/#/'+tag});
                }
                document.title = "Tag: "+tag;
                this.items.selectTag(tag);
            }
            style(componentId){
                return `
                    * { box-sizing:border-box; margin:0; padding:0; }
                    .root { display:flex; flex-direction:column; width:75%; margin-left:auto; margin-right:auto; }
                `;
            }
            template(componentId){
                return `<div id="root" class="root"></div>`;
            }
        }
        class Container extends Craft.UI.DefaultViewController {
            style(componentId){
                return `
                    .container { display:flex; flex-direction:row; margin-top:15px; width:100%; }
                `;
            }
            template(componentId){
                return `<div class="container"></div>`;
            }
        }
        class Header extends Craft.UI.View {
            style(componentId){
                return `
                    .header{ margin:0px; color:#54c524; border-width:0px 0px 1px 0px; border-style:solid; }
                `;
            }
            template(componentId){
                return `<h1 class="header">Qiita small</h1>`;
            }
        }
        class Footer extends Craft.UI.View {
            style(componentId){
                return `
                    .footer { border-style:solid; border-width:1px 0px 0px 0px; border-color:#54c524; margin-top:20px; color:#54c524; }
                `;
            }
            template(componentId){
                return `<div class="footer">This is Craft-UIKit example app.</div>`;
            }
        }
        class Items extends Craft.UI.View {
            constructor(options){
                super();
                this.delegate = options.delegate;
                this.items = ''; // current items
                this.itemsCache = {};
            }
            selectTag(tag){
                if( this.itemsCache[tag] ){
                    this.items = this.itemsCache[tag];
                    this.renderView();
                }else{
                    this.fetchItems(tag, () => { 
                        this.items = this.itemsCache[tag];
                        this.renderView();
                    });
                }
            }
            fetchItems(tag,callback){
                fetch(`https://qiita.com/api/v2/tags/${tag}/items?page=1&per_page=10`)
                    .then( res => { return res.json(); } )
                    .then( items => { this.itemsCache[tag]=items; if(callback){ callback(); } });
            }
            style(componentId){
                return `
                    a { text-decoration: none; }
                    :host { width:70%; }
                    .root { display:flex; flex-direction:column; overflow:scroll; }
                    .item { 
                        display:flex; flex-direction:row; 
                        line-height:28px; font-size:16px; cursor:pointer; margin-left:20px;
                        animation:show 0.1s ease-in 0s 1 normal both; }
                    .item:hover { background-color:#eee; }
                    @keyframes show {
                        0% { margin-left:20px; opacity:0; }
                        100% { margin-left:0px; opacity:1; }
                    }
                `;
            }
            template(componentId){
                if( !this.items ){ this.items = []; }
                return `
                    <div id="root" class="root">
                        ${ this.items.map( (item, idx) => `
                            <div class="item" style="animation-delay:${idx*30}ms">
                                <div style="margin-right:10px;">&#x1F4C4;</div>
                                <div><a href="${item.url}" title="${item.url}" target="_new">${Util.escape(item.title)}</a></div>
                            </div>
                        `).join('')}
                    </div>
                `;
            }
        }
        class Tags extends Craft.UI.View {
            constructor(options){
                super();
                this.tags = '';
                this.delegate = options.delegate;
            }
            viewWillAppear(callback){
                this.tags = default_tags;
                if(callback){ callback(); }
            }
            viewDidAppear(callback){
                this.renderView();
                if(callback){ callback(); }
            }
            style(componentId){
                return `
                    :host { all: initial; width:25%; } /* block cascading. font is colored by default black */
                    .root { display:flex; flex-direction:column; width:100%; height:100%; }
                    .tag { width:100%; line-height:32px; font-size:18px; cursor:pointer; }
                    .tag:hover { background-color:#eee; }
                `;
            }
            template(componentId){
                if( !this.tags ){ this.tags = []; }
                return `
                    <div id="root" class="root">
                        ${ this.tags.map( tag => `
                            <div class="tag" onclick="${componentId}.delegate.selectTag('${tag.id}');">
                            &#x1F3F7; ${tag.id}
                            </div>
                        `).join('')}
                    </div>
                `;
            }
        };
    </script>
</head>
<body id="CraftRoot">
</body>
</html>
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
0
Help us understand the problem. What are the problem?