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

Allow nested field values in session #573

Merged
merged 1 commit into from
Sep 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

New features:

- [Allow nested field values in session](https://github.com/alphagov/govuk-prototype-kit/pull/573)
- [Restart the app if environment variables change](https://github.com/alphagov/govuk-prototype-kit/pull/389)
- [Make it more difficult to accidentally clear the session data](https://github.com/alphagov/govuk-prototype-kit/pull/588)

Expand Down
79 changes: 77 additions & 2 deletions docs/documentation/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,87 @@ The easiest way to clear session data is to use 'Incognito mode' for each user,

In a route function, refer to `req.session`.

For example you might have `req.session.over18` or `req.session.firstName`.
### Accessing fields from the session

For example, when submitting the following (simplified) HTML:

```html
<input name="first-name" value="Sarah">
<input name="last-name" value="Philips">
```

You'll have a `req.session.data` object in your route function:

```js
{
'first-name': 'Sarah',
'last-name': 'Philips'
}
```

These two field values can be accessed in JavaScript as:

```js
req.session.data['first-name']
req.session.data['last-name']
```

Or in views as:

```
{{ data['first-name'] }}
{{ data['last-name'] }}
```
colinrotherham marked this conversation as resolved.
Show resolved Hide resolved

### Accessing nested fields from the session

Session data can also be nested for easy grouping. For example answers from multiple family members:

```html
<input name="claimant[first-name]" value="Sarah">
<input name="claimant[last-name]" value="Philips">

<input name="partner[first-name]" value="Michael">
<input name="partner[last-name]" value="Philips">
```

You'll have a nested `req.session.data` object in your route function:

```js
{
claimant: {
'first-name': 'Sarah',
'last-name': 'Philips'
},
partner: {
'first-name': 'Michael',
'last-name': 'Philips'
}
}
```

These four field values can be accessed in your route function as:

```js
req.session.data['claimant']['first-name']
req.session.data['claimant']['last-name']
req.session.data['partner']['first-name']
req.session.data['partner']['last-name']
```

Or in views as:

```
{{ data['claimant']['first-name'] }}
{{ data['claimant']['last-name'] }}
{{ data['partner']['first-name'] }}
{{ data['partner']['last-name'] }}
```

You can see a full example here:

[https://github.com/expressjs/session#example](https://github.com/expressjs/session#example)

You can read more about Express Session here:

[https://github.com/expressjs/session](https://github.com/expressjs/session)
[https://github.com/expressjs/session](https://github.com/expressjs/session)
62 changes: 60 additions & 2 deletions docs/views/examples/pass-data/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ <h2 class="govuk-heading-m">How to use</h2>

<pre class="app-code"><code>&lt;p&gt;{%raw%}{{ data['first-name'] }}{%endraw%}&lt;/p&gt;</code></pre>

<p>Or with nested fields:</p>

<pre class="app-code"><code>&lt;p&gt;{%raw%}{{ data['claimant']['first-name'] }}{%endraw%}&lt;/p&gt;</code></pre>

<h3 class="govuk-heading-s">
Clearing data
</h3>
Expand All @@ -69,7 +73,11 @@ <h3 class="govuk-heading-s">

<p>For a radio or checkbox input you need to use the 'checked' function:</p>

<pre class="app-code"><code>&lt;input type="radio" name="over-18" value="yes" {%raw%}{{ checked('over-18','yes') }}{%endraw%}&gt;</code></pre>
<pre class="app-code"><code>&lt;input type="radio" name="over-18" value="yes" {%raw%}{{ checked("over-18", "yes") }}{%endraw%}&gt;</code></pre>

<p>Or with nested fields:</p>

<pre class="app-code"><code>&lt;input type="radio" name="claimant[over-18]" value="yes" {%raw%}{{ checked("['claimant']['over-18']", "yes") }}{%endraw%}&gt;</code></pre>

<h3 class="govuk-heading-s">
Setting default data
Expand Down Expand Up @@ -119,16 +127,66 @@ <h3 class="govuk-heading-s">
}
]
}) }}
{% endraw %}<code></pre>

<h3 class="govuk-heading-s">
Using the data in Nunjucks macros (nested fields)
</h3>

<p>Example using the 'checked' function in a checkbox component macro (nested fields for multiple vehicles):</p>

<pre class="app-code"><code>{% raw %}
{{ govukCheckboxes({
name: "vehicle1[vehicle-features]"
fieldset: {
legend: {
text: "Which of these applies to your vehicle?"
}
},
hint: {
text: "Select all that apply"
},
items: [
{
value: "Heated seats",
text: "Heated seats",
id: "vehicle1-vehicle-features-heated-seats",
checked: checked("['vehicle1']['vehicle-features']", "Heated seats")
},
{
value: "GPS",
text: "GPS",
id: "vehicle1-vehicle-features-gps",
checked: checked("['vehicle1']['vehicle-features']", "GPS")
},
{
value: "Radio",
text: "Radio",
id: "vehicle1-vehicle-features-radio",
checked: checked("['vehicle1']['vehicle-features']", "Radio")
}
]
}) }}
{% endraw %}<code></pre>

<h3 class="govuk-heading-s">
Using the data on the server
</h3>

<p>You can access the data on the server in a route, for example for an input with name="first-name":</p>
<p>You can access the data on the server in a route function</p>

<p>For example for an input with name="first-name":</p>

<pre class="app-code"><code>var firstName = req.session.data['first-name']</code></pre>

<h3 class="govuk-heading-s">
Using the data on the server (nested fields)
</h3>

<p>For example for an input with name="claimant[first-name]":</p>

<pre class="app-code"><code>var firstName = req.session.data['claimant']['first-name']</code></pre>

<h3 class="govuk-heading-s">
Ignoring inputs
</h3>
Expand Down
27 changes: 21 additions & 6 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const fs = require('fs')

// NPM dependencies
const basicAuth = require('basic-auth')
const getKeypath = require('keypather/get')
const marked = require('marked')
const path = require('path')
const portScanner = require('portscanner')
Expand Down Expand Up @@ -34,7 +35,12 @@ exports.addCheckedFunction = function (env) {
return ''
}

var storedValue = this.ctx.data[name]
// Use string keys or object notation to support:
// checked("field-name")
// checked("['field-name']")
// checked("['parent']['field-name']")
name = !name.match(/[.[]/g) ? `['${name}']` : name
var storedValue = getKeypath(this.ctx.data, name)

// Check the requested data exists
if (storedValue === undefined) {
Expand Down Expand Up @@ -243,7 +249,7 @@ exports.matchMdRoutes = function (req, res) {
}

// Store data from POST body or GET query in session
var storeData = function (input, store) {
var storeData = function (input, data) {
for (var i in input) {
// any input where the name starts with _ is ignored
if (i.indexOf('_') === 0) {
Expand All @@ -254,7 +260,7 @@ var storeData = function (input, store) {

// Delete values when users unselect checkboxes
if (val === '_unchecked' || val === ['_unchecked']) {
delete store.data[i]
delete data[i]
continue
}

Expand All @@ -264,9 +270,18 @@ var storeData = function (input, store) {
if (index !== -1) {
val.splice(index, 1)
}
} else if (typeof val === 'object') {
// Store nested objects that aren't arrays
if (typeof data[i] !== 'object') {
data[i] = {}
}

// Add nested values
storeData(val, data[i])
continue
}

store.data[i] = val
data[i] = val
}
}

Expand All @@ -289,8 +304,8 @@ exports.autoStoreData = function (req, res, next) {

req.session.data = Object.assign({}, sessionDataDefaults, req.session.data)

storeData(req.body, req.session)
storeData(req.query, req.session)
storeData(req.body, req.session.data)
storeData(req.query, req.session.data)

// Send session data to all views

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"gulp-sass": "^4.0.1",
"gulp-sourcemaps": "^2.6.0",
"gulp-util": "^3.0.7",
"keypather": "^3.0.0",
"marked": "^0.4.0",
"minimist": "1.2.0",
"notifications-node-client": "^4.1.0",
Expand Down