20

How simultaneously to render a page and transmit my custom data to browser. As i understood it needs to send two layers: first with template and second with JSON data. I want to handle this data by backbone.

As i understood from tutorials express and bb app interact as follows:

  1. res.render send a page to browser
  2. when document.ready trigger jQuery.get to app.get('/post')
  3. app.get('/post', post.allPosts) send data to page

This is three steps and how to do it by one?

var visitCard = {
  name: 'John Smit',
  phone: '+78503569987'
};

exports.index = function(req, res, next){
  res.render('index');
  res.send({data: visitCard}); 
};

And how i should catch this variable on the page- document.card?

khex
  • 2,778
  • 6
  • 32
  • 56
  • 2
    AJAX is how you would talk to the server from an existing page without doing a full page request. It stands for Asynchronous JavaScript and XML, though you rarely see XML anymore since JSON has pretty much won that game. You may also see AJAX requests referred to as XHR (XML HTTP Request), such as in [Chrome's dev tools window](http://i.imgur.com/PwhxnHm.png). – CatDadCode Jan 21 '14 at 23:03
  • @AlexFord i played with JSON, but don't know that XHR in Chrome dev tools is XML HTTP Request. i haven't use it before because JSON win – khex Jan 21 '14 at 23:29
  • 1
    @khaljava I was saying that XHR is just another term for AJAX. It's only called XML Http Request because it was created before JSON was really a thing. XHR applies to JSON too, we just never renamed it to JHR :P – CatDadCode Feb 11 '14 at 21:49

5 Answers5

12

I created my own little middleware function that adds a helper method called renderWithData to the res object.

app.use(function (req, res, next) {
    res.renderWithData = function (view, model, data) {
        res.render(view, model, function (err, viewString) {
            data.view = viewString;
            res.json(data);
        }); 
    };
    next();
});

It takes in the view name, the model for the view, and the custom data you want to send to the browser. It calls res.render but passes in a callback function. This instructs express to pass the compiled view markup to the callback as a string instead of immediately piping it into the response. Once I have the view string I add it onto the data object as data.view. Then I use res.json to send the data object to the browser complete with the compiled view :)

Edit:

One caveat with the above is that the request needs to be made with javascript so it can't be a full page request. You need an initial request to pull down the main page which contains the javascript that will make the ajax request.

This is great for situations where you're trying to change the browser URL and title when the user navigates to a new page via AJAX. You can send the new page's partial view back to the browser along with some data for the page title. Then your client-side script can put the partial view where it belongs on the page, update the page title bar, and update the URL if needed as well.

If you are wanting to send a fully complete HTML document to the browser along with some initial JavaScript data then you need to compile that JavaScript code into the view itself. It's definitely possible to do that but I've never found a way that doesn't involve some string magic.

For example:

// controller.js
var someData = { message: 'hi' };
res.render('someView', { data: JSON.stringify(someData) });

// someView.jade
script.
    var someData = !{data};

Note: !{data} is used instead of #{data} because jade escapes HTML by default which would turn all the quotation marks into " placeholders.

It looks REALLY strange at first but it works. Basically you're taking a JS object on the server, turning it into a string, rendering that string into the compiled view and then sending it to the browser. When the document finally reaches the browser it should look like this:

// someSite.com/someView
<script type="text/javascript">
    var someData = { "message": "hi" };
</script>

Hopefully that makes sense. If I was to re-create my original helper method to ease the pain of this second scenario then it would look something like this:

app.use(function (req, res, next) {
    res.renderWithData = function (view, model, data) {
        model.data = JSON.stringify(data);
        res.render(view, model);
    };
    next();
});

All this one does is take your custom data object, stringifies it for you, adds it to the model for the view, then renders the view as normal. Now you can call res.renderWithData('someView', {}, { message: 'hi' });; you just have to make sure somewhere in your view you grab that data string and render it into a variable assignment statement.

html
    head
        title Some Page
        script.
            var data = !{data};

Not gonna lie, this whole thing feels kind of gross but if it saves you an extra trip to the server and that's what you're after then that's how you'll need to do it. Maybe someone can think of something a little more clever but I just don't see how else you'll get data to already be present in a full HTML document that is being rendered for the first time.

Edit2:

Here is a working example: https://c9.io/chevex/test

You need to have a (free) Cloud9 account in order to run the project. Sign in, open app.js, and click the green run button at the top.

CatDadCode
  • 58,507
  • 61
  • 212
  • 318
  • 1
    @Alex_Ford the first code is not working at all - i just receive JSON on window, but the second is very interesting and working solution. I just don't understand one thing - thats a useful feature and its still not in ExpressJS API? – khex Jan 20 '14 at 18:56
  • 1
    I think you don't really understand. Let me try to break it down in clear English and maybe that will help. The first example returns JSON data only. So yes, if you point your browser to it then all you'll get back is JSON data. The first example expects the request to be made from an existing web page with JavaScript code already running on it so it can make the AJAX request. – CatDadCode Jan 20 '14 at 19:01
  • Your browser makes one request to your server and expects to get back an HTML document. The only way you can also send JavaScript data to the browser with that document is if the JavaScript data is already embedded in the HTML document the browser receives. All Express does is give you a way to combine data with HTML markup. This is called "compiling views" and it all happens on the server. Express then sends that back to the browser as a fully-rendered HTML document. Your browser has no way to get HTML & JavaScript data without making two requests or the data already being in the markup. – CatDadCode Jan 20 '14 at 19:04
  • 1
    Hopefully that makes sense. In the first example we render a compiled view as a string property on the JSON data that we return. In the second example we do the opposite and we stick some JSON data inside the HTML markup instead. First example is for returning some data along with a partial view. Second is for full views along with some data. – CatDadCode Jan 20 '14 at 19:09
  • @Alex_Ford all clear, thank you. I already tried the second example before your answer, but it doesn't work because i don't guess to convert data to JSON. – khex Jan 20 '14 at 19:11
  • @khaljava You have to call `JSON.stringify(yourObj)` to turn your JavaScript objects into a string of JSON data. That string can then be rendered into your views like any other string. – CatDadCode Jan 20 '14 at 19:12
  • @khaljava See my final edit for a working example of both styles. – CatDadCode Jan 20 '14 at 21:03
9

My approach is to send a cookie with the information, and then use it from the client.

server.js

const visitCard = {
  name: 'John Smit',
  phone: '+78503569987'
};

router.get('/route', (req, res) => {
  res.cookie('data', JSON.stringify(pollsObj));
  res.render('index');
});

client.js

const getCookie = (name) => {
  const value = "; " + document.cookie;
  const parts = value.split("; " + name + "=");
  if (parts.length === 2) return parts.pop().split(";").shift();
};

const deleteCookie = (name) => {
  document.cookie = name + '=; max-age=0;';
};

const parseObjectFromCookie = (cookie) => {
  const decodedCookie = decodeURIComponent(cookie);
  return JSON.parse(decodedCookie);
};

window.onload = () => {
  let dataCookie = getCookie('data');
  deleteCookie('data');

  if (dataCookie) {
    const data = parseObjectFromCookie(dataCookie);
    // work with data. `data` is equal to `visitCard` from the server

  } else {
    // handle data not found
  }


Walkthrough

From the server, you send the cookie before rendering the page, so the cookie is available when the page is loaded.

Then, from the client, you get the cookie with the solution I found here and delete it. The content of the cookie is stored in our constant. If the cookie exists, you parse it as an object and use it. Note that inside the parseObjectFromCookie you first have to decode the content, and then parse the JSON to an object.

Notes:

  • If you're getting the data asynchronously, be careful to send the cookie before rendering. Otherwise, you will get an error because the res.render() ends the response. If the data fetching takes too long, you may use another solution that doesn't hold the rendering that long. An alternative could be to open a socket from the client and send the information that you were holding in the server. See here for that approach.

  • Probably data is not the best name for a cookie, as you could overwrite something. Use something more meaningful to your purpose.

  • I didn't find this solution anywhere else. I don't know if using cookies is not recommended for some reason I'm not aware of. I just thought it could work and checked it did, but I haven't used this in production.

Community
  • 1
  • 1
Daniel Reina
  • 5,764
  • 1
  • 37
  • 50
  • 2
    Note that this method is [up to 4Kbytes](https://stackoverflow.com/questions/640938/what-is-the-maximum-size-of-a-web-browsers-cookies-key), so it wouldn't work for all purposes. – ozgeneral Jun 03 '17 at 06:40
  • 1
    Excellent point - I like this script but there are limits so be careful when using it: http://browsercookielimits.squawky.net/ furthermore 4Kb is ~ 4000 characters http://extraconversion.com/data-storage-conversion-table/kilobytes-to-characters.html – Michael Nelles Sep 09 '19 at 15:03
  • thank you bro. also i can access directly cookie with this shorthand console.log(JSON.parse(decodeURIComponent(document.cookie).split('=')[1])); – Akif Kara Apr 04 '21 at 21:58
3

Use res.send instead of res.render. It accepts raw data in any form: a string, an array, a plain old object, etc. If it's an object or array of objects, it will serialize it to JSON for you.

var visitCard = {
  name: 'John Smit',
  phone: '+78503569987'
};

exports.index = function(req, res, next){
  res.send(visitCard}; 
};
Brandon
  • 9,822
  • 3
  • 27
  • 37
  • 2
    or by `res.json`. but i want to send data parallel with rendered page. this solution is only for data – khex Jan 16 '14 at 22:01
  • 1
    You changed your question after I answered. It's unclear what you are asking for now. – Brandon Jan 16 '14 at 22:59
2

Check out Steamer, a tiny module made for this this exact purpose.

https://github.com/rotundasoftware/steamer

Brave Dave
  • 1,300
  • 14
  • 9
  • That's pretty cool. Does kind of the same thing I was doing from scratch. I'll have to investigate; maybe I'll start using it instead of my helpers. – CatDadCode Feb 11 '14 at 21:47
0

Most elegant and simple way of doing this is by using rendering engine (at least for that page of concern). For example use ejs engine

node install ejs -s

On server.js:

let ejs = require('ejs');    
app.set('view engine', 'ejs');

then rename desired index.html page into index.ejs and move it to the /views directory. After that you may make API endpoit for that page (by using mysql module):

app.get('/index/:id', function(req, res) { 
    db.query("SELECT * FROM products WHERE id = ?", [req.params.id], (error, results) => {
    if (error) throw error;
        res.render('index', { title: results[0] }); 
    });
});  

On the front-end you will need to make a GET request, for example with Axios or directly by clicking a link in template index.ejs page that is sending request:

<a v-bind:href="'/index/' + co.id">Click</a>

where co.id is Vue data parameter value 'co' that you want to send along with request

Danny
  • 1
  • 2