タグをクリックすると、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 にすると全体がみえる感じ
クラスとページのレイアウト
クラスとページレイアウトの関連図。
レイアウトコード
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,'<').replace(/>/g,'>');
}
};
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;">📄</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}');">
🏷 ${tag.id}
</div>
`).join('')}
</div>
`;
}
};
</script>
</head>
<body id="CraftRoot">
</body>
</html>