example

simple

First pick a stream data source that will give you records and let you subscribe
to a changes feed. In this example we'll use
slice-file to read from a single text
file to simplify the example code.

Let's start with the rendering logic that will be used on both the client and
the server:

render.js:

var hyperspace =require('hyperspace');

var fs =require('fs');

var html =fs.readFileSync(__dirname+'/static/row.html');

module.exports=function(){

returnhyperspace(html,function(row){

return{

'.who':row.who,

'.message':row.message

};

});

};

The return value of hyperspace() is a stream that takes lines of json as input
and returns html strings as its output. Text, the universal interface!

We're doing fs.readFileSync() in this shared rendering code but we can use
brfs to make this work for the browser using
browserify. The callback to hyperspace() merely
takes row objects and returns
hyperglue mapping of css selectors to
content and attributes. Here we're updating the "who" and "message" divs
from the row.html which looks like:

row.html:

<divclass="row">

<divclass="who"></div>

<divclass="message"></div>

</div>

It's easy to pipe some data the renderer in stdout

var r =require('./render')();

r.pipe(process.stdout);

r.write(JSON.stringify({ who:'substack', message:'beep boop'})+'\n');

r.write(JSON.stringify({ who:'h4ckr', message:'h4x'})+'\n');

which prints:

<divclass="row">

<divclass="who">substack</div>

<divclass="message">beep boop</div>

</div>

<divclass="row">

<divclass="who">h4ckr</div>

<divclass="message">h4x</div>

</div>

To make the rendering code work in browsers, we can just require() the
shared render.js file and hook that into a stream. In this example we'll use
shoe to open a simple streaming websocket
connection with fallbacks:

browser.js:

var shoe =require('shoe');

var render =require('./render');

shoe('/sock').pipe(render().appendTo('#rows'));

If you need to do something with each rendered row you can just listen for
'element' events from the render() object to get each element from the
dataset, including the elements that were rendered server-side.

hooking up a data feed

Now our server will need to serve up 2 parts of our data stream: the initial
content list and the stream of realtime updates. We'll use
hyperstream to pipe content rendered
with our render.js from before into the #rows div of our index.html file.
Then we'll use shoe to pipe the rest of the
content to the browser where it can be rendered client-side.

then navigate to localhost:8000 where we will see our content. If we add some
more content:

$ echo '{"who":"substack","message":"oh hello."}' >> data.txt

$ echo '{"who":"zoltar","message":"HEAR ME!"}' >> data.txt

then the page updates automatically with the realtime updates, hooray!

We're now using exactly the same rendering logic on both the client and the
server to serve up SEO-friendly, indexable realtime content.

requesting more data

We can extend the previous example with a "more" button to load more content on
demand using the existing streams and rendering logic already in place.

We'll first supplement the rendering in server.js to parse incoming requests
offsets:

var sock =shoe(function(stream){

sf.follow(-1,0).pipe(stream);

stream.pipe(split()).pipe(through(function(line){

var offsets =JSON.parse(line);

sf.sliceReverse(offsets[0], offsets[1]).pipe(stream);

}));

});

Now when the browser sends us a json array [i,j], we'll send back the reversed
slice from data.txt at those indices.

However, now results arrive both from realtime updates and from requested
offsets on the same websocket stream so we'll need to add some additional data
to our data and rendering logic in render.js.

Add a <div class="time"></div> to row.html then set that element to
row.time:

module.exports=function(){

returnhyperspace(html,function(row){

return{

'.time':row.time,

'.who':row.who,

'.message':row.message

};

});

};

Now we can add a more button to the index.html and bind a click handler in the
browser.js to request more rows given the count of rows we've already
observed. The comparison function passed to .sortTo() will make sure that all
the results end up in the proper order no matter if they arrived from a realtime
update or a requested slice:

var shoe =require('shoe');

var render =require('./render')();

var count =0;

render.on('element',function(elem){ count ++});

var more =document.querySelector('#more');

more.addEventListener('click',function(ev){

stream.write(JSON.stringify([-count-3,-count ])+'\n');

});

var stream =shoe('/sock');

stream.pipe(render.sortTo('#rows', cmp));

functioncmp(a,b){

var at =Number(a.querySelector('.time').textContent);

var bt =Number(b.querySelector('.time').textContent);

return bt - at;

}

And now we have an seo-friendly, indexable feed with realtime updates and a
"more" button to load more content!

no more

For a simple extension to the previous example, we can remove the "more" button
once the end of the feed is reached by sending a false row in the result set
to specify a "no more" boundary.

In the server.js we can just pipe through an intermediary stream:

var shoe =require('shoe');

var sock =shoe(function(stream){

sf.follow(-1,0).pipe(stream);

stream.pipe(split()).pipe(through(function(line){

var offsets =JSON.parse(line);

sf.sliceReverse(offsets[0], offsets[1])

.pipe(insertBoundary(offsets[0], offsets[1]))

.pipe(stream)

;

}));

});

sock.install(server,'/sock');

functioninsertBoundary(i,j){

// add a `false` to the result stream when there are no more records

var count =0;

returnthrough(write, end);

functionwrite(line){ count ++;this.queue(line)}

functionend(){

if(count < j - i)this.queue('false\n');

}

}

then our render.js can emit a 'no-more' event when it finds a falsy row:

module.exports=function(){

returnhyperspace(html,function(row){

if(!row){

this.emit('no-more');

returnundefined;

}

return{

'.time':row.time,

'.who':row.who,

'.message':row.message

};

});

};

and we can listen for the 'no-more' event in browser.js:

render.on('no-more',function(){

more.parentNode.removeChild(more);

});

which removes the more button from the page when the end of the feed is
reached.

The complete code for this demo is in example/more.

methods

var hyperspace =require('hyperspace')

var render = hyperspace(html, opts={}, f)

Return a new render through stream that takes json strings or objects as input
and outputs a stream of html strings after applying the transformations from
f(row).

f(row) gets an object from the data source as input and should return an
object of hyperglue css selectors
mapped to content and attributes or a falsy value if nothing should be rendered
for the given row.

The html string must have a class defined in the top-level element so that
hyperspace can pick up on which elements were rendered server-side. For
example, in this html snippet row is necessary, although any class name
will work:

<divclass="row">

<spanclass="key"></span>:

<spanclass="value"></span>

</div>

If you pass in an opts.key, the value of row[opts.key] will be set as a
key attribute on each top-level element and rows that resolve to the same key
will update existing content. The attribute name to set is controlled by
opts.attr and defaults to 'key' when opts.key is set.

For example,for an opts.key of "key" and an opts.attr of 'data-key',
this html is generated for a row with: { key: 'abc' }:

<div data-key="abc" class="row">

<span class="key"></span>:

<span class="value"></span>

</div>

In the browser, when a row comes in with a row.key that matches a key that has
already been seen, and 'update' event will fire instead of an 'element'
event and the contents of the row dom node will be updated in place instead of
creating a new element from the html string.

opts.key can be a string, a function (row) {} that returns the keyname to
use for a given row, or an array of strings that define a path to the key from
the row root.

browser methods

These methods only apply browser-side because they deal with how to handle the
realtime update stream.

render.appendTo(target)

Append the html elements created from the hyperspace transform function
f(row) directly to target.

target can be an html element or a css selector.

render.prependTo(target)

Prepend the html elements created from the hyperspace transform function
f(row) directly to target.

target can be an html element or a css selector.

render.sortTo(target, cmp)

Insert the html elements created from the hyperspace transform function
f(row) to target using the sorting function cmp(a, b) for each html
element a and b to be sorted.

target can be an html element or a css selector.

If cmp is undefined but an opts.key has been set, the opts.key top-level
attribute will be used for the comparison.

If cmp is a string, it will be interpreted as a query selector that will
traverse down to the textContent of an element. If both textContent strings look
like numbers, a numeric comparison will be used. Otherwise, a string comparison
is used. You can negate the string match by using a leading '~' character in
the query selector string.

browser events

render.on('element', function (elem) {})

This event fires for all elements created by the result stream, including those
elements created server-side so long as .prependTo() or .appendTo() as been
called on the same container that the server populated content with.

render.on('update', function (elem) {})

When an opts.key was configured and a new row comes in with a matching key to
an existing element, the 'update' event fires instead of 'element'.

render.on('delete', function (elem) {})

If a row comes along with a row.type of "del" and opts.key was set, the
row.key will be used to index the element and the element will be removed from
the page and the 'delete' event fires with the element reference.

render.on('parent', function (elem) {})

This event fires with the container element when .appendTo(), .prependTo(),
or .sortTo() is called.