勉強がてら、Django REST FrameworkでAPIを実装し、Angular2でAPIを利用したTodoアプリを作ってみたので、メモ
#環境
- python 3.6.0
- node 7.5.0
- npm 4.1.2
#Django
##Djangoのセットアップ
###必要なライブラリのインストール
$ pip install django
$ pip install djangorestframework
$ pip install django-filter
$ pip install django-cors-headers
###プロジェクトの作成
$ django-admin startproject django_app
###アプリケーションの作成
$ cd django_app
$ python manage.py startapp to_do
###modelの作成
シンプルにtitleと作成日時だけのModelでやっていく
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=140, blank=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
settings.pyにアプリケーションを反映
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'to_do', # 追加
]
###マイグレーション
$ python manage.py makemigrations
$ python manage.py migrate
DBは準備の必要のないsqliteを利用
###adminユーザの作成
$ python manage.py createsuperuser
適当にユーザ名、メールアドレス、パスワードを設定する
###adminサイトのセットアップ
from django.contrib import admin
from .models import Todo
@admin.register(Todo)
class Todo(admin.ModelAdmin):
pass
adminサイトの確認
python manage.py runserver
http://localhost:8000/admin
にアクセス&ログインしてみる
TodosのAddから適当に何件かtodoを登録しておきましょう
##Django REST Frameworkの設定をしてAPIを実装する
###REST Frameworkの読み込み
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'to_do',
'rest_framework', #追記
]
###Serializerの定義
django_app/to_doにserializer.pyを作成する
from rest_framework import serializers
from .models import Todo
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('id', 'title', 'created_at')
###Viewの定義
django_app/to_doのviews.pyを編集
from django.shortcuts import render
import django_filters
from rest_framework import viewsets, filters
from .models import Todo
from .serializer import TodoSerializer
from rest_framework.decorators import api_view
class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all().order_by('-created_at')
serializer_class = TodoSerializer
###URLパターンの定義
from django.conf.urls import url, include //includeを追記
from django.contrib import admin
from to_do.urls import router as to_do_router //追記
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^api/', include(to_do_router.urls)), //追記
]
アプリケーション側のurls.pyは最初は作られてないので作る
from rest_framework import routers
from .views import TodoViewSet
router = routers.DefaultRouter()
router.register(r'todo', TodoViewSet)
###API動作確認
$ python manage.py runserver
http://localhost:8000/api
にアクセス
XMLHTMLの許可設定
用意したAPIにAngularからアクセス可能にするための設定をsettings.pyに書いていく
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'to_do',
'rest_framework',
'corsheaders', //追記
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware', //追記
'corsheaders.middleware.CorsMiddleware', //追記
]
〜中略〜
// 追記
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
#Angular
##Angular CLIのインストール
$ npm install -g @angular/cli
##Angularのセットアップ
###プロジェクトの作成
$ ng new ng2app
ちょっと時間かかる
###build
$ cd ng2app
$ ng build
###動作確認
$ ng serve
http://localhost:4200
にアクセスしてみる
app works!という画面が出ているはず
Todo一覧を表示させる
まずは単純にDjango REST Frameworkで生成されたAPIを取得し、表示させるといった機能を実装する
モデルの定義
/ng2app/src/appにmodelsというフォルダを作成
そのなかにtodo.model.tsというファイルを作成する
export class Todo {
id: number;
title: string;
}
Djangoのmodelではcreated_atを定義していましたが、フロント側では不要なので、idとtitleのみを定義
serviceの作成
/ng2app/src/appにservicesというフォルダを作成
そのなかにtodo.service.tsというファイルを作成
ここではAPIをGETして、データを渡す、といった動きを実装していく
import { Injectable } from "@angular/core";
import { Http, Headers } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Todo } from '../models/todo.model';
@Injectable()
export class TodoService {
todo: Todo[] = [];
private Url = `http://127.0.0.1:8000/api/todo/`
private headers = new Headers({'Content-Type': 'application/json'});
constructor(
private http: Http
){}
// 全てのtodoをGETする
getAllTodo(): Promise<Todo[]> {
return this.http
.get(this.Url)
.toPromise()
.then(response => response.json() as Todo[])
.catch(this.handleError)
}
}
componentの作成
/ng2app/src/appにcomponentsというフォルダを作成
そのなかにtodo-list.component.tsというファイルを作成
import { Component,Input } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { TodoService } from '../services/todo.service';
import { Todo } from '../models/todo.model';
@Component({
selector: 'todo-list',
templateUrl: '../templates/todo-list.component.html',
styleUrls: ['../static/todo-list.component.css']
})
export class TodoListComponent {
todos: Todo[] = [];
constructor(
private todoService: TodoService,
){}
ngOnInit(): void {
this.todoService.getAllTodo()
.then(todos => this.todos = todos);
}
}
htmlの作成
/ng2app/src/appにtemplatesというフォルダを作成
そのなかにtodo-list.component.htmlを作成
<div class="container">
<div class="todo-list row">
<div *ngFor="let todo of todos" class="col-sm-8 col-sm-offset-2">
<div class="panel panel-default">
<div class="panel-body">
<span from="name">{{todo.title}}</span>
</div>
</div>
</div>
</div>
ルーティングの設定
/ng2app/src/appにapp-routing.module.tsを作成
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TodoListComponent } from './components/todo-list.component';
const routes: Routes = [
{ path: '', component: TodoListComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
http://localhost:4200
にアクセスがあったら、TodoListComponentを見にいかせる
app.componentの編集
/ng2app/src/app/app.component.tsを下記の通り編集する
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h1 class="text-center">
<span class="title">{{ title }}</span>
<p class="sub-title">{{ subtitle }}</p>
</h1>
<router-outlet></router-outlet>
`,
styles: [
'.title { color: #ee6e73;}',
'.sub-title { font-size: small; }'
],
})
export class AppComponent {
title = 'Simple Todo';
subtitle = 'Angular2 + Django Rest Framework'
}
各種モジュールの読み込み
/ng2app/src/app/app.module.tsを編集
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TodoListComponent } from './components/todo-list.component';
import { TodoService } from './services/todo.service';
@NgModule({
declarations: [
AppComponent,
TodoListComponent,
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
AppRoutingModule
],
providers: [TodoService],
bootstrap: [AppComponent]
})
export class AppModule { }
cssファイルの配置
/ng2app/src/appにstaticというフォルダを作成
todo-list.component.cssを作成する
とりあえず、下記の通り記載
.todo-list {
padding-top: 10px;
}
.todo {
min-height: 130px;
}
.btn-circle {
width: 30px;
height: 30px;
text-align: center;
padding: 6px 0;
font-size: 12px;
line-height: 1.428571429;
border-radius: 15px;
}
.add-todo {
margin-top: 10px;
}
bootstrapモジュールのインストール
下記を実行
$ npm install --save bootstrap ng2-bootstrap
/ng2app/.angular-cli.json(隠しファイルになっているので注意)のstylesの部分を編集
"styles": [
"styles.css",
"../node_modules/bootstrap/dist/css/bootstrap.css", //追記
],
動作確認
下記コマンドでアプリケーション起動
$ ng serve
http://localhost:4200/
にアクセスすると、下記のような状態になっているはず
(DjangoのAdminからtodoを登録していれば)
Todoを新規作成する機能を追加
serviceの編集
todo.service.tsのTodoService内に下記を追記
// 追加時の挙動
create(todo: Todo): Promise<Todo> {
return this.http
.post(this.Url, JSON.stringify(todo), {headers: this.headers})
.toPromise()
.then(res => res.json())
.catch(this.handleError);
}
// 追加された最新のtodoを一件取得する
getNewTodo(): Promise<Todo> {
return this.http
.get(this.Url+"?limit=1")
.toPromise()
.then(res => res.json().results)
.catch(this.handleError)
}
componentの編集
todo.component.tsのTodoListComponent内に下記を追記
export class TodoListComponent {
todos: Todo[] = [];
newtodos: Todo[] = []; //追記
@Input() todo: Todo = new Todo(); //追記
〜中略〜
// 保存ボタンを押した時の挙動
save(): void {
this.todoService
.create(this.todo)
.then(data => {this.getNewTodo()});
this.todo = new Todo();
}
// 最新の一件を呼び出す挙動
getNewTodo(): void {
this.todoService
.getNewTodo()
.then(res => {this.pushData(res)});
}
// htmlに渡すnewtodosにデータをpushする
pushData(data: Todo): void {
this.newtodos.unshift(data);
}
}
新規todoを追加すると、save()でTodoService内のcreateを実行し、新規TodoをPOST
その後、getNewTodo()でTodoService内のgetNewTodoを実行し、最新の一件(=POSTされたtodo)を呼び出す
呼び出した一件をpushData()でnewtodosに格納、といった動きをさせています
htmlの編集
todo-list.component.htmlを編集
<div class="container">
<!-- ここから -->
<div class="center">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<input [(ngModel)]="todo.title"
id="input_text"
type="text"
length="140"
class="form-control add-todo"
placeholder="add-todo"
(keydown.enter)="save()"
>
<button (click)="save()" class="btn btn-success pull-right add-todo">Add</button>
</div>
</div>
</div>
<div class="newtodo-list row" style="margin-top:10px;">
<div *ngFor="let newtodo of newtodos" class="col-sm-8 col-sm-offset-2">
<div class="panel panel-default">
<div class="panel-body">
<span from="name">{{newtodo[0].title}}</span>
</div>
</div>
</div>
</div>
<hr class="col-sm-8 col-sm-offset-2">
<!-- ここまで -->
<div class="todo-list row">
<div *ngFor="let todo of todos" class="col-sm-8 col-sm-offset-2">
<div class="panel panel-default">
<div class="panel-body">
<span from="name">{{todo.title}}</span>
</div>
</div>
</div>
</div>
動作確認
下記コマンドでアプリケーション起動
$ ng serve
input部分で新規todoを追加ができる
Todoを削除する機能を追加
serviceの編集
todo.service.tsのTodoService内に下記を追記
// 削除時の挙動
delete(id: number): Promise<void> {
const url = `${this.Url}${id}/`;
return this.http
.delete(url, {headers: this.headers})
.toPromise()
.then(() => null)
.catch(this.handleError);
}
componentの編集
todo.component.tsのTodoListComponent内に下記を追記
// 削除ボタンを押した時の挙動
delete(id): void {
this.todoService
.delete(id);
}
htmlの編集
todo-list.component.htmlを編集
<div class="container">
<div class="center">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<input [(ngModel)]="todo.title"
id="input_text"
type="text"
length="140"
class="form-control add-todo"
placeholder="add-todo"
(keydown.enter)="save()"
>
<button (click)="save()" class="btn btn-success pull-right add-todo">Add</button>
</div>
</div>
</div>
<div class="newtodo-list row" style="margin-top:10px;">
<div *ngFor="let newtodo of newtodos" class="col-sm-8 col-sm-offset-2">
<div [style.display]="!newtodo.hideElement ? 'inline':'none'">
<div class="panel panel-default">
<div class="panel-body">
<span from="name">{{newtodo[0].title}}</span>
<button (click)="delete(newtodo[0].id) || newtodo.hideElement=true"
type="button"
class="btn btn-success btn-circle pull-right"
>
<i class="glyphicon glyphicon-ok"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<hr class="col-sm-8 col-sm-offset-2">
<div class="todo-list row">
<div *ngFor="let todo of todos" class="col-sm-8 col-sm-offset-2">
<div [style.display]="!todo.hideElement ? 'inline':'none'">
<div class="panel panel-default">
<div class="panel-body">
<span from="name">{{todo.title}}</span>
<button (click)="delete(todo.id) || todo.hideElement=true"
type="button"
class="btn btn-success
btn-circle pull-right"
>
<i class="glyphicon glyphicon-ok"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
ボタンの追加、ボタンが押されると、deleteメソッドを呼び出す&要素がdisplay=noneとなるようにしています
(ここあんまりイケてない気がするので直したい)
動作確認
下記コマンドでアプリケーション起動
$ ng serve
チェックボタンでtodoを削除ができる
Todoを編集する機能を追加
serviceの編集
todo.service.tsのTodoService内に下記を追記
// 更新時の挙動
update(todo: Todo): Promise<Todo> {
const url = `${this.Url}${todo.id}/`;
return this.http
.put(url, JSON.stringify(todo), {headers: this.headers})
.toPromise()
.then(res => res.json())
.catch(this.handleError);
}
componentの編集
todo.component.tsのTodoListComponent内に下記を追記
// todoを更新した時の挙動
update(id: number, title: string): void {
let todo = {
id: id,
title: title
}
this.todoService.update(todo);
}
(ここもあんまりイケてないので直したい)
htmlの編集
todo-list.component.htmlを編集
<div class="container">
<div class="center">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<input [(ngModel)]="todo.title"
id="input_text"
type="text"
length="140"
class="form-control add-todo"
placeholder="add-todo"
(keydown.enter)="save()"
>
<button (click)="save()" class="btn btn-success pull-right add-todo">Add</button>
</div>
</div>
</div>
<div class="newtodo-list row" style="margin-top:10px;">
<div *ngFor="let newtodo of newtodos" class="col-sm-8 col-sm-offset-2">
<div [style.display]="!newtodo.hideElement ? 'inline':'none'">
<div class="panel panel-default">
<div class="panel-body">
<span *ngIf="!newtodo.isEdit" (click)="newtodo.isEdit=true" from="name">{{newtodo[0].title}}</span>
<input *ngIf="newtodo.isEdit"
(focusout)="newtodo.isEdit=false || update(newtodo[0].id, newtodo[0].title)"
[(ngModel)]="newtodo[0].title"
id="input_text"
type="text"
length="140"
style="border:none; width:70%"
>
<button (click)="delete(newtodo[0].id) || newtodo.hideElement=true"
type="button"
class="btn btn-success btn-circle pull-right"
>
<i class="glyphicon glyphicon-ok"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<hr class="col-sm-8 col-sm-offset-2">
<div class="todo-list row">
<div *ngFor="let todo of todos" class="col-sm-8 col-sm-offset-2">
<div [style.display]="!todo.hideElement ? 'inline':'none'">
<div class="panel panel-default">
<div class="panel-body">
<span *ngIf="!todo.isEdit" (click)="todo.isEdit=true" from="name">{{todo.title}}</span>
<input *ngIf="todo.isEdit"
(focusout)="todo.isEdit=false || update(todo.id, todo.title)"
[(ngModel)]="todo.title"
id="input_text"
type="text"
length="140"
style="border:none; width:70%"
>
<button (click)="delete(todo.id) || todo.hideElement=true"
type="button"
class="btn btn-success
btn-circle pull-right"
>
<i class="glyphicon glyphicon-ok"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
todoの文字列をクリックしたら、input要素に切り替わり、inputからフォーカスが外れたら、updateが実行される
(これもイケてな(ry
動作確認
下記コマンドでアプリケーション起動
$ ng serve
冒頭のgifのような動きをするはず
#以上です
超ビギナーなので、こうしたほうがいいんじゃね?的なことがあったら、ガシガシ指摘してやってください
また、ソースはこちら。Pull-Request歓迎
#今後
とりあえず、このアプリケーションにユーザ認証とかを取り入れてそこら辺をやっていく気持ちがある
#参考にしたもの
[Django REST Frameworkを使って爆速でAPIを実装する - Qiita]
(http://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8)
Rails 5 + Angular2 + TypeScript でTodoアプリを作った。 - DC4