Skip to content

Commit 142d4b3

Browse files
authored
feat: add preserveQuery option to Wizard (#126)
1 parent ba94b90 commit 142d4b3

File tree

3 files changed

+141
-2
lines changed

3 files changed

+141
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ A higher order component that adds [`context.wizard`](#contextwizard) as a `wiza
129129
* `step` (object): Describes the current step with structure: `{ id: string }`.
130130
* `steps` (array): Array of `step` objects in the order they were declared within `<Steps>`.
131131
* `history` (object): The backing [`history`](https://github.com/ReactTraining/history#properties) object.
132+
* `preserveQuery` (boolean): Whether or not to preserve the query string when navigating between steps.
132133
* `next()` (function): Moves to the next step in order.
133134
* `previous()` (function): Moves to the previous step in order.
134135
* `go(n)` (function): Moves `n` steps in history.

__tests__/components/Wizard.spec.jsx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,130 @@ describe('Wizard', () => {
203203
mounted.unmount();
204204
});
205205
});
206+
207+
describe('with existing history and preserving search params', () => {
208+
let wizard;
209+
let mounted;
210+
211+
const mockReplace = jest.fn();
212+
const mockPush = jest.fn();
213+
214+
beforeEach(() => {
215+
jest.clearAllMocks();
216+
});
217+
218+
describe('initially at /gryffindor with ?foo=bar', () => {
219+
const history = {
220+
replace: mockReplace,
221+
listen: () => () => null,
222+
location: {
223+
pathname: '/gryffindor',
224+
search: '?foo=bar',
225+
},
226+
};
227+
228+
beforeEach(() => {
229+
mounted = mount(
230+
<Wizard history={history} preserveQuery={true}>
231+
<WithWizard>
232+
{prop => {
233+
wizard = prop;
234+
return null;
235+
}}
236+
</WithWizard>
237+
<Steps>
238+
<Step id="gryffindor">
239+
<div />
240+
</Step>
241+
<Step id="slytherin">
242+
<div />
243+
</Step>
244+
<Step id="hufflepuff">
245+
<div />
246+
</Step>
247+
</Steps>
248+
</Wizard>
249+
);
250+
});
251+
252+
it('should preserve query when calling next', () => {
253+
wizard.history.push = mockPush;
254+
wizard.next();
255+
expect(mockPush).toBeCalledWith({ pathname: '/slytherin', search: '?foo=bar' });
256+
});
257+
258+
it('should preserve query when calling replace', () => {
259+
wizard.replace('hufflepuff');
260+
expect(mockReplace).toBeCalledWith({ pathname: '/hufflepuff', search: '?foo=bar' });
261+
});
262+
263+
it('should produce the correct URL string when preserving search params', () => {
264+
wizard.replace('hufflepuff');
265+
const callArgs = mockReplace.mock.calls[0][0];
266+
const actualURL = `${callArgs.pathname}${callArgs.search}`;
267+
expect(actualURL).toBe('/hufflepuff?foo=bar');
268+
});
269+
270+
it('should not add search params if none existed initially when calling push', () => {
271+
history.location.search = '';
272+
wizard.push('hufflepuff');
273+
expect(mockPush).toBeCalledWith({ pathname: '/hufflepuff', search: '' });
274+
});
275+
});
276+
277+
describe('initially at /slytherin with ?quidditch=true', () => {
278+
const history = {
279+
replace: mockReplace,
280+
listen: () => () => null,
281+
location: {
282+
pathname: '/slytherin',
283+
search: '?quidditch=true',
284+
},
285+
};
286+
287+
beforeEach(() => {
288+
mounted = mount(
289+
<Wizard history={history} preserveQuery={true}>
290+
<WithWizard>
291+
{prop => {
292+
wizard = prop;
293+
return null;
294+
}}
295+
</WithWizard>
296+
<Steps>
297+
<Step id="gryffindor">
298+
<div />
299+
</Step>
300+
<Step id="slytherin">
301+
<div />
302+
</Step>
303+
<Step id="hufflepuff">
304+
<div />
305+
</Step>
306+
</Steps>
307+
</Wizard>
308+
);
309+
});
310+
311+
it('should preserve query when calling next', () => {
312+
wizard.history.push = jest.fn();
313+
wizard.next();
314+
expect(wizard.history.push).toBeCalledWith({
315+
pathname: '/hufflepuff',
316+
search: '?quidditch=true',
317+
});
318+
});
319+
320+
it('should produce the correct URL string when preserving search params', () => {
321+
wizard.replace('gryffindor');
322+
const callArgs = mockReplace.mock.calls[0][0];
323+
const actualURL = `${callArgs.pathname}${callArgs.search}`;
324+
expect(actualURL).toBe('/gryffindor?quidditch=true');
325+
});
326+
});
327+
328+
afterEach(() => {
329+
mounted.unmount();
330+
});
331+
});
206332
});

src/components/Wizard.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,19 @@ class Wizard extends Component {
9797
});
9898
};
9999

100-
set = step => this.history.push(`${this.basename}${step}`);
100+
constructPath = step => {
101+
if (this.props.preserveQuery) {
102+
return {
103+
...this.history.location,
104+
pathname: `${this.basename}${step}`,
105+
};
106+
}
107+
return `${this.basename}${step}`;
108+
};
109+
101110
push = (step = this.nextStep) => this.set(step);
102-
replace = (step = this.nextStep) => this.history.replace(`${this.basename}${step}`);
111+
set = step => this.history.push(this.constructPath(step));
112+
replace = (step = this.nextStep) => this.history.replace(this.constructPath(step));
103113
pushPrevious = (step = this.previousStep) => this.set(step);
104114

105115
next = () => {
@@ -122,6 +132,7 @@ class Wizard extends Component {
122132

123133
Wizard.propTypes = {
124134
basename: PropTypes.string,
135+
preserveQuery: PropTypes.bool,
125136
history: PropTypes.shape({
126137
// disabling due to lost context
127138
// eslint-disable-next-line react/forbid-prop-types
@@ -141,6 +152,7 @@ Wizard.propTypes = {
141152

142153
Wizard.defaultProps = {
143154
basename: '',
155+
preserveQuery: false,
144156
history: null,
145157
onNext: null,
146158
render: null,

0 commit comments

Comments
 (0)