queckezz/koa-views

set defaultLayout and partials render questions in koa2 koa-views hbs

windbell2 opened this issue · 16 comments

1,How to set defautlayout with handlebars
2,my code:

app.use(views(__dirname+"/views", {
    extension: 'hbs',
    map: { hbs: 'handlebars' },
    options: {
        partials: {
            error: './error' 
        }
    }
}));

routes/index.js

var router = require('koa-router')();
router.get('/', async function (ctx, next) {
  ctx.state = {
    title: 'koa2 title'
  };

  await ctx.render('index', {
  });
})
module.exports = router;

views/index.hbs

{{> error }}

<h1>{{title}}</h1>
<p>Welcome to {{title}}</p>

first time render ok,
but, when i refresh the browser.display an error:

  xxx GET / 500 12ms -
{ Error: ENOENT: no such file or directory, open 'C:\study\koa2\koa2-hbs\views\error

<h1>{{message}}<\h1>
<h2>{{error.status}}<\h2>
<pre>{{error.stack}}<\pre>
.hbs'
    at Error (native)
  errno: -4058,
  code: 'ENOENT',
  syscall: 'open',
  path: 'C:\\study\\koa2\\koa2-hbs\\views\\error\n\n<h1>{{message}}<\\h1>\n<h2>{{error.status}}<\\h2>\n<pre>{{error.stack}}<\\pre>\n.hbs' }

Did you find a solution to this? I'm getting the same thing, initial load is fine but when I then refresh I get this error:

Error: ENOENT: no such file or directory, open '/home/joe/Documents/LSDM/views/<head>
    <title>{{pageTitle}}</title>
    <meta name="description" content="Awesome Description Here">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link rel="stylesheet" type="text/css" href="/base.css" />
  </head>
  .hbs'

My code:

import Koa from 'koa';
import Router from 'koa-router';
import serve from 'koa-static';
import views from 'koa-views';
import path from 'path';

const app = new Koa()
  .use(serve('./res'))
  .use(views(path.resolve(__dirname, '../views'), {
    map: {
      hbs: 'handlebars',
    },
    extension: 'hbs',
    options: {
      partials: {
        Head: './partials/head',
        Header: './partials/header',
      },
    },
  }));

const router = new Router()
  .get('/', async (ctx) => {
    await ctx.render('index', {
      pageTitle: 'London School of Digital Marketing | Home',
    });
  });

app.use(router.routes());
app.listen(3000);

Moving the partials into the render method seems to have fixed the problem:

import Koa from 'koa';
import Router from 'koa-router';
import serve from 'koa-static';
import views from 'koa-views';
import path from 'path';

const app = new Koa();

app.use(serve('./res'));
app.use(views(path.resolve(__dirname, '../views'), {
  map: {
    hbs: 'handlebars',
  },
  extension: 'hbs',
}));

const router = new Router()
  .get('/', async (ctx) => {
    await ctx.render('index', { 
      title: 'London School of Digital Marketing | Home',
      partials: {
        Head: './partials/head',
        Header: './partials/header',
      },
    });
  });

app.use(router.routes());
app.listen(3000);

this will work for now but isn't ideal

and in new version 'partials' did not work at all...

still an issue in 5.2.0, I've started using Object.assign as a decent workaround:

const defaultPartials = {
  Layout: './partials/layout',
  Header: './partials/header',
  HeaderLogo: './partials/header-logo',
  HeaderLinks: './partials/header-links',
  Footer: './partials/footer',
};

const router = new Router()
  .get('/', async (ctx) => {
    await ctx.render('home', {
      pageTitle: 'Home | London School of Digital Marketing',
      partials: Object.assign({}, defaultPartials, {
        Fold: './partials/fold',
        Diagnostic: './partials/diagnostic',
      }),
    });
  })

so it seems like this is by design in consolidate:

tj/consolidate.js#100 (comment)

@sanpoChew This method dosen't solve that problem, my koa-views version is 5.2.0. It still have this problem. Even, you post another solution it dosen't work.

Getting this same problem still...

@ianstormtaylor I already give up this module. if you use handlebars as a templet, I suggest you chosing koa-hbs. It also support koa2.

PRs welcome. I don't use koa personally anymore, so this is not something that's on my todo list. If anyone wants to step up as a maintainer, feel free!

@queckezz i think i just findout somewhere faulted, let me join it

OFF

If you really want to use Handlebars like templating in Koa v2 and most of the modules not supporting partials yet (like this one), try Nunjucks (made by Mozilla). It does what Handlebars do, but it's much better (and I believe slightly faster). The partials logic handled in the templates, meaning you don't need specific package that's supporting it (or special support from koa-views). I wrote my lightweight Nunjucks parser which does not requires koa-views package for Koa v2: https://gist.github.com/DJviolin/4111b463daf973b6867164a2337c1a75


Or if you are crazy enough, you can try to use template literals, which is part of the standard library. koa-views also not required, because all of your rendering outputted by calling ctx.body (Koa) or res.send (Express) (like what every templating library does, including the upper parser script):
https://github.com/DJviolin/template-literals-demo
Be advised that because all of your templating logic is written by you in this case (like a css), there aren't any standard yet what are the best practices for this. One thing to note that complex expressions like for loops not working in template literals, because that's out of the scope what it was planned for. But you can write a function, place it in a file and reference with require or import. For example I wrote this loop component, and calling by this line in /views/index.js:

<ul>
  ${loop('<li>Number ', state.array, '</li>')}
</ul>

Where state.array is also a function argument from the router which is an array of numbers.

So you have to slice your project to little parts like in React (which is not a bad thing on the long term).

Koa is great because of ctx.state (which is not in Express), simply the best thing ever, because helps a lot ditching callback hell. One thing to not that if you use template literals, ctx.state not passing it's value to rendering, so you have to reference ctx.state's object in ctx.body like in Express. But this doesn't mean ctx.state not storing it's value, for example:

router.get('/', async (ctx) => {
  await db.one('SELECT version() as VALUE;', {}, v => v.value)
    .then((value) => {
      ctx.state = { title: value }; // initialization (making sure it's empty)
    })
    .catch((error) => {
      console.log('ERROR:', error); // print the error
      ctx.body = '::DATABASE CONNECTION ERROR::';
    });

  ctx.body = await index({
    welcome: ctx.state.title,
  }, {
  obj: meta });
});

In a properly setted up rendering module the ctx.state object initialization passing it's value to the rendering templates (like Handlebars, Nunjucks). But in the case of template literals, because there's no any templating setup, it's not happening. Everything is pure Javascript standard library (as far as I know, any other language can do that is maybe just PHP).

Too bad that many libraries still not supporting Koa v2 and Node v7.6.0 (with async/await support without --harmony flag) around the corner just a few days ahead. But you doesn't need to ditch Koa v2 because of cumbersome support, you have options.

ON

Yeah i started using koa-hbs as well

I've got to say though, using template literals is something I've never even considered before. Might have a play with it. I was planning to try something other than Handlebars for future projects anyway.

@sanpoChew I'm just experimenting with it as well. Worth it to note that some things much easier then a templating engine, some more complex (like for loops, if you have custom structure, placing multiple objects in it, every time you have to write a custom function for it). But for the plus side, you will have a proper error handling (try/catch block in my loop.js). Because you will write the entire logic of the loop (choose between for or while). This is not possible with any other templating engine.

One of the easiest parts that you still just writing plain Javascript. For example, I also included simple translation logic in my demo. The site language should have a default value (en-US), when not referenced in a router (otherwise it will give you undefined). I just used a simple OR operator and it works:

<html class="no-js" lang="${obj.lang || 'en-US'}">

One of the hardest thing is passing down a value from the router to 2 or 3 levels deep without referencing a template file more than once (like you have the page template part which is getting the data from the router, which is in the main layout, which is calling the header, and the header also should have data from the router). My solution was to use objects in the function arguments. This way I can daisy chain objects from the router all the way down to a partial. I'm not sure though in a much more complex project, with hundreds of partials, components, how easy would be this to manage.

Implementing helpers: I not figured out yet how to do this properly without breaking the compatibility with another modules like i18n-node (I'm not sure though maybe this package offer another way of translation then helper logic), etc. Right now my "helper" solution is that if I want to convert a text to uppercase, I will do this in the router, with template literals:

// http://127.0.0.1:3000/helpers
router.get('/helpers', async (ctx) => {
  const names = ['Steve', 'John', 'Peter', 'Jack', 'István'];
  const randomName = names[Math.floor(Math.random() * names.length)];

  ctx.body = await index({
    welcome: `${randomName}!!!`, // Use TL in a router like if it was a helper
    num: 2,
  }, {
    obj: meta,
  });
});

So template literals, objects and function arguments are all the way down the building blocks of such native rendering.

Yes, you can see this movement also in a lot of frontend frameworks like for example choo or hyperapp

@DJviolin, can your loop component not also just be expressed simply by using an array.map?

module.exports = `
  <ul>
    ${array.map((item) => `<li>${item}</li>`)}
  </ul>
`

So koa-views is not really worth the abstraction since you are not really swapping out template engines constantly. Choose one, stick with it. But it still seems a lot of people want this. If you decide to change, swapping a module like koa-swig to koa-hbs is also not difficult.

Also, if your using libraries like React you are doing SSR anyway and template engines are also redundant:

const render = (req, res) => {
  const vdom = render(
    <Shell title='Demo App' assets={assets}>
      <StaticRouter location={req.url} context={ctx}>
        <App />
      </StaticRouter>
    </Shell>
  )

  res.send('<!DOCTYPE html>' + vdom)
}

@queckezz Yes, array.map will work, the first loop implementation that I found here used this:

${Array(5).join(0).split(0).map((item, i) => `
  <div>I am item number ${i}.</div>
`).join('')}

However I decided to move the loop in a function (I can write any javascript logic there, like a for loop, error checking if the object missing), because this way I can keep clean my page template function, which is doing the rendering).

My whole idea is if I need to write a complex function for some part, I will place that function in the same file's head or referencing with require from another file (slicing up my project to little parts). This way maybe that loop is portable, because I can use multiple times. In practice, this probably won't be the case, but than I write a loops.js and I call:

${loops.simple('<div>', state.array, '</div>')}

This is just an aesthetic choice.


I also wrote a function where I placed the function arguments in the html tags with regex (to create a universal loop function), but I dropped this idea, because it was 300-400% slower. The <<>> parts will be replaced with the 2nd, 3rd function argument, because a slicing will happen there:

${loops.regex('<li class="<<>>">The number is <<>>!!!</li>', state.className, state.array)}

Well, in theory, we can write a module in C++, Rust for this regex match and return the final string, but don't be that crazy because of a loop. :)

Or new idea, using dotted function arguments, like ...args instead of named arguments and measuring the arguments length and placing the objects in the html tags in the order where they presented in the function arguments (untested).