All Articles

CanJS & FeathersJS Channels

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:

FeathersJS, Channels & Vuex

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
view raw user-model.js hosted with ❤ by GitHub

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
view raw session-model.js hosted with ❤ by GitHub

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
view raw messages-model.js hosted with ❤ by GitHub

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
})
})
}
}
})
view raw message-list--vm.js hosted with ❤ by GitHub

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:

Mattchewone/realtime-canjs

Thanks for reading.