1 | # startupjs react-sharedb
|
2 |
|
3 | > Run `ShareDB` in `React`
|
4 |
|
5 | ## What it does
|
6 |
|
7 | 1. Brings real-time collaboration to React using [ShareDB](https://github.com/share/sharedb);
|
8 | 2. Uses [Racer](https://derbyjs.com/docs/derby-0.10/models) to add a `model`
|
9 | to your app to do any data manipulations;
|
10 | 3. The `model` acts as a global singleton state, so you can use it as a
|
11 | replacement for other state-management systems like `Redux` or `MobX`;
|
12 | 4. Makes the `render` reactive similar to how it's done in `MobX` --
|
13 | rerendering happens whenever any `model` data you used in `render`
|
14 | changes.
|
15 |
|
16 | ## Installation
|
17 |
|
18 | It's recommended to just use `startupjs` package, since it proxies the API of `@startupjs/react-sharedb`.
|
19 |
|
20 | ```
|
21 | yarn add startupjs
|
22 | ```
|
23 |
|
24 | ## Hooks Usage
|
25 |
|
26 | ### `observer(FunctionalComponent)` HOF
|
27 |
|
28 | Higher Order Function which makes your functional component rendering reactive.
|
29 | **You have to** wrap your functional components in it to be able to use `react-sharedb` hooks.
|
30 |
|
31 | ```js
|
32 | import {observer, useDoc} from 'startupjs'
|
33 |
|
34 | export default observer(function User ({userId}) {
|
35 | let [user, $user] = useDoc('users', userId)
|
36 | return (
|
37 | <input value={user.name} onChange={e => $user.set('name', e.target.value)} />
|
38 | )
|
39 | })
|
40 | ```
|
41 |
|
42 | ### `useDoc(collection, docId)`
|
43 |
|
44 | Subscribe to the particular Mongo document by id.
|
45 |
|
46 | `collection` \[String\] -- collection name. Required
|
47 |
|
48 | `docId` \[String\] -- document id. Required
|
49 |
|
50 | **Returns:** `[doc, $doc]`, where:
|
51 |
|
52 | `doc` \[Object\] -- value of document
|
53 |
|
54 | `$doc` \[Model\] -- scoped model targeting path `collection.docId`
|
55 |
|
56 | **Example:**
|
57 |
|
58 | ```js
|
59 | import React from 'react'
|
60 | import { observer, useDoc } from 'startupjs'
|
61 |
|
62 | export default observer(function Room ({
|
63 | roomId = 'DUMMY_ID'
|
64 | }) {
|
65 | let [room, $room] = useDoc('rooms', roomId)
|
66 |
|
67 | // If the document with an `_id` of `roomId` doesn't exist yet, we create it.
|
68 | // We have to wait for the document to be created by throwing the promise.
|
69 | if (!room) throw $room.createAsync({ title: 'New Room' })
|
70 |
|
71 | function onChange (e) {
|
72 | $room.set('title', e.target.value)
|
73 | }
|
74 |
|
75 | return <input onChange={onChange} value={room.title} />
|
76 | })
|
77 | ```
|
78 |
|
79 | **IMPORTANT:** The id of the document is stored internally in Mongo inside the `_id` field.
|
80 | But when it gets into the model, it gets replaced with the `id` field instead, and vice versa.
|
81 |
|
82 | ### `useQuery(collection, query)`
|
83 |
|
84 | Subscribe to the Mongo query.
|
85 |
|
86 | `collection` \[String\] -- collection name. Required
|
87 |
|
88 | `query` \[Object\] -- query (regular, `$count`, `$aggregate` queries are supported). Required
|
89 |
|
90 | **Returns:** `[docs, $docs]`, where:
|
91 |
|
92 | `docs` \[Array\] -- array of documents
|
93 |
|
94 | `$docs` \[Model\] -- scoped model targeting the whole `collection`
|
95 |
|
96 | **Example:**
|
97 |
|
98 | ```js
|
99 | let [users, $users] = useQuery('users', { roomId: props.roomId, anonymous: false })
|
100 | ```
|
101 |
|
102 | **IMPORTANT:** The scoped model `$docs`, which you receive from the hook, targets the global collection path.
|
103 | You can use it to easily reach a document with a particular id using scoped models:
|
104 |
|
105 | ```js
|
106 | let [users, $users] = useQuery('users', { roomId, anonymous: false })
|
107 | for (let user of users) {
|
108 | $users.at(user.id).setEach({
|
109 | joinedRoom: true,
|
110 | updatedAt: Date.now()
|
111 | })
|
112 | }
|
113 | ```
|
114 |
|
115 | ### `useQueryIds(collection, ids, options)`
|
116 |
|
117 | Subscribe to documents in collection by their ids
|
118 |
|
119 | `collection` \[String\] -- collection name. Required
|
120 |
|
121 | `ids` \[Array\] -- array of strings which should be document ids.
|
122 |
|
123 | `options` \[Object\] --
|
124 |
|
125 | ```js
|
126 | {
|
127 | reverse: false // reverse the order of resulting array
|
128 | }
|
129 | ```
|
130 |
|
131 | **Example:**
|
132 |
|
133 | ```js
|
134 | export default observer(function Players ({ gameId }) {
|
135 | let [game] = useDoc('games', gameId)
|
136 | let [players, $players] = useQueryIds('players', game.playerIds)
|
137 |
|
138 | return (
|
139 | <div>{players.map(i => i.name).join(' ,')}</div>
|
140 | )
|
141 | })
|
142 | ```
|
143 |
|
144 | ### `useQueryDoc(collection, query)`
|
145 |
|
146 | Subscribe to a document using a query. It's the same as `useDoc()`, but
|
147 | with `query` parameter instead of the particular `docId`.
|
148 | `$limit: 1` and `$sort: { createdAt: -1 }` are added to the query automatically (if they don't already exist).
|
149 |
|
150 | `collection` \[String\] -- collection name. Required
|
151 |
|
152 | `query` \[Object\] -- query object, same as in `useQuery()`.
|
153 |
|
154 | **Example:**
|
155 |
|
156 | ```js
|
157 | export default observer(function NewPlayer ({ gameId }) {
|
158 | // { $sort: { createdAt: -1 }, $limit: 1 }
|
159 | // is added automatically to the query, so the newest player will be returned.
|
160 | // It's also reactive, so whenever a new player joins, you'll receive the new data and model.
|
161 | let [newPlayer, $newPlayer] = useQueryDoc('players', { gameId })
|
162 | if (!newPlayer) return null // <Loading />
|
163 |
|
164 | return (
|
165 | <div>New player joined: {newPlayer.name}</div>
|
166 | )
|
167 | })
|
168 | ```
|
169 |
|
170 | ### `useLocal(path)`
|
171 |
|
172 | Subscribe to the data you already have in your local model by path.
|
173 |
|
174 | You will usually use it to subscribe to private collections like `_page` or `_session`.
|
175 | This is very useful when you want to share the state between multiple components.
|
176 |
|
177 | It's also possible to subscribe to the path from a public collection, for example when you
|
178 | want to work with some nested value of a particular document you have already subscribed to.
|
179 |
|
180 | **Returns:** `[value, $value]`, where:
|
181 |
|
182 | `value` \[any\] -- data, located on that `path`
|
183 |
|
184 | `$value` \[Model\] -- model, targeting that `path`
|
185 |
|
186 | **Example:**
|
187 |
|
188 | ```js
|
189 | const SIDEBAR_OPENED = '_page.Sidebar.opened'
|
190 |
|
191 | const Topbar = observer(() => {
|
192 | let [sidebarOpened, $sidebarOpened] = useLocal(SIDEBAR_OPENED)
|
193 | return <>
|
194 | <button
|
195 | onClick={() => $sidebarOpened.set(!sidebarOpened)}
|
196 | >Toggle Sidebar</button>
|
197 | </>
|
198 | })
|
199 |
|
200 | const Sidebar = observer(() => {
|
201 | let [sidebarOpened] = useLocal(SIDEBAR_OPENED)
|
202 | return sidebarOpened ? <p>Sidebar</p> : null
|
203 | })
|
204 |
|
205 | const App = observer(() => {
|
206 | return <>
|
207 | <Topbar />
|
208 | <Sidebar />
|
209 | </>
|
210 | })
|
211 | ```
|
212 |
|
213 | ### `useSession(path)`
|
214 |
|
215 | A convenience method to access the `_session` local collection.
|
216 |
|
217 | ```js
|
218 | let [userId, $userId] = useSession('userId')
|
219 | // It's the same as doing:
|
220 | let [userId, $userId] = useLocal('_session.userId')
|
221 | ```
|
222 |
|
223 | ### `usePage(path)`
|
224 |
|
225 | A convenience method to access the `_page` local collection.
|
226 |
|
227 | **Example:**
|
228 |
|
229 | ```js
|
230 | let [game, $game] = usePage('game')
|
231 | // It's the same as doing:
|
232 | let [game, $game] = useLocal('_page.game')
|
233 | ```
|
234 |
|
235 | ### `useValue(defaultValue)`
|
236 |
|
237 | An observable alternative to `useState`.
|
238 |
|
239 | **Example:**
|
240 |
|
241 | ```js
|
242 | const DEFAULT_USER = {
|
243 | first: 'John',
|
244 | last: 'Smith',
|
245 | address: 'Washington St.'
|
246 | }
|
247 |
|
248 | const Field = observer(({ label, $value }) => {
|
249 | return <div>
|
250 | <span>{label}: </span>
|
251 | <input value={$value.get()} onChange={e => $value.set(e.target.value)} />
|
252 | </div>
|
253 | })
|
254 |
|
255 | const User = observer(() => {
|
256 | let [user, $user] = useValue(DEFAULT_USER)
|
257 |
|
258 | return <>
|
259 | <Field label='First' $value={$user.at('first')} />
|
260 | <Field label='Last' $value={$user.at('last')} />
|
261 | <Field label='Address' $value={$user.at('address')} />
|
262 | <code>{user}</code>
|
263 | </>
|
264 | })
|
265 | ```
|
266 |
|
267 | ### `useModel(path)`
|
268 |
|
269 | Return a model scoped to `path` (memoized by the `path` argument).
|
270 | If `path` is not provided, returns the model scoped to the root path.
|
271 |
|
272 | **Example:**
|
273 |
|
274 | ```js
|
275 | import React from 'react'
|
276 | import {render} from 'react-dom'
|
277 | import {observer, useModel, useLocal} from 'startupjs'
|
278 |
|
279 | const Main = observer(() => {
|
280 | return (
|
281 | <div style={{display: 'flex'}}>
|
282 | <Content />
|
283 | <Sidebar />
|
284 | </div>
|
285 | )
|
286 | })
|
287 |
|
288 | const Content = observer(() => {
|
289 | let $showSidebar = useModel('_page.Sidebar.show')
|
290 |
|
291 | // sidebar will be opened without triggering rerendering of the <Content /> component (this component)
|
292 | return (
|
293 | <div>
|
294 | <p>I am Content</p>
|
295 | <button onClick={() => $showSidebar.setDiff(true)}>Open Sidebar</button>
|
296 | </div>
|
297 | )
|
298 | })
|
299 |
|
300 | const Sidebar = observer(() => {
|
301 | let [show, $show] = useLocal('_page.Sidebar.show')
|
302 | if (!show) return null
|
303 | return (
|
304 | <div>
|
305 | <p>I am Sidebar</p>
|
306 | <button onClick={() => $show.del()}>Close</button>
|
307 | </div>
|
308 | )
|
309 | })
|
310 |
|
311 | render(<Main />, document.body.appendChild(document.createElement('div')))
|
312 | ```
|
313 |
|
314 | ### Hooks Example
|
315 |
|
316 | ```js
|
317 | import React from 'react'
|
318 | import {observer, useDoc, useQuery, useLocal, useValue} from 'startupjs'
|
319 |
|
320 | export default observer(function Game ({gameId}) {
|
321 | let [secret, $secret] = useValue('Game Secret Password')
|
322 | let [userId, $userId] = useLocal('_session.userId')
|
323 | let [user, $user] = useDoc('users', userId)
|
324 | let [game, $game] = useDoc('games', gameId)
|
325 | let [players, $players] = useQuery('players', {_id: {$in: game.playerIds}})
|
326 | let [users, $users] = useQuery('users', {_id: {$in: players.map(i => i.userId)}})
|
327 |
|
328 | function updateSecret (event) {
|
329 | $secret.set(event.target.value)
|
330 | }
|
331 |
|
332 | function updateName (event) {
|
333 | $user.set('name', event.target.value)
|
334 | }
|
335 |
|
336 | return (
|
337 | <div>
|
338 | <label>
|
339 | Secret:
|
340 | <input type='text' value={code} onChange={updateSecret} />
|
341 | </label>
|
342 |
|
343 | <label>
|
344 | My User Name:
|
345 | <input type='text' value={user.name} onChange={updateName} />
|
346 | </label>
|
347 |
|
348 | <h1>Game {game.title}</h1>
|
349 |
|
350 | <h2>Users in game:</h2>
|
351 | <p>{users.map(i => i.name).join(', ')}</p>
|
352 | </div>
|
353 | )
|
354 | })
|
355 | ```
|
356 |
|
357 | ## Classes Usage
|
358 |
|
359 | ### `@subscribe(cb)` HOC
|
360 |
|
361 | `@subscribe` decorator is used to specify what you want to subscribe to.
|
362 |
|
363 | `@subscribe` gives react component a personal local scope model, located at path `$components.<random_id>`.
|
364 | This model will be automatically cleared when the component is unmounted.
|
365 |
|
366 | ### HOW TO: Subscribe to data and use it in render()
|
367 |
|
368 | `@subscribe` accepts a single argument -- `cb`, which receives `props` and must return the `subscriptions object`.
|
369 |
|
370 | Each `key` in the `subscriptions object` will fetch specified data from the MongoDB or a Local path and
|
371 | will write it into the component's personal scope model under that `key`.
|
372 |
|
373 | The read-only data of the component's model is available as `this.props.store`.
|
374 | Use it to render the data you subscribed to, same way you would use `this.state`.
|
375 |
|
376 | **IMPORTANT** As with `this.state`, the `this.props.store` **SHOULD NOT** be modified directly! Read below to find out how to modify data.
|
377 |
|
378 | Example:
|
379 |
|
380 | ```js
|
381 | import React from 'react'
|
382 | import {subscribe, subLocal, subDoc, subQuery} from 'startupjs'
|
383 |
|
384 | @subscribe(props => ({
|
385 | myUserId: subLocal('_session.userId'),
|
386 | room: subDoc('rooms', props.roomId)
|
387 | }))
|
388 | @subscribe(props => ({
|
389 | myUser: subDoc('users', props.store.userId),
|
390 | usersInRoom: subQuery('users', {_id: {
|
391 | $in: props.store.room && props.store.room.userIds || []
|
392 | }})
|
393 | }))
|
394 | export default class Room extends React.Component {
|
395 | render () {
|
396 | let {room, myUser, usersInRoom} = this.props.store
|
397 | return <Fragment>
|
398 | <h1>{room.name}</h1>
|
399 | <h2>Me: {myUser.name}</h2>
|
400 | <p>Other users in the room: {usersInRoom.map(user => user.name)}</p>
|
401 | </Fragment>
|
402 | }
|
403 | }
|
404 | ```
|
405 |
|
406 | As seen from the example, you can combine multiple `@subscribe` one after another.
|
407 |
|
408 | ### HOW TO: Modify the data you subscribed to
|
409 |
|
410 | The actual scoped model of the component is available as `this.props.$store`.
|
411 | Use it to modify the data. For the API to modify stuff refer to the [Racer documentation](https://derbyjs.com/docs/derby-0.10/models)
|
412 |
|
413 | In addition, all things from `subscriptions object` you subscribed to are
|
414 | available to you as additional scope models in `this.props` under names `this.props.$KEY`
|
415 |
|
416 | Example:
|
417 |
|
418 | ```js
|
419 | import React from 'react'
|
420 | import {subscribe, subLocal, subDoc, subQuery} from 'startupjs'
|
421 |
|
422 | @subscribe(props => ({
|
423 | room: subDoc('rooms', props.roomId)
|
424 | }))
|
425 | export default class Room extends React.Component {
|
426 | updateName = () => {
|
427 | let {$store, $room} = this.props
|
428 | $room.set('name', 'New Name')
|
429 | // You can also use $store to do the same:
|
430 | $store.set('room.name', 'New Name')
|
431 | }
|
432 | render () {
|
433 | let {room} = this.props.store
|
434 | return <Fragment>
|
435 | <h1>{room.name}</h1>
|
436 | <button onClick={this.updateName}>Update Name</button>
|
437 | </Fragment>
|
438 | }
|
439 | }
|
440 | ```
|
441 |
|
442 | ## *\[Classes\]* `sub*()` functions
|
443 |
|
444 | Use sub*() functions to define a particular subscription.
|
445 |
|
446 | <a name="subDoc"></a>
|
447 | ### `subDoc(collection, docId)`
|
448 |
|
449 | Subscribe to a particular document.
|
450 | You'll receive the document data as `props.store.{key}`
|
451 |
|
452 | `collection` \[String\] -- collection name. Required
|
453 | `docId` \[String\] -- document id. Required
|
454 |
|
455 | Example:
|
456 |
|
457 | ```js
|
458 | @subscribe(props => ({
|
459 | room: subDoc('rooms', props.roomId)
|
460 | }))
|
461 | ```
|
462 |
|
463 | <a name="subQuery"></a>
|
464 | ### `subQuery(collection, query)`
|
465 |
|
466 | Subscribe to the Mongo query.
|
467 | You'll receive the docuents as an array: `props.store.{key}`
|
468 | You'll also receive an array of ids of those documents as `props.store.{key}Ids`
|
469 |
|
470 | `collection` \[String\] -- collection name. Required
|
471 | `query` \[Object\] -- query (regular, `$count`, `$aggregate` queries are supported). Required
|
472 |
|
473 | Example:
|
474 |
|
475 | ```js
|
476 | @subscribe(props => ({
|
477 | users: subQuery('users', { roomId: props.roomId, anonymous: false })
|
478 | }))
|
479 | ```
|
480 |
|
481 | **IMPORTANT:** The scope model `${key}`, which you receive from subscription, targets
|
482 | an the array in the local model of the component, NOT the global `collection` path.
|
483 | So you won't be able to use it to efficiently update document's data. Instead you should manually
|
484 | create a scope model which targets a particular document, using the id:
|
485 |
|
486 | ```js
|
487 | let {usersInRoom, $store} = this.props.store
|
488 | for (let user of usersInRoom) {
|
489 | $store.scope(`${collection}.${user.id}`).setEach({
|
490 | joinedRoom: true,
|
491 | updatedAt: Date.now()
|
492 | })
|
493 | }
|
494 | ```
|
495 |
|
496 | <a name="subLocal"></a>
|
497 | ### `subLocal(path)`
|
498 |
|
499 | Subscribe to the data you already have in your local model by path.
|
500 | You'll receive the data on that path as `props.store.{key}`
|
501 |
|
502 | You will usually use it to subscribe to private collections like `_page` or `_session`.
|
503 | This is very useful when you want to share the state between multiple components.
|
504 |
|
505 | It's also possible to subscribe to the path from a public collection, for example when you
|
506 | want to work with some nested value of a particular document you have already subscribed to.
|
507 |
|
508 | Example:
|
509 |
|
510 | ```js
|
511 | const TopBar = subscribe(props => ({
|
512 | sidebarOpened: subLocal('_page.Sidebar.opened')
|
513 | }))({
|
514 | $sidebarOpened
|
515 | }) =>
|
516 | <button
|
517 | onClick={() => $sidebarOpened.setDiff(true)}
|
518 | >Open Sidebar</button>
|
519 |
|
520 | const Sidebar = subscribe(props => ({
|
521 | sidebarOpened: subLocal('_page.Sidebar.opened')
|
522 | }))({
|
523 | store: {sidebarOpened}
|
524 | }) =>
|
525 | sidebarOpened ? <p>Sidebar</p> : null
|
526 | ```
|
527 |
|
528 | <a name="subValue"></a>
|
529 | ### `subValue(value)`
|
530 |
|
531 | A constant value to assign to the local scoped model of the component.
|
532 |
|
533 | `value` \[String\] -- value to assign (any type)
|
534 |
|
535 | ### *\[Classes\]* Example
|
536 |
|
537 | Below is an example of a simple app with 2 components:
|
538 | 1. `Home` -- sets up my userId and renders `Room`
|
539 | 2. `Room` -- shows my user name ands lets user update it,
|
540 | shows all users which are currently in the room.
|
541 |
|
542 | ```js
|
543 | // Home.jsx
|
544 | import React from 'react'
|
545 | import Room from './Room.jsx'
|
546 | import {model, subscribe, subLocal, subDoc, subQuery} from 'startupjs'
|
547 |
|
548 | // `subscribe` means that the data is reactive and gets dynamically updated
|
549 | // whenever the data in MongoDB or in private collections gets updated.
|
550 |
|
551 | @subscribe(props => ({
|
552 | // Subscribe to the path in a private collection `_session`.
|
553 | //
|
554 | // `private` collection means that the data does NOT get synced with MongoDB.
|
555 | // Data in these collections live only on the client-side.
|
556 | //
|
557 | // Collection is considered `private` when its name starts from `_` or `$`.
|
558 | //
|
559 | // Private collections like `_session` are used as a singleton client-only storage,
|
560 | // an alternative to `Redux` or `MobX`.
|
561 | //
|
562 | // You can have as many private collections as you like, but the common
|
563 | // guideline is to use just one collection to store all private data -- `_session`
|
564 | userId: subLocal('_session.userId')
|
565 | }))
|
566 |
|
567 | export default class Home extends React.Component {
|
568 | constructor (...props) {
|
569 | super(...props)
|
570 |
|
571 | // For each thing you subscribe to, you receive a scoped `model`
|
572 | // with the same name prefixed with `$` in `props`. Use it
|
573 | // to update the data with the `model` operations available in Racer:
|
574 | // https://derbyjs.com/docs/derby-0.10/models
|
575 | let {$userId} = this.props
|
576 |
|
577 | // Update the private path `_session.userId` with the new value
|
578 | //
|
579 | // We'll use this `_session.userId` in all other children
|
580 | // components to easily get the userId without doing the potentially
|
581 | // heavy/long process of fetching the userId over and over again.
|
582 | $userId.set(this.getUserId())
|
583 | }
|
584 |
|
585 | // Get my userId somehow (for example from the server-side response).
|
586 | // For simplicity, we'll just generate a random guid in this example
|
587 | // by creating an empty user document each time, so whenever
|
588 | // you open a new page, you'll be a new user.
|
589 | getUserId = () => model.add('users', {})
|
590 |
|
591 | render = () => <Room roomId={'myCoolRoom1'} />
|
592 | }
|
593 | ```
|
594 |
|
595 | ```js
|
596 | // Room.jsx
|
597 | import React from 'react'
|
598 | import {model, subscribe, subLocal, subDoc, subQuery} from 'startupjs'
|
599 |
|
600 | @subscribe(props => ({
|
601 |
|
602 | // Subscribe to the same private path again to get the userId, which
|
603 | // we did manually setup in the parent `<Home>` component.
|
604 | // The pattern of subscribing and updating data in a private path
|
605 | // can be used to expose some data from one component to another.
|
606 | userId: subLocal('_session.userId'),
|
607 |
|
608 | // Subscribe to the particular document from a public collection `rooms` by id.
|
609 | // `public` means that it DOES sync with MongoDB.
|
610 | // `public` collection names start with a regular letter a-z.
|
611 | // You can also use ClassCase for collection names (A-Z), but it is
|
612 | // NOT recommended unless you have such guidelines in your project.
|
613 | room: subDoc('rooms', props.roomId)
|
614 |
|
615 | }))
|
616 |
|
617 | // All things you subscribe to end up in `props.store` under the same name.
|
618 | // We can do a second subscribe using the data we received in the first one.
|
619 |
|
620 | @subscribe(props => ({
|
621 |
|
622 | // Subscribe to the particular document from a public collection by id
|
623 | // which we got from the previous subscription
|
624 | myUser: subDoc('users', props.store.userId),
|
625 |
|
626 | // Subscribe to a query to get docs from a public `users` collection
|
627 | // using the array of userIds from `room` received in the previous subscription
|
628 | users: subQuery('users', {_id: {
|
629 | $in: props.store.room && props.store.room.userIds || []
|
630 | }})
|
631 |
|
632 | }))
|
633 |
|
634 | // Additionally, whenever you subscribe to the MongoDB query, you also
|
635 | // receive the `Ids` in store.
|
636 | // For example, subscribing to the `users` collection above populated
|
637 | // `props.store.users` (array of documents) AND
|
638 | // `props.store.userIds` (array of ids) - auto singular name with the `Ids` suffix
|
639 |
|
640 | export default class Room extends React.Component {
|
641 | constructor (...props) {
|
642 | super(...props)
|
643 | let {$room, roomId} = this.props
|
644 | let {userId, room, room: {userIds = []}} = this.props.store
|
645 |
|
646 | // Create an empty room if it wasn't created yet
|
647 | if (!room) model.add('rooms', {id: roomId, title: `Room #${roomId}`})
|
648 |
|
649 | // Add user to the room unless he's already there
|
650 | if (!userIds.includes(userId)) $room.push('userIds', userId)
|
651 | }
|
652 |
|
653 | changeName = (event) => {
|
654 | let {$myUser} = this.props
|
655 | $myUser.setEach({name: event.target.value})
|
656 | }
|
657 |
|
658 | render () {
|
659 | let { myUser, room, users, userId } = this.props.store
|
660 | return (
|
661 | <main>
|
662 | <h1>{room.title}</h1>
|
663 | <section>
|
664 | My User Name:
|
665 | <input type='text' value={myUser.name} onChange={this.changeName} />
|
666 | </section>
|
667 | <h3>Users in the room:</h3>
|
668 | {
|
669 | users
|
670 | .filter(({id}) => id !== userId) // exclude my user
|
671 | .map(user =>
|
672 | <section key={user.id}>
|
673 | {user.name || `User ${user.id}`}
|
674 | </section>
|
675 | )
|
676 | }
|
677 | </main>
|
678 | )
|
679 | }
|
680 | }
|
681 | ```
|
682 |
|
683 | ## Licence
|
684 |
|
685 | MIT
|
686 |
|
687 | (c) Decision Mapper - http://decisionmapper.com
|