Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

migration .0.4.42 to 0.7.0 each pages #8

Open
rparree opened this issue Dec 17, 2015 · 17 comments
Open

migration .0.4.42 to 0.7.0 each pages #8

rparree opened this issue Dec 17, 2015 · 17 comments

Comments

@rparree
Copy link
Contributor

rparree commented Dec 17, 2015

My '{{#each pages }}' no longer works. Guessing from the fact collections became first class citizens i tried creating a view collection:

    app.create("blog")
    app.blogs('./src/templates/blog-pages/**');
    return app.toStream("blogs")
        .pipe(app.renderFile('*'))
        .pipe(extname())
        .pipe(app.dest('./build/public/blog'));

I have tried many variations like {{#each blog }} etc. Also my {{#each categories}} is not working.

@rparree
Copy link
Contributor Author

rparree commented Dec 18, 2015

This is the page i am currently migrating http://www.edc4it.com/blog/index.html.

In Assemble 0.4.42 the following works (out of the box)

For the category buttons (each blog has categories in the front matter)

{{#each categories}}
  <label class="btn btn-{{category}} ">
    <input class="chkbx-filter" type="checkbox" autocomplete="off" value=".cat-{{category}}">{{category}}
  </label>
{{/each}}

To render to the blog tiles

{{#each pages  }}
    {{#is data.published true}}

        <a href="{{relative ../../page.dest this.dest}}" >
             <div class="grid-item {{#each data.categories}} cat-{{this}} {{/each}}"
                   data-date="{{formatDate data.date '%Y%m%d'}}" data-categories="{categories}}">
                                <span class="title">{{data.title}}</span>
             </div>
         </a>
     {{/is}}
{{/each}}

I found some related issues

@jonschlinkert
Copy link
Member

Guessing from the fact collections became first class citizens

kind of, but primarily it's just because the properties on the context object are no longer the same. the following might cast some perspective on all of the linked issues.

(TLDR: depending on the type of "list" you want to generate with the each helper, you might want to use lists or collections. Assemble has "lists" (arrays) as well as "colllections" (objects). A list might work better for what you need. See these unit tests for examples of how to work with lists, and do things like pagination. Once these lists are generated, it should be fairly trivial to use helpers to render them.)

How Assemble 0.6.0 is different than 0.4.x

In grunt-assemble (assemble 0.4.x), we were adding both the context for the current page, AND the pages array to the context.

The implication being that we needed to:

  • read in all files first,
  • then process the files
  • expose each file's context, "global" context, and pages context (for pagination etc)

There were advantages to this, like making it easier to do simple things with pagination, use the each helper to build pages lists, etc. But the downside was that memory management is difficult or impossible. The more pages, the slower it got. (we now of some users that build sites with 25k pages or more, and it gets very slow)

In Assemble 0.6.0, we don't assume that this is always what the user wants or need, but it's still possible (and maybe easier in some ways).

Regardless of how we approach the solution, to generate a list of pages (or posts, or widgets, etc), the entire "list" must be loaded first. Once that's done, we can easily render the list using helpers. If you're using app.src() to load views, then (by default) you won't see the entire list when you render, since they haven't all been loaded yet.

However, this is easily solved by building up the list in the flush function of a plugin, or by not using app.src() to load pages. Instead, we can use the .toStream() method.

Example

Try something like the following in your assemblefile.js

/**
 * Helper for showing the context in the console
 */

app.helper('log', console.log.bind(console));

/**
 * Middleware 
 * 
 * Add the `pages` collection to `view.data`,
 * which exposes it to the context for rendering
 */

app.preRender(/./, function(view, next) {
  view.data.pages = app.views.pages;
  next();
});

/**
 * Task for rendering "site"
 */

app.task('site', function() {
  app.pages('src/pages/**/*.hbs');
  app.partials('src/partials/*.hbs');
  app.layouts('src/layouts/*.hbs');

  // use the `toStream` method instead of `src`
  // so that all pages are available at render time
  return app.toStream('pages')
    .pipe(app.renderFile())
    .pipe(extname())
    .pipe(app.dest('_build'));
});

Layout: default.hbs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
  </head>
  <body>
    {% body %}

    {{> list }}
  </body>
</html>

Partial: list.hbs (or you could add this inline in the layout)

{{#each pages}}
{{log .}}
{{@key}}
{{/each}}

Inspecting the context

Since views are vinyl files, you'll need to inspect them to see what's available to use. To make this easier, you might also try adding a helper to see what's on the context:

Example

Create a helper, arbitrarily named ctx (for context) or whatever you want, and add it to your assemblefile.js:

app.helper('ctx', function(context) {
  console.log(arguments);
  console.log(context);      // the object passed to the helper
  console.log(context.hash); // hash arguments, like `foo="bar"`

  console.log(this);         // handlebars context
  console.log(this.options); // assemble `options`
  console.log(this.context); // context of the current "view"
  console.log(this.app);     // assemble instance
});

Then use the ctx helper inside the {{#each}} loop:

{{#each pages}}
  {{ctx .}}
{{/each}}

And try it outside the loop:

{{ctx .}}

@rparree
Copy link
Contributor Author

rparree commented Dec 18, 2015

Thanks for the excellent explanation. I was indeed fairly easy to push in collection into the view's data. The difficulty was mostly in creating the relative link to the page. The destination for the outer page has not yet been set when rendering the page (this.context.view.path still points to the template file of the outer page, whereas context.path already has the current's destination path during the iteration). I wrote a helper function absurl where i can pass in the base for the build:

// hack for now
module.exports =  function(basedir){
    return function(context){
        return "/"+path.relative(basedir,context.path)
    }
};

It works, but now have to enable permalinks and see if still works. I forgot i also have that obstacle: permalinks. (that then need to create and push the category collection into the page as well)

Thanks again for your thorough explanation.

@jonschlinkert
Copy link
Member

The difficulty was mostly in creating the relative link to the page

try this (ironically I just created this, literally right before I read your message! lol):

// somewhere on the assemble options, define a `dest`
app.option('dest', '_build');

// relative path helper
app.helper('relative', function(item) {
  var view = this.context.view; // this is the "current" view being rendered
  var from = rename(this.options.dest)(view); 
  var dest = rename(this.options.dest)(item);
  return relative(from, dest);
});

// "rename" function 

function rename(dest) {
  return function(file) {
    // return if dest is defined, so we don't calculate the 
    // dest path for a file more than once
    if (file.dest) return file.dest;
    var fp = path.join(dest || file.base, file.relative);
    file.dest = fp.replace(path.extname(fp), '.html');
    return file.dest;
  };
}

Also, add hbs to the renderFile() method in the task, to force the hbs engine to be used on .html files:

.pipe(app.renderFile('hbs'))

Then define the following in your template:

{{#each pages}}
<a href="{{relative .}}">{{data.title}}</a>
{{/each}}

@rparree
Copy link
Contributor Author

rparree commented Dec 18, 2015

Thanks..

Could you not just use context.path as that already points to the destination of the current "each" view? Also would this not conflict with using gulp's extname and perhaps the assemble permalinks?

@jonschlinkert
Copy link
Member

Meaning item.path from the helper arguments (e.g. the context passed to the helper)?

That won't work for two reasons:

  • the files haven't all come through yet
  • assemble.dest() has not renamed the files yet

This is why I'm using a custom rename function and intentionally avoiding updating any vinyl properties.

You would see the following if you use item.path (with an added line break to make it easier to see what's happening):

/Users/jonschlinkert/dev/assemble-collections/src/pages/context-from-inline.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/context-from-page.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/context-from-partial.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/index.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/sub-folder/index.hbs

/Users/jonschlinkert/dev/assemble-collections/_build/context-from-inline.html
/Users/jonschlinkert/dev/assemble-collections/src/pages/context-from-page.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/context-from-partial.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/index.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/sub-folder/index.hbs

/Users/jonschlinkert/dev/assemble-collections/_build/context-from-inline.html
/Users/jonschlinkert/dev/assemble-collections/_build/context-from-page.html
/Users/jonschlinkert/dev/assemble-collections/src/pages/context-from-partial.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/index.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/sub-folder/index.hbs

/Users/jonschlinkert/dev/assemble-collections/_build/context-from-inline.html
/Users/jonschlinkert/dev/assemble-collections/_build/context-from-page.html
/Users/jonschlinkert/dev/assemble-collections/_build/context-from-partial.html
/Users/jonschlinkert/dev/assemble-collections/src/pages/index.hbs
/Users/jonschlinkert/dev/assemble-collections/src/pages/sub-folder/index.hbs

/Users/jonschlinkert/dev/assemble-collections/_build/context-from-inline.html
/Users/jonschlinkert/dev/assemble-collections/_build/context-from-page.html
/Users/jonschlinkert/dev/assemble-collections/_build/context-from-partial.html
/Users/jonschlinkert/dev/assemble-collections/_build/index.html
/Users/jonschlinkert/dev/assemble-collections/src/pages/sub-folder/index.hbs

@rparree
Copy link
Contributor Author

rparree commented Dec 18, 2015

That explains why the "index" page was still referring to the template.

But would this solution not conflict with gulp extname / permalinks / gulp rename?

@jonschlinkert
Copy link
Member

As long as the same destination path is generated in both places it should work. I don't use gulp-rename, but out of curiosity what permalinks solution are you using?

@rparree
Copy link
Contributor Author

rparree commented Dec 18, 2015

Nothing yet i am trying to figure out assemble-permalinks. I was expecting i could just use that on my view collection. Working my way through the tests and example on that repo. (BTW one tests fails, i'll report it over there)

@jonschlinkert
Copy link
Member

sounds good, thx

@jonschlinkert
Copy link
Member

@doowb put that posts helper example up here! lol it's a good one

@doowb
Copy link
Member

doowb commented Dec 18, 2015

@rparree @jonschlinkert is referring to this...

app.helper('posts', function(options) {
  var list = this.app.list(this.app.views.posts);
  return list.items.map(function(post) {
    return options.fn(post.data);
  }).join('\n');
});

I haven't completely tested it yet, but I think that will get you close to what you're looking for on your tiled posts page...

<ul>
{{#posts}}
  <li>{{title}}<li>
{{/posts}}
</ul>

@jonschlinkert
Copy link
Member

much better solution! but now you see multiple ways to do it :)

@rparree
Copy link
Contributor Author

rparree commented Dec 18, 2015

That looks very clean, i'll give it a try tomorrow...

Thanks guys!

@rparree
Copy link
Contributor Author

rparree commented Dec 19, 2015

I've tried this approach but it does not work for me.

The hbs

{{#posts}}
    {{title}}

{{/posts}}

The helper:

app.helper('posts', function(options) {
    var list = this.app.list(this.app.views.blogs); // shows list {"options":{"/home/rparree/projec....
    console.log("list", JSON.stringify(list.items))  // shows []
    return list.items.map(function(post) {
        return options.fn(post.data);;
    }).join('\n');
});

The list has values, it's items not.

@rparree
Copy link
Contributor Author

rparree commented Dec 20, 2015

BTW for reference...the list of catagories i've solved like this:

 {{#categories}}
   {{category}}
{{/categories}}

The helper

"categories" : function(options) {
    var cats = _.chain(this.app.views.blogs)
        .values()
        .map(function(v){return v.data.categories})
        .flatten()
        .uniq()
        .value()
    return cats.map(function(cat){
        return options.fn({category : cat})
    }).join('\n')

I guess i can do something similar for an improved page list

@jonschlinkert
Copy link
Member

that's great! I'd love to see what you come up with for the page list too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants