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

feat(breadcrumb): Add the breadcrumb widget #2451

Merged
merged 45 commits into from
Oct 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
02211b8
feat(breadcrumb): kick-off connectBreadcrumb
iam4x Aug 10, 2017
32dc64c
chore(dev): add breadcrumb into storybook
iam4x Aug 10, 2017
06f4e72
feat(connectBreadcrumb): remove view logic
iam4x Aug 10, 2017
cd38ab2
feat(widgets): export breadcrumb widget
iam4x Aug 10, 2017
c9bd97e
feat(widget): breadcrumb
iam4x Aug 10, 2017
5f85ed2
feat(component): kick-off breadcrumb component
iam4x Aug 10, 2017
265fcce
feat(Breadcrumb): WIP
marielaures Aug 11, 2017
3eeaed9
feat(Breadcrumb): modified separator
marielaures Aug 11, 2017
0b00615
feat(Breadcrumb): display breadcrumb (without any links)
marielaures Aug 16, 2017
cb437d8
feat(Breadcrumb): WIP
marielaures Aug 17, 2017
5d684ec
feat(Breadcrumb): add css classes
marielaures Aug 21, 2017
05e31fc
feat(Breadcrumb): working on styling
marielaures Aug 21, 2017
e0a1399
feat(Breadcrumb): modified onClick prop
marielaures Aug 22, 2017
be5473d
feat(Breadcrumb): Add headerFooter + autoHideContainer
marielaures Aug 22, 2017
f13b950
feat(Breadcrumb): documentation
marielaures Aug 22, 2017
51d9196
feat(Breadcrumb): remove cssClass root
marielaures Aug 23, 2017
2f95f52
enable use as a standalone widget
marielaures Aug 31, 2017
dc144f6
add template to widget
marielaures Sep 4, 2017
b08495a
add custom label story
marielaures Sep 4, 2017
37d3c9a
first series of tests
marielaures Sep 4, 2017
813a2f8
add logic for the onclick on "Home"
marielaures Sep 12, 2017
71ae51b
add templates for home and separator
marielaures Sep 12, 2017
4e8abce
warn the user if there's already a Breadcrumb used with the same facet
marielaures Sep 12, 2017
2904ff9
add cssClasses
marielaures Sep 12, 2017
9e104a8
add lifecycle test
marielaures Sep 12, 2017
954d0a3
modify template so it re renders when rootProps are modified
marielaures Sep 13, 2017
3ed7f38
modify css classes applied to the home label
marielaures Sep 13, 2017
dd9bda9
change the separator in the theme
marielaures Sep 13, 2017
b673e71
more tests for connectBreadcrumb
marielaures Sep 13, 2017
9857ff3
add documentation
marielaures Sep 18, 2017
0e2f667
add createUrl + some refactoring
marielaures Sep 18, 2017
5913478
add default selected item story
marielaures Sep 28, 2017
89fe4e5
WIP
marielaures Oct 2, 2017
8448ddc
WIP - fixing response data for hierarchical values
Oct 2, 2017
a964702
add createUrl + rootPath
marielaures Sep 18, 2017
d0c164f
add default selected item story
marielaures Sep 28, 2017
6e50bc0
WIP
marielaures Oct 2, 2017
e336c15
chore(test): test on second value
Oct 3, 2017
61e0740
Merge branch 'feat/2.2-breadcrumb' of github.com:algolia/instantsearc…
marielaures Oct 4, 2017
f4866fe
feat(Breadcrumb): update API of the connector
marielaures Oct 4, 2017
4f8abfc
fix(dep): use same casing for isEqual
Oct 4, 2017
c41c486
chore(doc): fix misc. errors
Oct 4, 2017
ec4effe
chore(breadcrumb): add more docs and validation
marielaures Oct 4, 2017
9b63631
Merge branch 'feat/2.3' into feat/2.2-breadcrumb
marielaures Oct 5, 2017
88ded98
various code improvements
marielaures Oct 8, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 147 additions & 1 deletion dev/app/init-builtin-widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,137 @@ import instantsearch from '../../index.js';
import wrapWithHits from './wrap-with-hits.js';

export default () => {
storiesOf('Breadcrumb')
.add(
'default',
wrapWithHits(container => {
container.innerHTML = `
<div id="hierarchicalMenu"></div>
<div id="breadcrumb"></div>
`;

window.search.addWidget(
instantsearch.widgets.breadcrumb({
container: '#breadcrumb',
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
})
);

//Custom Widget to toggle refinement
window.search.addWidget({
init({ helper }) {
helper.toggleRefinement(
'hierarchicalCategories.lvl0',
'Cameras & Camcorders > Digital Cameras'
);
},
});
})
)
.add(
'with custom home label',
wrapWithHits(container => {
container.innerHTML = `
<div id="hierarchicalMenu"></div>
<div id="breadcrumb"></div>
`;

window.search.addWidget(
instantsearch.widgets.breadcrumb({
container: '#breadcrumb',
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
templates: { home: 'Home Page' },
})
);

//Custom Widget to toggle refinement
window.search.addWidget({
init({ helper }) {
helper.toggleRefinement(
'hierarchicalCategories.lvl0',
'Cameras & Camcorders > Digital Cameras'
);
},
});
})
)
.add(
'with default selected item',
wrapWithHits(container => {
container.innerHTML = `
<div id="breadcrumb"></div>
<div id="hierarchicalMenu"></div>
`;

window.search.addWidget(
instantsearch.widgets.breadcrumb({
container: '#breadcrumb',
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
rootPath: 'Cameras & Camcorders > Digital Cameras',
})
);

window.search.addWidget(
instantsearch.widgets.hierarchicalMenu({
showParentLevel: false,
container: '#hierarchicalMenu',
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
rootPath: 'Cameras & Camcorders',
})
);
})
)
.add(
'with hierarchical menu',
wrapWithHits(container => {
container.innerHTML = `
<div id="breadcrumb"></div>
<div id="hierarchicalMenu"></div>
`;

window.search.addWidget(
instantsearch.widgets.breadcrumb({
container: '#breadcrumb',
separator: ' / ',
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
})
);

window.search.addWidget(
instantsearch.widgets.hierarchicalMenu({
showParentLevel: false,
container: '#hierarchicalMenu',
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
rootPath: 'Cameras & Camcorders',
})
);
})
);

storiesOf('Analytics').add(
'default',
wrapWithHits(container => {
Expand Down Expand Up @@ -699,7 +830,22 @@ export default () => {
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
rootPath: 'Cameras & Camcorders',
})
);
})
)
.add(
'only show current level',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.hierarchicalMenu({
container,
showParentLevel: false,
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
})
);
})
Expand Down
82 changes: 82 additions & 0 deletions src/components/Breadcrumb/Breadcrumb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Template from '../Template.js';
import autoHideContainerHOC from '../../decorators/autoHideContainer.js';

const itemsPropType = PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
})
);

class Breadcrumb extends PureComponent {
static propTypes = {
createURL: PropTypes.func,
cssClasses: PropTypes.objectOf(PropTypes.string),
items: itemsPropType,
refine: PropTypes.func.isRequired,
separator: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
templateProps: PropTypes.object.isRequired,
translate: PropTypes.func,
};

render() {
const { createURL, items, refine, cssClasses } = this.props;

const breadcrumb = items.map((item, idx) => {
const isLast = idx === items.length - 1;
const label = isLast
? <a className={`${cssClasses.disabledLabel} ${cssClasses.label}`}>
{item.name}
</a>
: <a
className={cssClasses.label}
href={createURL(item.value)}
onClick={e => {
e.preventDefault();
refine(item.value);
}}
>
{item.name}
</a>;

return [
<Template
key={item.name + idx}
rootProps={{ className: cssClasses.separator }}
templateKey="separator"
{...this.props.templateProps}
/>,
label,
];
});

const homeClassNames =
items.length > 0
? [cssClasses.home, cssClasses.label]
: [cssClasses.disabledLabel, cssClasses.home, cssClasses.label];

const homeOnClickHandler = e => {
e.preventDefault();
refine(null);
};

const homeUrl = createURL(null);

return (
<div className={cssClasses.root}>
<a
className={homeClassNames.join(' ')}
href={homeUrl}
onClick={homeOnClickHandler}
>
<Template templateKey="home" {...this.props.templateProps} />
</a>
{breadcrumb}
</div>
);
}
}

export default autoHideContainerHOC(Breadcrumb);
19 changes: 10 additions & 9 deletions src/components/Template.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export class PureTemplate extends React.Component {
shouldComponentUpdate(nextProps) {
return (
!isEqual(this.props.data, nextProps.data) ||
this.props.templateKey !== nextProps.templateKey
this.props.templateKey !== nextProps.templateKey ||
!isEqual(this.props.rootProps, nextProps.rootProps)
);
}

Expand Down Expand Up @@ -41,7 +42,7 @@ export class PureTemplate extends React.Component {

if (isReactElement(content)) {
throw new Error(
'Support for templates as React elements has been removed, please use react-instantsearch'
'Support for templates as React elements has been removed, please use react-instantsearch',
);
}

Expand All @@ -59,7 +60,7 @@ PureTemplate.propTypes = {
rootProps: PropTypes.object,
templateKey: PropTypes.string,
templates: PropTypes.objectOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.func])
PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
),
templatesConfig: PropTypes.shape({
helpers: PropTypes.objectOf(PropTypes.func),
Expand All @@ -70,7 +71,7 @@ PureTemplate.propTypes = {
PropTypes.shape({
o: PropTypes.string,
c: PropTypes.string,
})
}),
),
delimiters: PropTypes.string,
disableLambda: PropTypes.bool,
Expand Down Expand Up @@ -112,15 +113,15 @@ function transformData(fn, templateKey, originalData) {
}
} else {
throw new Error(
`transformData must be a function or an object, was ${typeFn} (key : ${templateKey})`
`transformData must be a function or an object, was ${typeFn} (key : ${templateKey})`,
);
}

const dataType = typeof data;
const expectedType = typeof originalData;
if (dataType !== expectedType) {
throw new Error(
`\`transformData\` must return a \`${expectedType}\`, got \`${dataType}\`.`
`\`transformData\` must return a \`${expectedType}\`, got \`${dataType}\`.`,
);
}
return data;
Expand All @@ -140,15 +141,15 @@ function renderTemplate({

if (!isTemplateString && !isTemplateFunction) {
throw new Error(
`Template must be 'string' or 'function', was '${templateType}' (key: ${templateKey})`
`Template must be 'string' or 'function', was '${templateType}' (key: ${templateKey})`,
);
} else if (isTemplateFunction) {
return template(data);
} else {
const transformedHelpers = transformHelpersToHogan(
helpers,
compileOptions,
data
data,
);
const preparedData = { ...data, helpers: transformedHelpers };
return hogan.compile(template, compileOptions).render(preparedData);
Expand All @@ -165,7 +166,7 @@ function transformHelpersToHogan(helpers, compileOptions, data) {
curry(function(text) {
const render = value => hogan.compile(value, compileOptions).render(this);
return method.call(data, text, render);
})
}),
);
}

Expand Down
28 changes: 19 additions & 9 deletions src/components/__tests__/Template-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,15 +208,10 @@ describe('Template', () => {
it('forward rootProps to the first node', () => {
function fn() {}

const props = getProps({});
const tree = renderer
.create(
<PureTemplate
rootProps={{ className: 'hey', onClick: fn }}
{...props}
/>
)
.toJSON();
const props = getProps({
rootProps: { className: 'hey', onClick: fn },
});
const tree = renderer.create(<PureTemplate {...props} />).toJSON();
expect(tree).toMatchSnapshot();
});

Expand All @@ -229,6 +224,7 @@ describe('Template', () => {
container = document.createElement('div');
props = getProps({
data: { hello: 'mom' },
rootProps: { className: 'myCssClass' },
});
component = ReactDOM.render(<PureTemplate {...props} />, container);
sinon.spy(component, 'render');
Expand All @@ -251,12 +247,25 @@ describe('Template', () => {
ReactDOM.render(<PureTemplate {...props} />, container);
expect(component.render.called).toBe(true);
});

it('calls render when rootProps changes', () => {
props.rootProps = { className: 'myCssClass mySecondCssClass' };
ReactDOM.render(<PureTemplate {...props} />, container);
expect(component.render.called).toBe(true);
});

it('does not call render when rootProps remain unchanged', () => {
props.rootProps = { className: 'myCssClass' };
ReactDOM.render(<PureTemplate {...props} />, container);
expect(component.render.called).toBe(false);
});
});

function getProps({
templates = { test: '' },
data = {},
templateKey = 'test',
rootProps = {},
useCustomCompileOptions = {},
templatesConfig = { helper: {}, compileOptions: {} },
transformData = null,
Expand All @@ -265,6 +274,7 @@ describe('Template', () => {
templates,
data,
templateKey,
rootProps,
useCustomCompileOptions,
templatesConfig,
transformData,
Expand Down
Loading