ç®æ¬¡ïŒåŒ·èª¿è¡šç€ºãããŠããçŸåšã®è³æïŒ æ°ããããŒãããªã¹ãããŠäœæããŸã
ãªãªãžãã«
çŸæç¹ã§ã¯ããŠãŒã¶ãŒç»é²ãšèªèšŒç®¡çã®ãã¹ãŠã®éèŠãªåŽé¢ãå®è£
ãããœã±ãããžã®æ¥ç¶ãšãã£ãã«ã®å
¥åãè¡ã£ãŠããããã次ã®ã¬ãã«ã«é²ã¿ããŠãŒã¶ãŒã«ãªã¹ãã衚瀺ããŠç¬èªã®ããŒããäœæããæ©äŒãäžããŸãã
ãã¿ãã¬ã®äžã«éåžžã«é·ããªã¹ããé ããŸãã-çŽ ç¿»èš³è
ããŒãã¢ãã«ã®ç§»è¡
ãŸãã移è¡ãšã¢ãã«ãäœæããå¿
èŠããããŸãã ãããè¡ãã«ã¯ãåã«æ¬¡ãå®è¡ããŸãïŒ
$ mix phoenix.gen.model Board boards user_id:references:users name:string
ããã«ããã次ã®ãããªæ°ãã移è¡ãã¡ã€ã«ãäœæãããŸãã
# priv/repo/migrations/20151224093233_create_board.exs defmodule PhoenixTrello.Repo.Migrations.CreateBoard do use Ecto.Migration def change do create table(:boards) do add :name, :string, null: false add :user_id, references(:users, on_delete: :delete_all), null: false timestamps end create index(:boards, [:user_id]) end end
id
ãã£ãŒã«ããštimestamps
ãã£ãŒã«ãã«å ããŠã boards
ãšããååã®æ°ããããŒãã«ãåä¿¡ããŸãïŒ å®éãåŸè
ã¯ã察å¿ããããŒã¿ããŒã¹ã®datetime
åã«é¡äŒŒããåã§ã inserted_at
ãšcreated_at
ãã£ãŒã«ãã®ãã¢ãäœæããããã®ãã¯ãã§ã ïŒ;ããŒãã«ã®name
ãã£ãŒã«ããšå€éšããŒusers
åé€ãããå ŽåããŠãŒã¶ãŒã«é¢é£ããããŒãã®ãªã¹ããã¯ãªã¢ããããã«ããŒã¿ããŒã¹ã«äŸåããŠããããšã«æ³šæããŠãã ããã 移è¡ãã¡ã€ã«ãé«éåããããã«ã user_id
ãã£ãŒã«ãã«ã€ã³ããã¯ã¹ãè¿œå ãã name
ãã£ãŒã«ãã«null
å¶éãè¿œå ããŸããã
移è¡ãã¡ã€ã«ã®å€æŽãå®äºãããã次ãå®è¡ããå¿
èŠããããŸãã
$ mix ecto.migrate
ããŒãã¢ãã«
board
ã¢ãã«ãèŠãŠãã ããã
# web/models/board.ex defmodule PhoenixTrello.Board do use PhoenixTrello.Web, :model alias __MODULE__ @derive {Poison.Encoder, only: [:id, :name, :user]} schema "boards" do field :name, :string belongs_to :user, User timestamps end @required_fields ~w(name user_id) @optional_fields ~w() @doc """ Creates a changeset based on the `model` and `params`. If no params are provided, an invalid changeset is returned with no validation performed. """ def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields)) end end
ã泚æ 翻蚳è
ãã¹ãŠã®æ¶é²å£«ã«ãšã£ãŠãã¢ãã«ã¯ãããã«ç°ãªãæ¹æ³ã§çæãããã®ã§ãã³ãŒãã1ã€ãã€ã³ããŒããã®ã§ã¯ãªããçæãããã¢ãã«ã«å€æŽãå ããããšããå§ãããŸãã
ãã ããèšåãã䟡å€ã®ãããã®ããããŸããã User
ã¢ãã«ãæŽæ°ããŠãç¬èªã®ããŒããžã®ãªã³ã¯ãè¿œå ããå¿
èŠããããŸãã
# web/models/user.ex defmodule PhoenixTrello.User do use PhoenixTrello.Web, :model # ... schema "users" do # ... has_many :owned_boards, PhoenixTrello.Board # ... end # ... end
ãªããŸãã«owned_boards
ïŒç¬èªã®ããŒãïŒãªã®ã ãŠãŒã¶ãŒãäœæããããŒãããä»ã®ãŠãŒã¶ãŒãè¿œå ããããŒããšåºå¥ããããã çŸæç¹ã§ã¯ããã«ã€ããŠå¿é
ããå¿
èŠã¯ãããŸããããã®åé¡ã«ã€ããŠã¯åŸã§è©³ãã説æããŸãã
ããŒãã³ã³ãããŒã©ãŒã³ã³ãããŒã©ãŒ
ãã®ãããæ°ããããŒããäœæããã«ã¯ãã«ãŒããã¡ã€ã«ãæŽæ°ããŠããªã¯ãšã¹ããåŠçããããã®é©åãªã¬ã³ãŒããè¿œå ããå¿
èŠããããŸãã
# web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router # ... scope "/api", PhoenixTrello do # ... scope "/v1" do # ... resources "boards", BoardController, only: [:index, :create] end end # ... end
BoardController
ãªãœãŒã¹ãè¿œå ããã¢ã¯ã·ã§ã³ãã³ãã©ãŒã:index
ããã³:create
ãªã¹ãã«å¶éããŠã BoardController
ã次ã®ãªã¯ãšã¹ããåŠçã§ããããã«ããŸããã
$ mix phoenix.routes board_path GET /api/v1/boards PhoenixTrello.BoardController :index board_path POST /api/v1/boards PhoenixTrello.BoardController :create
æ°ããã³ã³ãããŒã©ãŒãäœæããŸãã
web /ã³ã³ãããŒã©ãŒ/ board_controller.ex # web/controllers/board_controller.ex defmodule PhoenixTrello.BoardController do use PhoenixTrello.Web, :controller plug Guardian.Plug.EnsureAuthenticated, handler: PhoenixTrello.SessionController alias PhoenixTrello.{Repo, Board} def index(conn, _params) do current_user = Guardian.Plug.current_resource(conn) owned_boards = current_user |> assoc(:owned_boards) |> Board.preload_all |> Repo.all render(conn, "index.json", owned_boards: owned_boards) end def create(conn, %{"board" => board_params}) do current_user = Guardian.Plug.current_resource(conn) changeset = current_user |> build_assoc(:owned_boards) |> Board.changeset(board_params) case Repo.insert(changeset) do {:ok, board} -> conn |> put_status(:created) |> render("show.json", board: board ) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> render("error.json", changeset: changeset) end end end
GuardianããEnsureAuthenticated
ãè¿œå ããŠããããããã®ã³ã³ãããŒã©ãŒã§ã¯èªèšŒãããæ¥ç¶ã®ã¿ãèš±å¯ãããããšã«æ³šæããŠãã ããã index
ãã³ãã©ãŒã§ã¯ãçŸåšã®ãŠãŒã¶ãŒããŒã¿ãæ¥ç¶ããååŸãã BoardView
ã䜿çšããŠãããã衚瀺ã§ããããã«ãããŒã¿ããŒã¹ã«åœŒã«å±ããããŒãã®ãªã¹ããèŠæ±ããŸãã create
ãã³ãã©ãŒã§ã¯ãã»ãŒåãããšãèµ·ãããŸããçŸåšã®ãŠãŒã¶ãŒã®ããŒã¿ã䜿çšããŠã owned_board
å€æŽã»ããïŒchangesetïŒãäœæããããŒã¿ããŒã¹ã«è¿œå ããŸãã
BoardsView
äœæããŸãã
# web/views/board_view.ex defmodule PhoenixTrello.BoardView do use PhoenixTrello.Web, :view def render("index.json", %{owned_boards: owned_boards}) do %{owned_boards: owned_boards} end def render("show.json", %{board: board}) do board end def render("error.json", %{changeset: changeset}) do errors = Enum.map(changeset.errors, fn {field, detail} -> %{} |> Map.put(field, detail) end) %{ errors: errors } end end
Reactãã¥ãŒã³ã³ããŒãã³ã
ããã¯ãšã³ããããŒãã®ãªã¹ãã®ãªã¯ãšã¹ããšãã®äœæãåŠçããæºåãã§ããã®ã§ãä»åºŠã¯ããã³ããšã³ãã«çŠç¹ãåãããŸãã ãŠãŒã¶ãŒãèªèšŒããŠã¢ããªã±ãŒã·ã§ã³ãå
¥åããåŸãæåã«å¿
èŠãªã®ã¯ããã®ããŒãã®ãªã¹ããšæ°ããããŒããè¿œå ããããã®ãã©ãŒã ã衚瀺ããããšã§ãããããã£ãŠã HomeIndexView
äœæããŸãããã
Homeindexview // web/static/js/views/home/index.js import React from 'react'; import { connect } from 'react-redux'; import classnames from 'classnames'; import { setDocumentTitle } from '../../utils'; import Actions from '../../actions/boards'; import BoardCard from '../../components/boards/card'; import BoardForm from '../../components/boards/form'; class HomeIndexView extends React.Component { componentDidMount() { setDocumentTitle('Boards'); const { dispatch } = this.props; dispatch(Actions.fetchBoards()); } _renderOwnedBoards() { const { fetching } = this.props; let content = false; const iconClasses = classnames({ fa: true, 'fa-user': !fetching, 'fa-spinner': fetching, 'fa-spin': fetching, }); if (!fetching) { content = ( <div className="boards-wrapper"> {::this._renderBoards(this.props.ownedBoards)} {::this._renderAddNewBoard()} </div> ); } return ( <section> <header className="view-header"> <h3><i className={iconClasses} /> My boards</h3> </header> {content} </section> ); } _renderBoards(boards) { return boards.map((board) => { return <BoardCard key={board.id} dispatch={this.props.dispatch} {...board} />; }); } _renderAddNewBoard() { let { showForm, dispatch, formErrors } = this.props; if (!showForm) return this._renderAddButton(); return ( <BoardForm dispatch={dispatch} errors={formErrors} onCancelClick={::this._handleCancelClick}/> ); } _renderAddButton() { return ( <div className="board add-new" onClick={::this._handleAddNewClick}> <div className="inner"> <a id="add_new_board">Add new board...</a> </div> </div> ); } _handleAddNewClick() { let { dispatch } = this.props; dispatch(Actions.showForm(true)); } _handleCancelClick() { this.props.dispatch(Actions.showForm(false)); } render() { return ( <div className="view-container boards index"> {::this._renderOwnedBoards()} </div> ); } } const mapStateToProps = (state) => ( state.boards ); export default connect(mapStateToProps)(HomeIndexView);
å€ãã®ããšãè¡ãããŠããã®ã§ã次ãèŠãŠã¿ãŸãããã
- ãŸãããã®ã³ã³ããŒãã³ãã¯ã¹ãã¢ã«æ¥ç¶ãããŠãããå€æŽã®å Žåã
boards
ã³ã³ããŒã¿ãŒã䜿çšããŠãã©ã¡ãŒã¿ãŒïŒ props
ïŒãåãåãããšã«æ³šæããŠãã ããã - æ¥ç¶ããããšãã³ã³ããŒãã³ãã¯ããã¥ã¡ã³ãã®ã¿ã€ãã«ãBoardsã«å€æŽããã¢ã¯ã·ã§ã³ãã¶ã€ããŒã«ããã¯ãšã³ãããããŒãã®ãªã¹ããååŸããããã«äŸé ŒããŸãã
- ãããŸã§ã®ãšããã
owned_boards
é
åã®è¡šç€ºã®ã¿ãowned_boards
ã BoardForm
ã³ã³ããŒãã³ããBoardForm
ã§ãã - ãããã®2ã€ã®èŠçŽ ã衚瀺ããåã«ã
fetching
ããããã£ãtrueã«èšå®ãããŠãããã©ããããã§ãã¯ãããŸã ã ãã®å Žåãããã¯ãªã¹ãããŸã ããŠã³ããŒãäžã§ããããšãæå³ãããããããŠã³ããŒãã€ã³ãžã±ãŒã¿ãŒã衚瀺ãããŸãã ãã以å€ã®å Žåã¯ãããŒãã®ãªã¹ããšæ°ããããŒããè¿œå ããããã®ãã¿ã³ã衚瀺ãããŸãã - [ æ°èŠè¿œå ]ãã¿ã³ãã¯ãªãã¯ãããšããã®ãã¿ã³ãé衚瀺ã«ããŠãã©ãŒã ã衚瀺ããæ°ããã¢ã¯ã·ã§ã³ã³ã³ã¹ãã©ã¯ã¿ãŒãèŠæ±ãããŸãã
次ã«ã BoardForm
ã³ã³ããŒãã³ããè¿œå ããŸãã
ããŒããã©ãŒã // web/static/js/components/boards/form.js import React, { PropTypes } from 'react'; import PageClick from 'react-page-click'; import Actions from '../../actions/boards'; import {renderErrorsFor} from '../../utils'; export default class BoardForm extends React.Component { componentDidMount() { this.refs.name.focus(); } _handleSubmit(e) { e.preventDefault(); const { dispatch } = this.props; const { name } = this.refs; const data = { name: name.value, }; dispatch(Actions.create(data)); } _handleCancelClick(e) { e.preventDefault(); this.props.onCancelClick(); } render() { const { errors } = this.props; return ( <PageClick onClick={::this._handleCancelClick}> <div className="board form"> <div className="inner"> <h4>New board</h4> <form id="new_board_form" onSubmit={::this._handleSubmit}> <input ref="name" id="board_name" type="text" placeholder="Board name" required="true"/> {renderErrorsFor(errors, 'name')} <button type="submit">Create board</button> or <a href="#" onClick={::this._handleCancelClick}>cancel</a> </form> </div> </div> </PageClick> ); } }
ãã®ã³ã³ããŒãã³ãã¯éåžžã«ã·ã³ãã«ã§ãã ãã©ãŒã ã衚瀺ããéä¿¡ããããšãæå®ãããååã§æ°ããããŒããäœæããããã¢ã¯ã·ã§ã³ãã¶ã€ããŒã«èŠæ±ããŸãã PageClick
ã¯ãã³ã³ããèŠçŽ ã®å€åŽã®ããŒãžã®ã¯ãªãã¯ã远跡ããå€éšã³ã³ããŒãã³ãã§ãã ãã®å Žåããã©ãŒã ãé衚瀺ã«ããŠãã æ°èŠè¿œå ããã¿ã³ãå床衚瀺ããããã«äœ¿çšããŸãã
ã¢ã¯ã·ã§ã³ã³ã³ã¹ãã©ã¯ã¿ãŒ
ã¢ã¯ã·ã§ã³ã¯ãªãšãŒã¿ãŒ // web/static/js/actions/boards.js import Constants from '../constants'; import { routeActions } from 'react-router-redux'; import { httpGet, httpPost } from '../utils'; import CurrentBoardActions from './current_board'; const Actions = { fetchBoards: () => { return dispatch => { dispatch({ type: Constants.BOARDS_FETCHING }); httpGet('/api/v1/boards') .then((data) => { dispatch({ type: Constants.BOARDS_RECEIVED, ownedBoards: data.owned_boards }); }); }; }, showForm: (show) => { return dispatch => { dispatch({ type: Constants.BOARDS_SHOW_FORM, show: show, }); }; }, create: (data) => { return dispatch => { httpPost('/api/v1/boards', { board: data }) .then((data) => { dispatch({ type: Constants.BOARDS_NEW_BOARD_CREATED, board: data, }); dispatch(routeActions.push(`/boards/${data.id}`)); }) .catch((error) => { error.response.json() .then((json) => { dispatch({ type: Constants.BOARDS_CREATE_ERROR, errors: json.errors, }); }); }); }; }, }; export default Actions;
fetchBoards
ïŒæåã«ãã¿ã€ãBOARDS_FETCHING
ã¢ã¯ã·ã§ã³ãçºè¡ããåè¿°ã®ããŠã³ããŒãã€ã³ãžã±ãŒã¿ãŒã衚瀺ããŸãã ãŸããhttpãªã¯ãšã¹ããããã¯ãšã³ãã«éä¿¡ããŠããŠãŒã¶ãŒãææããããŒãã®ãªã¹ããååŸããŸããããã¯BoardController:index
ã䜿çšããŠåŠçããBoardController:index
ã å¿çãåä¿¡ãããšãããŒãã¯ã¹ãã¢ã«ãªãã€ã¬ã¯ããããŸããshowForm
ïŒãã®ã³ã³ã¹ãã©ã¯ã¿ãŒã¯éåžžã«ã·ã³ãã«ã§ããã©ãŒã ã衚瀺ãããã©ããã瀺ãBOARDS_SHOW_FORM
ã¢ã¯ã·ã§ã³ãèšå®ããŸããcreate
ïŒæ°ããããŒããäœæããããã«POST
ãªã¯ãšã¹ããéä¿¡ããŸãã çµæãæ£ã®å ŽåãäœæãããããŒãã«é¢ããããŒã¿ãšãšãã«ã¢ã¯ã·ã§ã³BOARDS_NEW_BOARD_CREATED
ãæ瀺ãããã¹ãã¢å
ã®ããŒãã«è¿œå ãããŸããããŒãã®å
容ã衚瀺ãããšã察å¿ããã«ãŒãã«æ²¿ã£ãŠãŠãŒã¶ãŒããªãã€ã¬ã¯ããããŸãã ãšã©ãŒãçºçããå Žåãã¢ã¯ã·ã§ã³BOARDS_CREATE_ERROR
ãéä¿¡ãããŸãã
å€æåš
ããºã«ã®æåŸã®ããŒã¹ã¯ãéåžžã«ã·ã³ãã«ãªã³ã³ããŒã¿ãŒã§ãã
web / static / js / reducers / boards.js // web/static/js/reducers/boards.js import Constants from '../constants'; const initialState = { ownedBoards: [], showForm: false, formErrors: null, fetching: true, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { case Constants.BOARDS_FETCHING: return { ...state, fetching: true }; case Constants.BOARDS_RECEIVED: return { ...state, ownedBoards: action.ownedBoards, fetching: false }; case Constants.BOARDS_SHOW_FORM: return { ...state, showForm: action.show }; case Constants.BOARDS_CREATE_ERROR: return { ...state, formErrors: action.errors }; case Constants.BOARDS_NEW_BOARD_CREATED: const { ownedBoards } = state; return { ...state, ownedBoards: [action.board].concat(ownedBoards) }; default: return state; } }
ããŒãã®ããŒããå®äºãããã fetching
å±æ§ãfalseã«èšå®ãconcat
äœæããæ°ããããŒããæ¢åã®ããŒããšconcat
æ¹æ³ã«æ³šæããŠãã ããã
ä»æ¥ã¯ããã§ååã§ãïŒ æ¬¡ã®éšåã§ã¯ãããŒãã®å
容ã衚瀺ãããã¬ãŒã³ããŒã·ã§ã³ãäœæããããŒãã«æ°ããåå è
ãè¿œå ããæ©èœãè¿œå ããé¢é£ãããŠãŒã¶ãŒã«ããŒãããŒã¿ãéä¿¡ããŠãåå ã®æåŸ
ãåãåã£ãããŒãã®ãªã¹ãã«è¡šç€ºããŸãã ãã®ãªã¹ããäœæãããŸãã
æ°ããããŒããŠãŒã¶ãŒãè¿œå ãã
ãªãªãžãã«
åã®ããŒãã§ã¯ãããŒããä¿åããããã®ããŒãã«ã Board
ã¢ãã«ãäœæããèªèšŒããããŠãŒã¶ãŒã®æ°ããããŒããäžèŠ§è¡šç€ºããã³äœæããã³ã³ãããŒã©ãŒãçæããŸããã ãŸããæ¢åã®ããŒããšæ°ããããŒããè¿œå ããããã®ãã©ãŒã ã衚瀺ã§ããããã«ãããã³ããšã³ããããã°ã©ã ããŸããã æ°ããããŒããäœæããåŸãã³ã³ãããŒã©ãŒãã確èªãåãåã£ãåŸããŠãŒã¶ãŒããã¬ãŒã³ããŒã·ã§ã³ã«ãªãã€ã¬ã¯ãããŠããã¹ãŠã®è©³çŽ°ã衚瀺ããæ¢åã®ãŠãŒã¶ãŒãåå è
ãšããŠè¿œå ã§ããããã«ããå¿
èŠããããŸãã ãããŸãããïŒ
Reactãã¬ãŒã³ããŒã·ã§ã³ã³ã³ããŒãã³ã
ç¶è¡ããåã«ã Reactã«ãŒãã確èªããŠãã ããã
// web/static/js/routes/index.js import { IndexRoute, Route } from 'react-router'; import React from 'react'; import MainLayout from '../layouts/main'; import AuthenticatedContainer from '../containers/authenticated';; import BoardsShowView from '../views/boards/show'; // ... export default ( <Route component={MainLayout}> ... <Route path="/" component={AuthenticatedContainer}> <IndexRoute component={HomeIndexView} /> ... <Route path="/boards/:id" component={BoardsShowView}/> </Route> </Route> );
ã«ãŒã/boards/:id
ã¯BoardsShowView
ã³ã³ããŒãã³ãã«ãã£ãŠåŠçãããäœæããå¿
èŠããããŸãã
ããŒãShowView // web/static/js/views/boards/show.js import React, {PropTypes} from 'react'; import { connect } from 'react-redux'; import Actions from '../../actions/current_board'; import Constants from '../../constants'; import { setDocumentTitle } from '../../utils'; import BoardMembers from '../../components/boards/members'; class BoardsShowView extends React.Component { componentDidMount() { const { socket } = this.props; if (!socket) { return false; } this.props.dispatch(Actions.connectToChannel(socket, this.props.params.id)); } componentWillUnmount() { this.props.dispatch(Actions.leaveChannel(this.props.currentBoard.channel)); } _renderMembers() { const { connectedUsers, showUsersForm, channel, error } = this.props.currentBoard; const { dispatch } = this.props; const members = this.props.currentBoard.members; const currentUserIsOwner = this.props.currentBoard.user.id === this.props.currentUser.id; return ( <BoardMembers dispatch={dispatch} channel={channel} currentUserIsOwner={currentUserIsOwner} members={members} connectedUsers={connectedUsers} error={error} show={showUsersForm} /> ); } render() { const { fetching, name } = this.props.currentBoard; if (fetching) return ( <div className="view-container boards show"> <i className="fa fa-spinner fa-spin"/> </div> ); return ( <div className="view-container boards show"> <header className="view-header"> <h3>{name}</h3> {::this._renderMembers()} </header> <div className="canvas-wrapper"> <div className="canvas"> <div className="lists-wrapper"> {::this._renderAddNewList()} </div> </div> </div> </div> ); } } const mapStateToProps = (state) => ({ currentBoard: state.currentBoard, socket: state.session.socket, currentUser: state.session.currentUser, }); export default connect(mapStateToProps)(BoardsShowView);
æ¥ç¶ããããšãã³ã³ããŒãã³ãã¯ã ããŒã7ã§äœæããã«ã¹ã¿ã ãœã±ããã䜿çšããŠããŒããã£ãã«ã«æ¥ç¶ããŸãã 衚瀺ããããšãæåã«fetching
å±æ§ãtrue
ã«èšå®ãããŠãããã©ããã確èªããããŒã¿ããŸã ããŠã³ããŒãäžã®å Žåã¯ãããŠã³ããŒãã€ã³ãžã±ãŒã¿ãŒã衚瀺ãããŸãã ã芧ã®ãšããã currentBoard
èŠçŽ ãããã©ã¡ãŒã¿ãŒãåãåããŸãã currentBoard
èŠçŽ ã¯ã次ã®ã³ã³ããŒã¿ãŒã«ãã£ãŠäœæãããç¶æ
ã«æ ŒçŽãããŸãã
ãã©ã³ã¹ãã©ãŒããŒãšã¢ã¯ã·ã§ã³ã®ã³ã³ã¹ãã©ã¯ã¿ãŒ
çŸåšã®ããŒãã®ã¹ããŒã¿ã¹ã®éå§ç¹ãšããŠã board
ããŒã¿ã channel
ããã³fetching
ãã©ã°ãä¿åããã ãã§ãã
// web/static/js/reducers/current_board.js import Constants from '../constants'; const initialState = { channel: null, fetching: true, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { case Constants.CURRENT_BOARD_FETHING: return { ...state, fetching: true }; case Constants.BOARDS_SET_CURRENT_BOARD: return { ...state, fetching: false, ...action.board }; case Constants.CURRENT_BOARD_CONNECTED_TO_CHANNEL: return { ...state, channel: action.channel }; default: return state; } }
current_board
ã¢ã¯ã·ã§ã³ã³ã³ã¹ãã©ã¯ã¿ãŒãèŠãŠããã£ã³ãã«ã«æ¥ç¶ããå¿
èŠãªãã¹ãŠã®ããŒã¿ãåŠçããæ¹æ³ã確èªããŸãããã
// web/static/js/actions/current_board.js import Constants from '../constants'; const Actions = { connectToChannel: (socket, boardId) => { return dispatch => { const channel = socket.channel(`boards:${boardId}`); dispatch({ type: Constants.CURRENT_BOARD_FETHING }); channel.join().receive('ok', (response) => { dispatch({ type: Constants.BOARDS_SET_CURRENT_BOARD, board: response.board, }); dispatch({ type: Constants.CURRENT_BOARD_CONNECTED_TO_CHANNEL, channel: channel, }); }); }; }, // ... }; export default Actions;
UserChannel
ãšUserChannel
ããœã±ããã䜿çšããŠã boards:${boardId}
ãšããŠå®çŸ©ãããæ°ãããã£ãã«ãäœæããŠæ¥ç¶ãboards:${boardId}
ãããã³ããŒãã®JSONè¡šçŸãåçãšããŠåãåãã BOARDS_SET_CURRENT_BOARD
ã¢ã¯ã·ã§ã³ãšãšãã«ã¹ãã¢ã«éä¿¡ãããŸãã ãã®æç¹ããããã¶ã€ããŒã¯ãã£ã³ãã«ã«æ¥ç¶ãããåå è
ãããŒãäžã§è¡ã£ããã¹ãŠã®å€æŽãåãåãã ReactãšReduxã®ãããã§ãããã®å€æŽãèªåçã«ç»é¢ã«è¡šç€ºããŸãã ãã ããæåã«BoardChannel
ãäœæããå¿
èŠããããŸãã
ããŒããã£ã³ãã«
æ®ãã®ã»ãŒãã¹ãŠã®æ©èœã¯ãã®ã¢ãžã¥ãŒã«ã«å®è£
ãããŸãããçŸæç¹ã§ã¯ãéåžžã«ã·ã³ãã«ãªããŒãžã§ã³ãå®è£
ããŠããŸãã
# web/channels/board_channel.ex defmodule PhoenixTrello.BoardChannel do use PhoenixTrello.Web, :channel alias PhoenixTrello.Board def join("boards:" <> board_id, _params, socket) do board = get_current_board(socket, board_id) {:ok, %{board: board}, assign(socket, :board, board)} end defp get_current_board(socket, board_id) do socket.assigns.current_user |> assoc(:boards) |> Repo.get(board_id) end end
join
ã¡ãœããã¯ããœã±ããã«å²ãåœãŠããããŠãŒã¶ãŒã«é¢é£ä»ããããçŸåšã®ããŒããåãåãããããè¿ãããœã±ããã«å²ãåœãŠãŸãããã®çµæãè¿œå ã®ã¡ãã»ãŒãžã«äœ¿çšã§ããããã«ãªããŸãïŒ ããŒã¿ããŒã¹ãžã®è¿œå ã¯ãšãªãªã-çŽTranslator ïŒã
圹å¡
ããŒãããŠãŒã¶ãŒã«è¡šç€ºããããã次ã®ã¹ãããã§ã¯ãæ¢åã®ãŠãŒã¶ãŒãåå è
ãšããŠè¿œå ããŠãäžç·ã«äœæ¥ã§ããããã«ããŸãã ããŒããä»ã®ãŠãŒã¶ãŒã«ãªã³ã¯ããã«ã¯ããã®é¢ä¿ãä¿åããæ°ããããŒãã«ãäœæããå¿
èŠããããŸãã ã³ã³ãœãŒã«ã«åãæ¿ããŠå®è¡ããŸãïŒ
$ mix phoenix.gen.model UserBoard user_boards user_id:references:users board_id:references:boards
çµæã®ç§»è¡ãã¡ã€ã«ããããã«æŽæ°ããå¿
èŠããããŸãã
# priv/repo/migrations/20151230081546_create_user_board.exs defmodule PhoenixTrello.Repo.Migrations.CreateUserBoard do use Ecto.Migration def change do create table(:user_boards) do add :user_id, references(:users, on_delete: :delete_all), null: false add :board_id, references(:boards, on_delete: :delete_all), null: false timestamps end create index(:user_boards, [:user_id]) create index(:user_boards, [:board_id]) create unique_index(:user_boards, [:user_id, :board_id]) end end
null
å¶éã«å ããŠã user_id
ãšboard_id
äžæã®ã€ã³ããã¯ã¹ãè¿œå ããŠã User
ãåãBoard
2åè¿œå ã§ããªãããã«ãUser
ã mix ecto.migrate
å®è¡ããåŸã UserBoard
ã¢ãã«ã«ç§»ããŸãããã
# web/models/user_board.ex defmodule PhoenixTrello.UserBoard do use PhoenixTrello.Web, :model alias PhoenixTrello.{User, Board} schema "user_boards" do belongs_to :user, User belongs_to :board, Board timestamps end @required_fields ~w(user_id board_id) @optional_fields ~w() def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) |> unique_constraint(:user_id, name: :user_boards_user_id_board_id_index) end end
ããã§ã¯çããããšã¯äœããããŸãããã User
ã¢ãã«ã«æ°ããé¢ä¿ãè¿œå ããå¿
èŠããããŸãã
# web/models/user.ex defmodule PhoenixTrello.User do use PhoenixTrello.Web, :model # ... schema "users" do # ... has_many :user_boards, UserBoard has_many :boards, through: [:user_boards, :board] # ... end # ... end
ããã«2ã€ã®é¢ä¿ããããŸãããæãéèŠãªãã®ã¯:boards
ãããã¯ãã¢ã¯ã»ã¹ã®å¶åŸ¡ã«äœ¿çšããŸãã Board
ã¢ãã«ã«ãè¿œå ããŸãã
# web/models/board.ex defmodule PhoenixTrello.Board do # ... schema "boards" do # ... has_many :user_boards, UserBoard has_many :members, through: [:user_boards, :user] timestamps end end
ãããã®å€æŽã«ããããŠãŒã¶ãŒãäœæããããŒããšæåŸ
ãããããŒããåºå¥ã§ããããã«ãªããŸããã ããã¯éåžžã«éèŠã§ããããŒãã®ãã¬ãŒã³ããŒã·ã§ã³ã§ã¯ãåå è
ããã®äœæè
ã«ã®ã¿è¿œå ããããã®ãã©ãŒã ã衚瀺ãããããã§ãã ããã«å ããŠãããã©ã«ãã§è¡šç€ºããããã«ãäœæè
ãåå è
ãšããŠèªåçã«è¿œå ããããã BoardController
å°ããªå€æŽãå ããŸãã
ããŒãã³ã³ãããŒã©ãŒ # web/controllers/api/v1/board_controller.ex defmodule PhoenixTrello.BoardController do use PhoenixTrello.Web, :controller #... def create(conn, %{"board" => board_params}) do current_user = Guardian.Plug.current_resource(conn) changeset = current_user |> build_assoc(:owned_boards) |> Board.changeset(board_params) if changeset.valid? do board = Repo.insert!(changeset) board |> build_assoc(:user_boards) |> UserBoard.changeset(%{user_id: current_user.id}) |> Repo.insert! conn |> put_status(:created) |> render("show.json", board: board ) else conn |> put_status(:unprocessable_entity) |> render("error.json", changeset: changeset) end end end
UserBoard
ãäœæããæ£ç¢ºæ§ã確èªããåŸã«è¿œå ããæ¹æ³ã«æ³šæããŠãã ããã
ããŒãåå è
ã³ã³ããŒãã³ã
ãã®ã³ã³ããŒãã³ãã¯ããã¹ãŠã®åå è
ã®ã¢ãã¿ãŒãšæ°ããåå è
ãè¿œå ããããã®ãã©ãŒã ã衚瀺ããŸãïŒ
ã芧ã®ãšããã BoardController
以åã®å€æŽã®ãããã§ãææè
ãå¯äžã®ã¡ã³ããŒãšããŠè¡šç€ºãããããã«ãªããŸããã ãã®ã³ã³ããŒãã³ããã©ã®ããã«èŠãããèŠãŠã¿ãŸãããïŒ
ããŒãåå è
ã³ã³ããŒãã³ã // web/static/js/components/boards/members.js import React, {PropTypes} from 'react'; import ReactGravatar from 'react-gravatar'; import classnames from 'classnames'; import PageClick from 'react-page-click'; import Actions from '../../actions/current_board'; export default class BoardMembers extends React.Component { _renderUsers() { return this.props.members.map((member) => { const index = this.props.connectedUsers.findIndex((cu) => { return cu === member.id; }); const classes = classnames({ connected: index != -1 }); return ( <li className={classes} key={member.id}> <ReactGravatar className="react-gravatar" email={member.email} https/> </li> ); }); } _renderAddNewUser() { if (!this.props.currentUserIsOwner) return false; return ( <li> <a onClick={::this._handleAddNewClick} className="add-new" href="#"><i className="fa fa-plus"/></a> {::this._renderForm()} </li> ); } _renderForm() { if (!this.props.show) return false; return ( <PageClick onClick={::this._handleCancelClick}> <ul className="drop-down active"> <li> <form onSubmit={::this._handleSubmit}> <h4>Add new members</h4> {::this._renderError()} <input ref="email" type="email" required={true} placeholder="Member email"/> <button type="submit">Add member</button> or <a onClick={::this._handleCancelClick} href="#">cancel</a> </form> </li> </ul> </PageClick> ); } _renderError() { const { error } = this.props; if (!error) return false; return ( <div className="error"> {error} </div> ); } _handleAddNewClick(e) { e.preventDefault(); this.props.dispatch(Actions.showMembersForm(true)); } _handleCancelClick(e) { e.preventDefault(); this.props.dispatch(Actions.showMembersForm(false)); } _handleSubmit(e) { e.preventDefault(); const { email } = this.refs; const { dispatch, channel } = this.props; dispatch(Actions.addNewMember(channel, email.value)); } render() { return ( <ul className="board-users"> {::this._renderUsers()} {::this._renderAddNewUser()} </ul> ); } }
å®éã members
ãã©ã¡ãŒã¿ãŒãå埩åŠçããŠãã¢ãã¿ãŒã衚瀺ããŸãã çŸåšã®ãŠãŒã¶ãŒãããŒãã®ææè
ã§ããå Žåãã³ã³ããŒãã³ãã«ã¯[ æ°èŠè¿œå ]ãã¿ã³ã衚瀺ãããŸãã ãã®ãã¿ã³ãã¯ãªãã¯ãããšãåå è
ã®é»åã¡ãŒã«ãèŠæ±ãããã©ãŒã ã衚瀺ããããã©ãŒã ãaddNewMember
ã¢ã¯ã·ã§ã³ã³ã³ã¹ãã©ã¯ã¿ãŒãaddNewMember
ãŸãã
AddNewMemberã¢ã¯ã·ã§ã³ã³ã³ã¹ãã©ã¯ã¿ãŒ
ããããã¯ãã³ã³ãããŒã©ãŒã䜿çšããŠReactããã³ããšã³ãã«å¿
èŠãªããŒã¿ãäœæããã³åä¿¡ãã代ããã«ãããã«å¯Ÿãã責任ãBoardChannel
ãå€æŽããã¹ãŠã®æ¥ç¶ãŠãŒã¶ãŒã«éä¿¡ãããããã«ããŸãã ãããå¿ããã«ãå¿
èŠãªã¢ã¯ã·ã§ã³ã³ã³ã¹ãã©ã¯ã¿ãŒãè¿œå ããŸãã
// web/static/js/actions/current_board.js import Constants from '../constants'; const Actions = { // ... showMembersForm: (show) => { return dispatch => { dispatch({ type: Constants.CURRENT_BOARD_SHOW_MEMBERS_FORM, show: show, }); }; }, addNewMember: (channel, email) => { return dispatch => { channel.push('members:add', { email: email }) .receive('error', (data) => { dispatch({ type: Constants.CURRENT_BOARD_ADD_MEMBER_ERROR, error: data.error, }); }); }; }, // ... } export default Actions;
showMembersForm
ã䜿çšãããšããã¢ã®ã«ããããç°¡åã«ãã©ãŒã ã衚瀺ãŸãã¯é衚瀺ã«ã§ããŸãã ãŠãŒã¶ãŒãæäŸããé»åã¡ãŒã«ã§æ°ããã¡ã³ããŒãè¿œå ããå Žåã¯ãããã«å°é£ã«ãªããŸãã ãããŸã§ã«è¡ã£ãããã«ãhttpèŠæ±ãéä¿¡ãã代ããã«ããã©ã¡ãŒã¿ãŒãšããŠé»åã¡ãŒã«ã䜿çšããŠ"members:add"
ã¡ãã»ãŒãžãchannel
éä¿¡ããŸãã ãšã©ãŒãåä¿¡ãããšãç»é¢ã«è¡šç€ºããããã«ãªãã€ã¬ã¯ãããŸãã è¯å®çãªçµæãåŠçããŠã¿ãŸãããïŒ ç°ãªãã¢ãããŒãã䜿çšãããããæ¥ç¶ãããŠãããã¹ãŠã®åå è
ã«çµæãéä¿¡ããŸãã
ããŒããã£ã³ãã«
, BoardChannel
:
# web/channels/board_channel.ex defmodule PhoenixTrello.BoardChannel do # ... def handle_in("members:add", %{"email" => email}, socket) do try do board = socket.assigns.board user = User |> Repo.get_by(email: email) changeset = user |> build_assoc(:user_boards) |> UserBoard.changeset(%{board_id: board.id}) case Repo.insert(changeset) do {:ok, _board_user} -> broadcast! socket, "member:added", %{user: user} PhoenixTrello.Endpoint.broadcast_from! self(), "users:#{user.id}", "boards:add", %{board: board} {:noreply, socket} {:error, _changeset} -> {:reply, {:error, %{error: "Error adding new member"}}, socket} end catch _, _-> {:reply, {:error, %{error: "User does not exist"}}, socket} end end # ... end
Phoenix handle_in
, Elixir . members:add
, email, . , e-mail UserBoard
. , ( broadcast
) member:added
, . :
PhoenixTrello.Endpoint.broadcast_from! self(), "users:#{user.id}", "boards:add", %{board: board}
boards:add
UserChannel
, , . , , .
ã泚æ 翻蚳è
, . .
:
{:reply, :ok, socket}
, {:reply, {:ok, message}, socket}
{:reply, {:error, message}, socket}
, message
â , ( ). , , callback - ;push(socket, event, message)
, event
â : ( . front-end channel.on(...)
);broadcast(socket, event, message)
: , ;broadcast_from(socket, event, message)
: , .
(, ):
AppName.Endpoint.broadcast(topic, event, message)
, topic
â : , ( (, , ), , )
, push
, "" . - , "" "", try do ... end
, ( Elixir, , ).
front-end member:added
channel
, :
// web/static/js/actions/current_board.js import Constants from '../constants'; const Actions = { // ... connectToChannel: (socket, boardId) => { return dispatch => { const channel = socket.channel(`boards:${boardId}`); // ... channel.on('member:added', (msg) => { dispatch({ type: Constants.CURRENT_BOARD_MEMBER_ADDED, user: msg.user, }); }); // ... } }, }; export default Actions;
boards:add
, :
// web/static/js/actions/sessions.js export function setCurrentUser(dispatch, user) { channel.on('boards:add', (msg) => { // ... dispatch({ type: Constants.BOARDS_ADDED, board: msg.board, }); }); };
, , , (state) :
// web/static/js/reducers/current_board.js export default function reducer(state = initialState, action = {}) { // ... case Constants.CURRENT_BOARD_MEMBER_ADDED: const { members } = state; members.push(action.user); return { ...state, members: members, showUsersForm: false }; } // ... }
// web/static/js/reducers/boards.js export default function reducer(state = initialState, action = {}) { // ... switch (action.type) { case Constants.BOARDS_ADDED: const { invitedBoards } = state; return { ...state, invitedBoards: [action.board].concat(invitedBoards) }; } // ... }
ããã§ãåå è
ã®ã¢ãã¿ãŒããªã¹ãã«è¡šç€ºãããããŒããžã®ã¢ã¯ã»ã¹æš©ãšããªã¹ããšã«ãŒããè¿œå ããã³å€æŽããããã«å¿
èŠãªæš©éãåŸãããŸãã
以åã«èª¬æãããã³ã³ããŒãã³ããæãåºããªãBoardMembers
ãclassName
ã¢ãã¿ãŒã¯åå è
IDããã©ã¡ãŒã¿ãªã¹ãã«ååšãããã©ããã«äŸåããŸãconnectedUsers
ããã®ãªã¹ãã«ã¯ãçŸåšããŒãã®ãã£ãã«ã«æ¥ç¶ããŠãããã¹ãŠã®åå è
ã®IDãæ ŒçŽãããŸãããªã¹ããäœæããŠåŠçããã«ã¯ãElixirã®æ°žç¶çãªé·æå®è¡ã¹ããŒããã«ããã»ã¹ã䜿çšããŸãããããã¯æ¬¡ã®åºçç©ã§è¡ããŸãã
ãããŸã§ã®éãã©ã€ããã¢ãšæçµçµæã®ãœãŒã¹ã³ãŒãã確èªããŠãã ããã