新しいボードをリストして作成します
現時点では、ユーザー登録と認証管理のすべての重要な側面を実装し、ソケットへの接続とチャネルの入力も行っているため、次のレベルに進み、ユーザーにリストを表示して独自のボードを作成する機会を与えます。
ネタバレの下に非常に長いリストを隠しました-約 翻訳者
ボードモデルの移行
まず、移行とモデルを作成する必要があります。 これを行うには、単に次を実行します:
$ 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/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
作成しましょう。
// 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 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
コンポーネントによって処理され、作成する必要があります。
// 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-endchannel.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の永続的な長期実行ステートフルプロセスを使用しますが、これは次の出版物で行います。
それまでの間、ライブデモと最終結果のソースコードを確認してください。