Phoenix and ReactでTrelloをクローンします。 パート8-9











新しいボードをリストして作成します



オリジナル







現時点では、ユーザー登録と認証管理のすべての重要な側面を実装し、ソケットへの接続とチャネルの入力も行っているため、次のレベルに進み、ユーザーにリストを表示して独自のボードを作成する機会を与えます。







ネタバレの下に非常に長いリストを隠しました-約 翻訳者







ボードモデルの移行



まず、移行とモデルを作成する必要があります。 これを行うには、単に次を実行します:







$ 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);
      
      





多くのことが行われているので、次を見てみましょう。









次に、 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;
      
      







変換器



パズルの最後のピースは、非常にシンプルなコンバーターです。







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



アクションとともにストアに送信されます。 この時点から、デザイナーはチャンネルに接続され、参加者がボード上で行ったすべての変更を受け取り、 ReactReduxのおかげでこれらの変更を自動的に画面に表示します。 ただし、最初に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の永続的な長期実行ステートフルプロセスを使用しますが、これは次の出版物で行います。







それまでの間、ライブデモ最終結果のソースコードを確認してください








All Articles