Assemble - How do I limit a collect to display X posts? - handlebars.js

Details:
Assemble: 0.4.4
Grunt: 0.4.1
Question:
I'm designing a blog where I want to put 5 of the most recent posts on the front page. I've created a collection for my posts based on keywords:
assemble: {
options: {
flatten: false,
partials: '<%= build.src %>/_partials/*.hbs',
layoutdir: '<%= build.src %>/_layouts',
data: ['<%= build.src %>/_data/*.{json,yml}', 'package.json'],
assets: '<%= build.out %>/',
helpers: [ 'helper-moment','<%= build.src %>/helpers/helper-*.js'],
collections: [
{ name: 'keywords', inflection: 'keyword' }
]
},
YAML front matter on the various posts look similar to this:
--
layout: default.hbs
title: <%= site.title %>
description: "Adult Redeploy All Sites Summit 2015"
dateCreated: 06-23-2014
slug: "Welcome"
breadCrumbs: false
posted: 01-12-2014
keywords:
news
navSort: 100
--
My code to display the titles and summaries is this:
<div>
{{#each keywords}}
{{#is keyword "news"}}
{{#withSort pages "data.posted" dir="desc"}}
<div>
<h2>{{data.title}}</h2>
<p>{{formatDate data.posted "%F"}}</p>
<div>
{{#markdown}}{{data.summary}}{{/markdown}}
</div>
<p>more...</p>
</div>
{{/withSort}}
{{/is}}
{{/each}}
</div>
This works. It displays all the blogs no problem. But I want to limit to 5 -- the five most recent.
I've looked at this issue:
https://github.com/assemble/assemble/issues/463
But I'm not sure how to incorporate it into the example above. Is there a way to limit the pages #withSort?
Confused.

Had the same problem and ended up creating a helper for it.
var _ = require('underscore');
var helpers = {
latest: function(array, amount, fn) {
var buffer = "";
_.chain(array)
.filter(function(i) {
return i.data.date;
})
.sortBy(function(i) {
return i.data.date;
})
.reverse()
.first(amount)
.forEach(function(i) {
buffer += fn.fn(i);
});
return buffer;
},
};
module.exports.register = function(Handlebars, options) {
options = options || {};
for (var helper in helpers) {
Handlebars.registerHelper(helper, helpers[helper]);
}
return this;
};
Add it somewhere along assemble.helpers search path. Make sure you have underscore dependency installed
npm install underscore --save-dev --save-exact
Then you can use the helper like this
<ul>
{{#latest pages 5}}
<li>{{data.title}}</li>
{{/latest}}
</ul>

There's good news and bad news. The bad news is that I don't believe there is a built-in helper that both sorts and limits the pages collection, nor can you piece two of them together in a pipeline.
Edited: I was wrong, there may be a built-in way combining collection sorting and the withFirst helper. I'll make a separate answer.
The good news is that you can write your own custom Handlebars Helper to do this. I wrote a sample helper below based on the withSort Helper. You would use it like this:
<div>
{{#each keywords}}
{{#is keyword "news"}}
{{#withSortLimit pages sortBy="data.posted" dir="desc" limit=5}}
<div>
<h2>{{data.title}}</h2>
<p>{{formatDate data.posted "%F"}}</p>
<div>
{{#markdown}}{{data.summary}}{{/markdown}}
</div>
<p>more...</p>
</div>
{{/withSortLimit}}
{{/is}}
{{/each}}
</div>
withSortLimit.js
Here is the no-frills source to the withSortLimit helper. You will need to register this in your Gruntfile's Assemble configuration as described in the options.helpers docs.
/*
* withSortLimit Handlebars Helper for Assemble
* Sample usage:
* {{#withSortLimit pages sortBy="data.posted" dir="desc" limit=5}}
* <li>{{formatDate data.posted "%F"}}: {{data.title}}</li>
* {{/withSortLimit}}
*/
(function() {
var _ = require("lodash");
function getPropertyFromSpec(obj, propertySpec) {
var properties = propertySpec.split('.');
var resultObj = obj;
_.each(properties, function (property) {
if (resultObj[property]) {
resultObj = resultObj[property];
}
});
return resultObj;
}
module.exports.register = function(Handlebars, options) {
Handlebars.registerHelper("withSortLimit", function(collection, options) {
var result = "";
var selectedPages = collection;
// Sorting
var sortProperty = options.hash.sortBy || "basename";
selectedPages = _.sortBy(collection, function (item) {
return getPropertyFromSpec(item, sortProperty);
});
if (options.hash.dir && options.hash.dir === "desc") {
selectedPages = selectedPages.reverse();
}
// Limit
if (options.hash.limit && options.hash.limit > 0) {
selectedPages = _.first(selectedPages, options.hash.limit);
}
// Rendering
_.each(selectedPages, function (page, index, pages) {
result += options.fn(page);
});
return result;
});
};
}).call(this);

It should be possible to accomplish your goal with a combination of the following:
Specify custom sorting rules for your keywords collection in the Gruntfile.
Use the {{#withFirst pages 5}} helper to restrict the now-sorted list to your first five posts.
Gruntfile
assemble: {
options: {
flatten: false,
partials: '<%= build.src %>/_partials/*.hbs',
layoutdir: '<%= build.src %>/_layouts',
data: ['<%= build.src %>/_data/*.{json,yml}', 'package.json'],
assets: '<%= build.out %>/',
helpers: [ 'helper-moment','<%= build.src %>/helpers/helper-*.js'],
collections: [
{ name: 'keywords', inflection: 'keyword', sortby: 'posted', sortorder: 'desc' }
]
},
Page Template
<div>
{{#each keywords}}
{{#is keyword "news"}}
{{#withFirst pages 5}}
<div>
<h2>{{data.title}}</h2>
<p>{{formatDate data.posted "%F"}}</p>
<div>
{{#markdown}}{{data.summary}}{{/markdown}}
</div>
<p>more...</p>
</div>
{{/withFirst}}
{{/is}}
{{/each}}
</div>

Well, I finally got this working, but it's probably not the best way to go about it. In fact, it's brute-force all the way. But I'm going to post what finally worked.
Here's the setup -- slightly modified from my original question.
I wanted to create a partial -- sidebar.hbs -- that lists the 5 most recent posts. I then wanted to call that partial inside my "normal" pages when I wanted a sidebar with recent content. Okay, so here's the setup.
My assemble routine in grunt.js:
grunt.js
assemble: {
options: {
flatten: false,
partials: '<%= build.src %>/_partials/*.hbs',
layoutdir: '<%= build.src %>/_layouts',
data: ['<%= build.src %>/_data/*.{json,yml}', 'package.json'],
assets: '<%= build.out %>/',
helpers: [ 'helper-moment','<%= build.src %>/helpers/helper-*.js'],
collections: [
{ name: 'keywords',
inflection: 'keyword',
sortby: 'posted',
sortorder: 'desc',
}
]
},
I then realized -- much like you kindly responded above -- that I can loop through the collection:
sidebar.hbs (not working -- {{relativeLink}} isn't defined -- but sorted!)
{{#each keywords}}
{{#is keyword "news"}}
{{#withFirst pages 3}}
<h5 style="padding-left: 7px; padding: 0; margin: 0;">
{{data.slug}}</h5>
{{/withFirst}}
{{/is}}
{{/each}}
Except that -- and here's the rub: {{relativeLink}} is not properly set here. It returns nothing -- it's blank. I suspect I'm confusing contexts by calling the sidebar.hbs partial from a template page. I'm not sure, but I do know that the code below works as a partial -- and returns the proper {{relativeLink}}:
sidebar.hbs (working, no sorting -- simply displays all pages in site)
{{#each pages}}
{{data.slug}} -- {{relativeLink}}
<br/>
{{/each}}
The problem there, of course, is that it returns all the pages -- and they're not sorted.
So, in order to get my sidebar partial working -- and returning the proper links no matter where we are in the site hierarchy -- I created my links like this (and I'm embarrassed to post this -- but it works). I used the 'page.src' variable in the #withFirst loop. This works for what I want but seems awkward:
sidebar.hbs (working, sorting, but doesn't seem the right way to do this):
{{#each keywords}}
{{#is keyword "news"}}
{{#withFirst pages 4}}
<div class="myArticle">
<article class="recent-posts" >
<h5 style="padding-left: 7px; padding: 0; margin: 0;">{{data.slug}}</h5>
<p>
<span >
{{moment data.posted format ="DD MMM YYYY"}}
</span>
</p>
</article>
</div>
{{/withFirst}}
{{/is}}
{{/each}}
What I'm essentially do is calling a helper -- 'constructPostLinks' and hacking together a URL from the site's site's folder on my website (again, defined in data.yml), and the 'src' page variable (which I pass to my custom handlebars template). The 'src' page variable is not anything I need -- it usually looks like /src/about/index.hbs or something like that -- so I strip off the 'src' and replace the '.hbs' with 'ext' (in this case, '.html').
Here's the handlebars helper:
helper-constructPostLinks.js
module.exports.register = function (Handlebars) {
Handlebars.registerHelper('constructPostLinks', function(page,ext) {
pageLink = page.replace('src/','').replace('.hbs',ext);
return new Handlebars.SafeString(pageLink);
});
};
I don't know. This seems like an awfully clumsy way to get the links generated, but it works. I can write a page -- index.hbs -- and then include the sidebar partial {{>sidebar}} -- and then everything is pulled together in the default page template (page.hbs) where the links are properly generated for the most recently posted articles. It works. But I wish the collection sort routine included the proper {{relativeLinks}}.
As I say, I'm sure I'm doing something wrong and confusing contexts -- but at least I've got it going.

Related

Access reactive-table row data in template/helper?

For Tabular:
“In your template and helpers, this is set to the document for the
current row”
Is there an equivalent of this for reactive-table? I need to access a rows data in my template/helper but am just struggling to find a way to access it, with tabular it was as easy as using this.
I'm using a template for a column called "Status" and in this there are different types of labels, depending on what the row data returns it will be a different type of label. The code below works for Tabular but I'm not really sure how to make this work for reactive-table?
Example.html
<template name="ApplicationStatus">
<div class="row">
{{#if statusPending}}
<label class="label label-warning label-xs">"Pending</label>
{{/if}}
{{#if statusConnected}}
<label class="label label-primary label-xs">Connected</label>
{{/if}}
</div>
</template>
Example.js
Template.ApplicationStatus.helpers({
statusPending: function() {
if (this.applications.app_status === 'Pending')
return true;
else
return false;
},
statusConnected: function() {
if (this.applications.app_status === 'Connected')
return true;
else
return false;
}
});
I am currently adding it to my reactive-table by doing this:
{ tmpl: Template.ApplicationStatus, label: 'Status' }
Any info is greatly appreciated or if there's a better way to achieve what I'm trying to achieve I would love to hear that as well!
TL; DR
Try Template.instance().data instead of this.
Little explanation
I am not shure what happenes when you miss key definition, but accoriding to docs:
You can specify a template to use to render cells in a column, by
adding tmpl to the field options.
{ fields: [
{ key: 'name', label: 'Name', tmpl: Template.nameTmpl },
{ key: 'location', label: 'Location', tmpl: Template.locationTmpl }
] }
The template's context will be the full object, so it will have access
to all fields.
So inside helpers and event handlers you can get access to full row object via Template.instance().data.

Use Meteor collections for Typeahead Bloodhound, preferably without making my own API

I want to build a tags input like the one in StackOverflow. I am trying to use Meteor collections as the remote or prefetch data for Typeahead Bloodhound because I want to eventually use Bootstrap Tokenfield.
According to their documentation and examples, a url to the JSON data is absolutely required. How can I provide the data, preferably reactively, to Bloodhound? I have looked into the Meteor Typeahead package, but I can't figure out how to use it with the Meteor Tokenfield package.
Below is what I've tried to do, but it doesn't work. :(
<div class="control-group">
<label class="control-label" for="users">Users</label>
<div class="controls">
<input type="text" class="form-control" id="tokenfield-typeahead-users" value="" />
</div>
</div>
Template.viewUsers.rendered = function() {
var users = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.username);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
limit: 20,
remote: {
// url points to a json file that contains an array of tokens
url: function() {
return Meteor.users.find().fetch().map(function(user){ return user.username; });
}
}
});
users.initialize();// kicks off the loading/processing of `local` and `prefetch`
// passing in `null` for the `options` arguments will result in the default
// options being used
$('#tokenfield-typeahead-users').tokenfield({
typeahead: [null, {
name: 'users',
displayKey: 'username',
source: users.ttAdapter()
// `ttAdapter` wraps the suggestion engine in an adapter that
// is compatible with the typeahead jQuery plugin
}]
});
};
I prefer not to build an API, but if I have to, how do I provide the data?
This code posting uses:
local: [{ val: 'dog' }, { val: 'pig' }, { val: 'moose' }],
Spend quite some time trying to get the tokenfield to work reactively with my Meteor collection, so I'll just post my solution here as well.
I ended up not using Bloodhound at all, but instead just using Meteor directly.
I realize that the RegEx search is pretty primitive, but if what you're searching is a collection of tags, it does the job.
var currentTags = []; // Handle this however you wish
$('#tokenfield').tokenfield({
typeahead: [null, {
name: 'tags',
displayKey: 'value',
source: function(query, syncResults, asyncResults) {
var suggestedTags = Tags.find({value: {
$regex: "^"+query,
$options: "i",
$nin: currentTags
}}).fetch();
syncResults(suggestedTags);
//Optionally do some server side lookup using asyncResults
}
}]
});

How do I render all pages in a page collection on one page?

I'm trying to figure out how to render all the pages in a page collection on a single page. I want all my posts after each other in the generated index.html, like on a blog's front page.
The file structure is
src/
index.hbs
posts/
post-1.hbs
post-2.hbs
post-3.hbs
The below does almost what I'm looking for.
<section>
{{#each pages}}
<article>
<header>
<h2>{{data.title}}</h2>
</header>
{{page}}
</article>
{{/each}}
</section>
What am I missing?
Excuse the quick and dirty answer, I wanted to get it up here as quickly as possible. I'll clean it up in a day or two. (2013-09-26)
After working on this I ended up making a Handlebars helper that I could use in the index.hbs template. With it, I end up with the below Handlebars template for index.hbs.
index.hbs
---
title: Home
---
<p>I'm a dude. I live in Stockholm, and most of the time I work with Goo.</p>
<section>
{{#md-posts 'src/posts/*.*'}}
<article>
<h2>{{title}}</h2>
{{#markdown}}
{{{body}}}
{{/markdown}}
</article>
{{/md-posts}}
</section>
Folder structure
src/
index.hbs
posts/
post-1.hbs
post-2.hbs
post-3.md // <--- Markdown with Handlebars
Gruntfile.coffee
assemble:
options:
flatten: true
layout: 'layouts/default.hbs'
assets: 'public/assets'
helpers: 'src/helpers/helper-*.js'
root: // this is my target
src: 'src/*.hbs' // <--- Only assemble files at source root level
dest: 'public/'
src/helpers/helper-md-posts.js
This helper takes a glob expression, reads the files, extracts the YAML front matter, compiles the body source, and finally adds it all to the Handlebars block context. The helper is somewhat misnames, as it doesn't actually compile Markdown... so naming suggestions are welcome.
var glob = require('glob');
var fs = require('fs');
var yamlFront = require('yaml-front-matter');
module.exports.register = function(Handlebars, options) {
// Customize this helper
Handlebars.registerHelper('md-posts', function(str, options) {
var files = glob.sync(str);
var out = '';
var context = {};
var data = null;
var template = null;
var _i;
for(_i = 0; _i < files.length; _i++) {
data = yamlFront.loadFront(fs.readFileSync(files[_i]), 'src');
template = Handlebars.compile(data.src); // Compile the source
context = data; // Copy front matter data to Handlebars context
context.body = template(data); // render template
out += options.fn(context);
}
return out;
});
};
See it all in this repo: https://github.com/marcusstenbeck/marcusstenbeck.github.io/tree/source

Meteor.js: Understanding sort parameters within a find{} method

I am doing exercises in the book "Getting Started With Meteor.js Javascript Framework" and
there is a section where a template called categories is created, then a {#each} loop thumbs through a collection.This is done with the intent of printing the results to the screen. This works and the code is below.
<template name ="categories">
<div class ="title">my stuff</div>
<div id='categories'>
{{#each lists}}
<div id="category">
{{Category}}
</div>
{{/each}}
</div>
</template>
My question is this:
In the code below the sort parameter doesn't seem to do anything. I am curious what it is supposed to be doing. I understand that theoretically it's suppose to "sort", but when I play with the parameters nothing changes. I looked it up in the meteor documentation and I couldn't figure it out. I am trying to modify the code so that it so that it sorts in a different order. This way I can see results and get a real understanding of how {sort: works. I tried modifying the {sort: {Category: 1}}) to {sort: {Category: -1}}) as well as {sort: {Category: 2}}) and it's always the same result.
Template.categories.lists = function() {
return lists.find({}, {sort: {Category: 1}}); // Works
/* return lists.find({}); */ // Works just the same
}
The Collections are as follows:
lists.insert({Category:"DVDs", items: [{Name:"Mission Impossible" ,Owner:"me",LentTo:"Alice"}]});
lists.insert({Category:"Tools", items: [{Name:"Linear Compression Wrench",Owner:"me",LentTo: "STEVE"}]});
In your code, you can attempt to change the argument of the sort to be an array instead of an object, like this:
sort: [["Category","asc"],["another argument","desc"],[...]]
so the code :
Template.categories.lists = function() {
return lists.find({}, {
sort: [
["Category", "asc"],
["another argument", "desc"],
[...]
]
});
// return lists.find({}); // Works just the same
}
As per my understanding that is because default is to sort by natural order. More info can be found at this link

Reactive Template with compex object

I want to render a reactive template based on this document:
Sprint:
WorkStories:
Tasks
I know this can be done by making a Meteor collection for each "level," but that means the result is actually being stored as a seperate document in the database. I want to know if its possible to have one collection/document for Sprint, that has standard collection of WorkStories with a standard collection of Tasks each, rendered to a reactive template.
I've seen [Meteor.deps.Context][1], but I can't figure out how to wire it up (or even if it's the right tool), and none of the examples do anything like this.
I've also seen [this question][2], but he seems to be asking about connecting related-but-separate documents, not rendering a single document.
Because database queries on collections are already reactive variables on the client, the below will render one Sprint document with nested WorkStories that has nested Tasks in a template:
HTML:
<head>
<title>Sprints Example</title>
</head>
<body>
{{> sprints }}
</body>
<template name="sprints">
{{#each items}}
<div>Name: {{name}}</div>
{{#each this.WorkStories}}
<div>{{name}}</div>
{{#each this.Tasks}}
<div>{{name}}</div>
{{/each}}
{{/each}}
{{/each}}
</template>
Javascript:
Sprints = new Meteor.Collection("sprints");
if (Meteor.isClient) {
Template.sprints.items = function () {
return Sprints.find({});
};
}
if (Meteor.isServer) {
Meteor.startup(function () {
if (Sprints.find().count() === 0) {
Sprints.insert({ name: 'sprint1', WorkStories: [{ name: 'workStory1', Tasks: [{ name: 'task1' }, { name: 'task2' }, { name: 'task3' }] }, { name: 'workStory2', Tasks: [{ name: 'task1' }, { name: 'task2' }, { name: 'task3' }] }] });
}
});
}
UPDATE WITH ANSWER
Per #Paul-Young's comment below, the problem with my usage of $set was the lack of quotes in the update. Once the nested object renders in the Template, as of Meteor 0.5.3 you can update sub arrays simply:
Sprints.update(Sprints.findOne()._id, { $set: { "WorkStories.0.name": "updated_name1" } });
BACKGROUND INFO
This does load the initial object, but updating seems problematic. I was able to get the Template to rerender by calling the below in the console:
Sprints.update(Sprints.findOne()._id, { name: 'sprint777', WorkStories: [{ name: 'workStory1232', Tasks: [{ name: 'task221' }, { name: 'task2' }, { name: 'task3' }] }, { name: 'workStory2', Tasks: [{ name: 'task1' }, { name: 'task2' }, { name: 'task3' }] }] })
Which follows these rules, per the Meteor Docs:
But if a modifier doesn't contain any $-operators, then it is instead interpreted as a literal document, and completely replaces whatever was previously in the database. (Literal document modifiers are not currently supported by validated updates.
Of course, what you want is to use the $set style operators on nested documents and get the templates to re-render when their nested properties change without having to replace the entire document in the table. The 0.5.3 version of Meteor included the ability to search for sub-arrays:
Allow querying specific array elements (foo.1.bar).
I've tried to do the . sub array search and haven't been able to update the WorkStories subdocument of the original entity yet, so I posted a question in the google talk.
Hope this helps

Resources