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アクションずずもにストアに送信されたす。 この時点から、デザむナヌはチャンネルに接続され、参加者がボヌド䞊で行ったすべおの倉曎を受け取り、 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の氞続的な長期実行ステヌトフルプロセスを䜿甚したすが、これは次の出版物で行いたす。


それたでの間、ラむブデモず最終結果の゜ヌスコヌドを確認しおください。



Source: https://habr.com/ru/post/J308382/


All Articles