Skip to content

Commit

Permalink
feat(templatesConfig): helpers and options transferred to Template
Browse files Browse the repository at this point in the history
New parameter passed to the `init` and `render` method: `templatesConfig`.
It replaces `templateHelpers`.

This parameter embeds both the `helpers` and the `Hogan` options.
It uses the same chain that the `templateHelpers` previously used to
be passed from a widget to the `Template` component.

The `Template` component now also accepts a `templateDefault` parameter
which allows us to ignore `Hogan`'s `options` if there is no
`template` provided

Closes #99.
  • Loading branch information
Jerska authored and vvo committed Sep 28, 2015
1 parent 9298ca1 commit 456d781
Show file tree
Hide file tree
Showing 18 changed files with 157 additions and 85 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,13 @@ search.addWidget(
);
```

### Template helpers
### Template configuration

In order to help you when defining your templates, `instantsearch.js` exposes
a few helpers. All helpers are accessible in the Mustache templating through
`{{#helpers.nameOfTheHelper}}{{valueToFormat}}{{/helpers.nameOfTheHelper}}`. To
use them in the function templates, you'll have to call
`search.templateHelpers.nameOfTheHelper` where `search` is your current
`search.templatesConfig.helpers.nameOfTheHelper` where `search` is your current
`instantsearch` instance.

Here is the list of the currently available helpers:
Expand All @@ -159,6 +159,16 @@ Here is the list of the currently available helpers:
option (defaults to `en-EN`).
eg. `100000` will be formatted as `100 000` with `en-EN`

`instantsearch.js` also provides the ability to set options to use with your
templating engine. These options are accessible through
`search.templatesConfig.options`.
With the embedded widgets using our `Template` component (widgets with
`template(s)` parameter), we're passing these options to `Hogan.compile`
if you're not using the default `template`.
```js
search.templatesConfig.options.delimiters = '[[ ]]';
```

## Development workflow

```sh
Expand Down
5 changes: 5 additions & 0 deletions components/Hits.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var Template = require('./Template');
class Hits extends React.Component {
renderWithResults() {
var template = this.props.hitTemplate;
var templatesConfig = this.props.templatesConfig;
var transformData = this.props.hitTransformData;

var renderedHits = map(this.props.hits, function(hit) {
Expand All @@ -15,6 +16,7 @@ class Hits extends React.Component {
transformData={transformData}
key={hit.objectID}
template={template}
config={templatesConfig}
/>
);
}, this);
Expand All @@ -25,6 +27,7 @@ class Hits extends React.Component {
renderNoResults() {
var data = this.props.results;
var template = this.props.noResultsTemplate;
var templatesConfig = this.props.templatesConfig;
var transformData = this.props.noResultsTransformData;

return (
Expand All @@ -33,6 +36,7 @@ class Hits extends React.Component {
data={data}
transformData={transformData}
template={template}
config={templatesConfig}
/>
</div>
);
Expand All @@ -57,6 +61,7 @@ Hits.propTypes = {
React.PropTypes.string,
React.PropTypes.func
]).isRequired,
templatesConfig: React.PropTypes.object.isRequired,
noResultsTransformData: React.PropTypes.func
};

Expand Down
11 changes: 10 additions & 1 deletion components/RefinementList.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class RefinementList extends React.Component {
data={facetValue}
transformData={this.props.transformData}
template={this.props.templates.item}
defaultTemplate={this.props.defaultTemplates.item}
config={this.props.templatesConfig}
/>
</div>
);
Expand All @@ -80,8 +82,15 @@ RefinementList.propTypes = {
item: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
]).isRequired
])
}),
defaultTemplates: React.PropTypes.shape({
item: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
])
}),
templatesConfig: React.PropTypes.object.isRequired,
transformData: React.PropTypes.func,
toggleRefinement: React.PropTypes.func.isRequired
};
Expand Down
12 changes: 9 additions & 3 deletions components/Stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ var Template = require('./Template');
class Stats extends React.Component {
render() {
var template = this.props.template;
var templateHelpers = this.props.templateHelpers;
var defaultTemplate = this.props.defaultTemplate;
var templatesConfig = this.props.templatesConfig;
var transformData = this.props.transformData;
var data = {
hasManyResults: this.props.nbHits > 1,
Expand All @@ -24,7 +25,8 @@ class Stats extends React.Component {
data={data}
transformData={transformData}
template={template}
templateHelpers={templateHelpers}
defaultTemplate={defaultTemplate}
config={templatesConfig}
/>
);
}
Expand All @@ -39,9 +41,13 @@ Stats.propTypes = {
template: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.string
]),
defaultTemplate: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.string
]).isRequired,
transformData: React.PropTypes.func,
templateHelpers: React.PropTypes.object,
templatesConfig: React.PropTypes.object.isRequired,
query: React.PropTypes.string
};

Expand Down
17 changes: 14 additions & 3 deletions components/Template.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ class Template {
render() {
var content = renderTemplate({
template: this.props.template,
templateHelpers: this.props.templateHelpers,
defaultTemplate: this.props.defaultTemplate,
config: this.props.config,
data: this.props.transformData ? this.props.transformData(this.props.data) : this.props.data
});

if (content === null) {
// Adds a noscript to the DOM but virtual DOM is null
// See http://facebook.github.io/react/docs/component-specs.html#render
return null;
}

return <div dangerouslySetInnerHTML={{__html: content}} />;
}
}
Expand All @@ -18,8 +25,12 @@ Template.propTypes = {
template: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
]).isRequired,
templateHelpers: React.PropTypes.object,
]),
defaultTemplate: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
]),
config: React.PropTypes.object.isRequired,
transformData: React.PropTypes.func,
data: React.PropTypes.object
};
Expand Down
17 changes: 14 additions & 3 deletions decorators/headerFooter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ function headerFooter(ComposedComponent) {
render() {
return (
<div className={cx(this.props.cssClasses.root)}>
<Template template={this.props.templates.header} />
<Template template={this.props.templates.header} defaultTemplate={this.props.defaultTemplates.header} config={this.props.templatesConfig} />
<ComposedComponent {...this.props} />
<Template template={this.props.templates.footer} />
<Template template={this.props.templates.footer} defaultTemplate={this.props.defaultTemplates.footer} config={this.props.templatesConfig} />
</div>
);
}
Expand All @@ -28,6 +28,17 @@ function headerFooter(ComposedComponent) {
React.PropTypes.func
])
}),
defaultTemplates: React.PropTypes.shape({
header: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
]),
footer: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
])
}),
templatesConfig: React.PropTypes.object.isRequired,
cssClasses: React.PropTypes.shape({
root: React.PropTypes.oneOfType([
React.PropTypes.string,
Expand All @@ -37,7 +48,7 @@ function headerFooter(ComposedComponent) {
};

HeaderFooter.defaultProps = {
templates: {
defaultTemplates: {
header: '',
footer: ''
},
Expand Down
5 changes: 5 additions & 0 deletions example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var search = instantsearch({
apiKey: '6be0576ff61c053d5f9a3225e2a90f76',
indexName: 'instant_search'
});
search.templatesConfig.options.sectionTags = [{o: '_refined', c: 'refined'}];

search.addWidget(
instantsearch.widgets.urlSync({
Expand Down Expand Up @@ -102,6 +103,10 @@ search.addWidget(
templates: {
header: '<div class="panel-heading">Shipping</div>',
body: require('./templates/free-shipping.html')
},
transformData: function(data) {
data._refined = data.isRefined;
return data;
}
})
);
Expand Down
1 change: 1 addition & 0 deletions example/templates/free-shipping.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<label>
<input type="checkbox" {{#isRefined}}checked{{/isRefined}} />
{{label}}
{{_refined}}!{{/refined}}
</label>
<span class="badge pull-right">{{count}}</span>
</div>
17 changes: 10 additions & 7 deletions lib/InstantSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ Usage: instantsearch({
this.indexName = indexName;
this.searchParameters = searchParameters || {};
this.widgets = [];
this.templateHelpers = {
formatNumber(number) {
return Number(number).toLocaleString(numberLocale);
}
this.templatesConfig = {
helpers: {
formatNumber(number) {
return Number(number).toLocaleString(numberLocale);
}
},
options: {}
};
}

Expand Down Expand Up @@ -83,7 +86,7 @@ Usage: instantsearch({
return;
}
widget.render({
templateHelpers: this.templateHelpers,
templatesConfig: this.templatesConfig,
results,
state,
helper
Expand All @@ -94,9 +97,9 @@ Usage: instantsearch({
_init(state, helper) {
forEach(this.widgets, function(widget) {
if (widget.init) {
widget.init(state, helper);
widget.init(state, helper, this.templatesConfig);
}
});
}, this);
}
}

Expand Down
23 changes: 19 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,37 @@ function isDomElement(o) {
return o instanceof HTMLElement || o && o.nodeType > 0;
}

function renderTemplate({template, templateHelpers, data}) {
function renderTemplate({template, defaultTemplate, config, data}) {
var hogan = require('hogan.js');
var forEach = require('lodash/collection/forEach');
var options = config.options;
var content;

if (template === null || typeof template === 'undefined') {
if (defaultTemplate === null || typeof defaultTemplate === 'undefined') {
throw new Error('No template provided and no defaultTemplate');
}
// If template isn't set, we reset the options since our defaultTemplates
// don't expect options to be set
options = {};
template = defaultTemplate;
}

if (template === '') {
return null;
}

// We add all our template helper methods to the template as lambdas. Note
// that lambdas in Mustache are supposed to accept a second argument of
// `render` to get the rendered value, not the literal `{{value}}`. But
// this is currently broken (see
// https://github.com/twitter/hogan.js/issues/222).
function addTemplateHelpersToData(templateData) {
templateData.helpers = {};
forEach(templateHelpers, (method, name) => {
forEach(config.helpers, (method, name) => {
data.helpers[name] = () => {
return (value) => {
return method(hogan.compile(value).render(templateData));
return method(hogan.compile(value, options).render(templateData));
};
};
});
Expand All @@ -55,7 +70,7 @@ function renderTemplate({template, templateHelpers, data}) {
if (typeof template === 'string') {
data = addTemplateHelpersToData(data);

content = hogan.compile(template).render(data);
content = hogan.compile(template, options).render(data);
}

return content;
Expand Down
2 changes: 1 addition & 1 deletion scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ set -e # exit when error

printf "\nLint\n"

eslint . --quiet
eslint . --quiet --no-color
6 changes: 3 additions & 3 deletions test/InstantSearch/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ test('InstantSearch: lifecycle', function(t) {
);
t.deepEqual(
widget.init.args[0],
[helper.state, helper],
'widget.init(helper.state, helper)'
[helper.state, helper, search.templatesConfig],
'widget.init(helper.state, helper, templatesConfig)'
);
t.ok(widget.render.notCalled, 'widget.render not yet called');

Expand All @@ -98,7 +98,7 @@ test('InstantSearch: lifecycle', function(t) {
results: results,
state: helper.state,
helper: helper,
templateHelpers: search.templateHelpers
templatesConfig: search.templatesConfig
}],
'widget.render(results, state, helper)'
);
Expand Down
3 changes: 2 additions & 1 deletion widgets/hits.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function hits({

return {
getConfiguration: () => ({hitsPerPage}),
render: function({results, helper}) {
render: function({results, helper, templatesConfig}) {
React.render(
<Hits
hits={results.hits}
Expand All @@ -26,6 +26,7 @@ function hits({
hideWhenNoResults={hideWhenNoResults}
hasResults={results.hits.length > 0}
hitTemplate={templates.hit}
templatesConfig={templatesConfig}
hitTransformData={transformData.hit}
/>,
containerNode
Expand Down
Loading

0 comments on commit 456d781

Please sign in to comment.