I recently wrote an article about how to use FeathersJS’s
channels to ensure the right realtime data goes to the correct user(s). I want to show how to do the same realtime fun but using CanJS.
I’ll refer to this article on how to setup the FeatherJS
channels:
Getting setup with CanJS
I cloned this repo to get started.
Let’s start at setting up the models so we can load the data and get the realtime goodness. We will first need to create a feathersConnection
which is a set of custom can-connect
behaviours.
import connect from 'can-connect' | |
import constructor from 'can-connect/constructor/constructor' | |
import canMap from 'can-connect/can/map/map' | |
import canRef from 'can-connect/can/ref/ref' | |
import constructorStore from 'can-connect/constructor/store/store' | |
import dataCallbacks from 'can-connect/data/callbacks/callbacks' | |
import combineRequests from 'can-connect/data/combine-requests/combine-requests' | |
import dataParse from 'can-connect/data/parse/parse' | |
import realTime from 'can-connect/real-time/real-time' | |
import callbacksOnce from 'can-connect/constructor/callbacks-once/callbacks-once' | |
import feathersServiceBehavior from 'can-connect-feathers/service' | |
const feathersConnection = function (newBehaviors, options) { | |
if (arguments.length === 1) { | |
options = newBehaviors | |
} | |
const behaviors = [ | |
feathersServiceBehavior, | |
constructor, | |
canMap, | |
canRef, | |
constructorStore, | |
dataCallbacks, | |
combineRequests, | |
dataParse, | |
realTime, | |
callbacksOnce] | |
if (arguments.length === 2) { | |
[].push.apply(behaviors, newBehaviors) | |
} | |
return connect(behaviors, options) | |
} | |
export default feathersConnection |
The above will be used for models to fetch data and keep the model up to date with realtime data. This will also handle combining multiple requests into a single request and a few other cool things.
There is a similar one needed for authentication
import connect from 'can-connect' | |
import dataParse from 'can-connect/data/parse/' | |
import construct from 'can-connect/constructor/' | |
import constructStore from 'can-connect/constructor/store/' | |
import constructCallbacksOnce from 'can-connect/constructor/callbacks-once/' | |
import canMap from 'can-connect/can/map/' | |
import canRef from 'can-connect/can/ref/' | |
import dataCallbacks from 'can-connect/data/callbacks/' | |
import realTime from 'can-connect/real-time/real-time' | |
import feathersSessionBehavior from 'can-connect-feathers/session' | |
const sessionConnection = function (options) { | |
return connect([ | |
feathersSessionBehavior, | |
dataParse, | |
canMap, | |
canRef, | |
construct, | |
constructStore, | |
constructCallbacksOnce, | |
realTime, | |
dataCallbacks | |
], options) | |
} | |
export default sessionConnection |
This will handle logging in and getting a user object once logged in.
Models
The user model we can setup using the above feathers-connection
like so:
import { DefineMap, DefineList, QueryLogic } from 'can' | |
import feathersClient from '../feathers-client' | |
import feathersConnection from './connections/feathers' | |
import feathersQueryLogic from 'feathers-query-logic' | |
const User = DefineMap.extend({ | |
_id: { | |
identity: true, | |
type: 'string' | |
}, | |
email: 'string', | |
password: 'string' | |
}) | |
User.List = DefineList.extend({ | |
'#': User, | |
get usersById () { | |
return this.reduce((users, user) => { | |
users[user._id] = user | |
return users | |
}, {}) | |
}, | |
get usersByEmail () { | |
// Map to object keyed by name for easy lookup | |
return this.reduce((users, user) => { | |
users[user.email] = user | |
return users | |
}, {}) | |
} | |
}) | |
User.connection = feathersConnection({ | |
idProp: '_id', | |
Map: User, | |
List: User.List, | |
name: 'user', | |
feathersService: feathersClient.service('users'), | |
queryLogic: new QueryLogic(User, feathersQueryLogic) | |
}) | |
export default User |
We define the properties on L6-L13
and on L15
we create a reactive list with each list item being an instance of User. The list itself has some computed properties, so we can get usersById
and usersByEmail
.
On L34-L41
we setup the connection details for this model which tells it how to get data. We pass it the feathers-service we want it to use to fetch data.
The session / authentication model is similar but it uses feathers-authentication
to create the connection:
import DefineMap from 'can-define/map/' | |
import DefineList from 'can-define/list/list' | |
import sessionConnection from './connections/session' | |
import feathersClient from '../feathers-client' | |
import User from './user' | |
const Session = DefineMap.extend('Session', { seal: false }, { | |
userId: 'string', | |
accessToken: 'string', | |
exp: { | |
identity: true, | |
type: 'string' | |
}, | |
user: { | |
Type: User, | |
get (lastVal, setVal) { | |
if (this.userPromise) { | |
this.userPromise.then(setVal) | |
} | |
return lastVal | |
} | |
}, | |
userError: { | |
get (lastVal, setVal) { | |
if (this.userPromise) { | |
this.userPromise.catch(setVal) | |
} | |
return null | |
} | |
}, | |
userPromise: { | |
get () { | |
if (this.userId) { | |
return User.get({ _id: this.userId }) | |
} | |
return null | |
} | |
} | |
}) | |
Session.List = DefineList.extend({ | |
'#': Session | |
}) | |
Session.connection = sessionConnection({ | |
feathersClient: feathersClient, | |
idProp: 'exp', | |
Map: Session, | |
List: Session.List, | |
name: 'session' | |
}) | |
export default Session |
We create a userPromise
async getter, which will load the user if the userId
exists, this will allow us within the user
prop to load in a user, which will be an instance of the User
model we defined earlier.
Finally we create a message
model which will handle loading in message data.
import { DefineMap, DefineList, QueryLogic } from 'can' | |
import feathersClient from '../feathers-client' | |
import feathersConnection from './connections/feathers' | |
import feathersQueryLogic from 'feathers-query-logic' | |
var Message = DefineMap.extend('Message', { | |
_id: { | |
identity: true, | |
type: 'string' | |
}, | |
name: 'string', | |
to: 'string', | |
from: 'string' | |
}) | |
Message.List = DefineList.extend({ | |
'#': Message | |
}) | |
Message.connection = feathersConnection({ | |
idProp: '_id', | |
Map: Message, | |
List: Message.List, | |
feathersService: feathersClient.service('/messages'), | |
name: 'message', | |
queryLogic: new QueryLogic(Message, feathersQueryLogic) | |
}) | |
export default Message |
We are using [can-query-logic](https://canjs.com/doc/can-query-logic.html)
along with feathers-query-logic
to handle converting feathers queries into a query format that can-connect
can use to query data.
Getting the data
So far we have discussed getting the models setup so we can load in data, let’s see how that’s done within a component.
Component.extend({ | |
ViewModel: { | |
msg: 'string', | |
to: 'string', | |
messages: 'any', | |
users: 'any', | |
get messagesAndUsers () { | |
return Promise.all([ | |
this.messagesPromise, | |
this.usersPromise | |
]) | |
}, | |
user: { | |
get: () => Session.current.user | |
}, | |
messagesPromise: { | |
default: () => Messages.getList({ $or: [{ from: Session.current.user._id }, { to: Session.current.user._id }] }) | |
}, | |
usersPromise: { | |
default: () => User.getList() | |
}, | |
sendMessage (event) { | |
event.preventDefault() | |
const toUser = this.users.usersByEmail[this.to] | |
if (toUser) { | |
new Messages({ name: this.msg, to: toUser._id }) | |
.save() | |
.then(() => { | |
this.msg = '' | |
this.to = '' | |
}) | |
} else { | |
console.log('No user found') | |
} | |
}, | |
updateMessage (event, message) { | |
event.preventDefault() | |
message.save() | |
}, | |
connectedCallback () { | |
// Listen to the session prop so we can load messages | |
// once we have an user | |
this.listenTo('user', (e, newVal) => { | |
// Load both users and messages and assign to VM | |
this.messagesAndUsers.then(([messages, users]) => { | |
this.messages = messages | |
this.users = users | |
}) | |
}) | |
} | |
} | |
}) |
The above is the ViewModel
for the MessageList
component. We create a usersPromise
and a messagesPromise
which will load in the initial messages and users for the page load. We need the users so we can map the email within the message to the users name.
We create a getter
which will Promise.all
both queries so we can load them both before rendering the list of messages. Using the connectedCallback
lifecycle method of the ViewModel
we create a listenTo
event listener, which will fire once a property changes. Once the current user is present on the ViewModel
we can then load the initial data.
Now that we have the initial data loaded, we can render this within the template. When we create new data or retrieve new data via sockets
the Message
model’s list will automatically be updated, and the data will update within the template!
Creating new messages
We can call new Message({ ...data })
to create a new instance, and calling .save()
will send this to the server and update our Message.List
. As this is a promise, we can .then
to reset the input bindings so the form is clear for another message.
new Message({ to: this.to, message: this.msg })
.save()
.then(() => {
this.to = ''
this.msg = ''
})
You can see the full repo here:
Thanks for reading.