visit
Recently, I wrote an article about using REST over WebSockets which generated quite a bit of interest—So I would like to use this opportunity to follow up with a practical example.
A while ago, I created a basic sample app to demonstrate an approach to building single-page real-time apps; see . The sample app demonstrates several key features that one might like to have in a real-time single page app including:In standard (not real-time) REST/HTTP, as a user, you simply wouldn’t know if someone else was trying to edit the same resource at the same time— You might find out later if you come back to that resource and notice that your changes have been overwritten with old data.
With real-time CRUD, because changes happen in real-time one field at a time, other concurrent users can see your changes in their browsers as they are made and therefore won’t unintentionally overwrite them. This approach doesn’t support Google Docs-style collaborative text editing by default (E.g. for large text-areas) but it’s suitable for 99% of cases (if you consider the form above, you wouldn’t benefit at all from allowing multiple users to edit individual input fields at the same time). For those rare cases that do require collaborative text-editing on a large body of text, you can use a transport-agnostic plugin like (or one of its more recent ).
On the front-end, data can be represented either as a collection ( sc-collection
) or a field ( sc-field
). A collection is essentially an array of empty placeholder resources with only an ID field (e.g. [{id:”14443ea2-b553-4195-9220-bce456c8281b"}, ...]
) and a field holds values for an individual property of a resource (associated with a resource ID). You can automatically attach sc-field
objects to each placeholder within an sc-collection
and they will automatically populate the placeholder with the appropriate field values from the server (more on that later).
On the back-end, the current implementation is built with SocketCluster/Node.js and RethinkDB, to use it, you can simply attach the sc-crud-rethink
module like :
As a user, if another user makes a change to a specific field of a resource that you are also using, it will be updated in real-time on your front-end as well. Conversely, if you make a change to your sc-field
model by invoking a on it:
To construct an sc-field
on the front end, you need to do it like :
In Polymer, by default, data can flow either in or out of components so tag attributes can be used either as an input to the component or to capture output from it. In this case, the resource-value
attribute causes data to flow out into the specified property of the parent component (, , , ) — This resource-value
output property can then be used as an input to other front-end component and it will update those other components in real-time. All other attributes of the sc-field
model component are used as input to the sc-field
and tell it which resource and field to hook up to on the back end.
You can add a new resource to an sc-collection
by invoking the on it:
Just like with sc-field
, all model updates will be propagated to the database and to all interested users in real-time.
As mentioned before, a collection is just a list of empty placeholder objects with an ID property. To fill-out the collection with data on the front end, you need to attach sc-field
components to it. To do this, you just need to iterate over each item in the sc-collection
and bind a property of the item/placeholder to an sc-field
for the same resource. See :
<sc-field
id$="{{item.id}}-qty"
resource-type="Product"
resource-id="{{item.id}}"
resource-field="qty"
resource-value="{{item.qty}}">
</sc-field>
<sc-field
id$="{{item.id}}-name"
resource-type="Product"
resource-id="{{item.id}}"
resource-field="name"
resource-value="{{item.name}}">
</sc-field>
</template>
^ Here we’re iterating over each placeholder ( item
) in the sc-collection and binding each sc-field
to a matching property on that item
(see the resource-value
attribute on sc-field
components above — Remember, the resource-value
is an output attribute and will set item.xxxx to whatever value the sc-field
gets from the server).
Transformations (or sorting and filtering in REST-speak) are done using views that are defined on the back end (as part of your object)—Views are defined under resource definitions like (in the following snippet, we are only exposing one view alphabeticalView which provides a sorted list of Category resources):
// --- BEGIN VIEW DECLARATION ---
// Here we define a view for the 'Category' sc-collection
// ordered alphabetically.
// We use RethinkDB's ReQL to do the transformation.
views: {
**alphabeticalView**: {
transform: function(fullTableQuery, r) {
return fullTableQuery.orderBy(r.asc('name'));
}
}
},
// --- END VIEW DECLARATION ---
// This is an auth filter for the 'Category' resource to
// decide which CRUD read/write operations to block/allow.
// DO NOT CONFUSE THIS WITH COLLECTION TRANSFORMATION.
filters: {
pre: mustBeLoggedIn
}
},
// ...
Then, on the front end, you can specify what view of the Category collection to use with the resource-view
attribute of the sc-collection
component like :
<sc-collectionid="categories"resource-type="Category"resource-value="{{categories}}"resource-view="alphabeticalView"resource-view-params="null"></sc-collection>
^ If you don’t provide a resource-view
, sc-crud-rethink will fetch the raw (non-transformed) collection.
Product: {fields: {id: type.string(),name: type.string(),qty: type.number().integer().optional(),price: type.number().optional(),desc: type.string().optional(),// The category ID field is used as a parameter to transform// the collection for the categoryView defined below.category: type.string()},views: {categoryView: {// Declare the fields from the Product model which// are required by the transform function.paramFields: ['category'],transform: function(fullTableQuery, r, productFields) {// Because we declared the category field above, it is// available in here inside productFields.// This allows us to tranform the Product collection// based on a specific category ID.// Only include products that belong to the category ID// provided by the client as part of this view.return fullTableQuery.filter(r.row('category').eq(productFields.category)).orderBy(r.asc('qty'))}}},filters: {pre: mustBeLoggedIn,post: postFilter}}
^ The productFields.category
property from the code above will hold the category ID which was provided by the resource-view-params
attribute on the front end like :
<sc-collectionid="category-products"resource-type="Product"resource-value="{{categoryProducts}}"resource-view="categoryView"resource-view-params="{{categoryViewParams}}"resource-page-offset="{{pageOffsetStart}}"resource-page-size="{{pageSize}}"resource-count="{{itemCount}}"></sc-collection>
The value of the resource-view-params
attribute (categoryViewParams
in our case) must be a plain JavaScript Object whose properties exactly match those that were specified in paramFields
in the categoryView
definition on the back end, so on the front end, we’re computing it like :
computeCategoryViewParams: function (categoryId) {return {category: categoryId};}
Whatever ID we provide as category
on the front end will be used as the value in productFields
inside the transform function on the back end (which we use as an argument to our ReQL query). This part (as shown earlier):
return fullTableQuery.filter(r.row('category').eq(productFields.category))
The fullTableQuery
is the ReQL query which fetches the entire table/collection — ReQL allows us to easily extend this query with additional filters and sorting rules so we can filter our products based on their category IDs as shown above.