Highlighting Dynamic Parts Of A Pretty-Printed JSON Value In JavaScript
As part of the bulk export functionality that I'm building at work, the user needs to copy-paste a JSON value into an Amazon S3 bucket policy. This bucket policy is generated dynamically, by the app, based on the user's input. And to make the dynamic nature of the JSON value more obvious to the user, I wanted to visually highlight the values driven by the user's input. However, since the JSON payload was being generated by JSON.stringify()
, it took me a while to figure out to interleave the value demarcation.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
Before adding the highlights, the generated JSON value was being rendered as a simple string inside a <pre>
tag:
<pre><code>{{ vm.policy }}</code></pre>
The vm.policy
value was being generated as a pretty-printed string using the optional arguments of the JSON.stringify()
method:
vm.policy = JSON.stringify( data, null, 4 );
This use of JSON.stringify()
puts each data entry on its own line and indents each object level with 4 spaces.
In order to highlight some of this stringified output, I need to break the JSON value up into segments. Some of the segments will be highlighted and some of them won't. And, I want to organize this in such a way that maintaining the output is resilient to changes in the data structure.
What I ended up doing was creating an array of segments in which each segment has a value
property and a highlighted
property. Then, instead of interpolating the entire <pre>
tag as a single string, I iterate over these segments and output a series of <span>
tags inside the <pre>
tag.
Here's my HTML template using Alpine.js. It uses the x-for
directive for iteration and the :class
binding to conditionally add a highlighting class to the relevant <span>
tags. Since the <pre>
tag makes strict use of white-space, I'm mangling the tags in my output in order to make the code more readable without introducing additional white-space.
<pre
><code
><template x-for="segment in policyParts"
><span
x-text="segment.value"
:class="{
highlight: segment.highlight
}"
></span
></template
></code
></pre>
What this ends up creating is a pretty-printed JSON rendering in which some parts are highlighted and some parts are not:
In order to build up the collection of segments, I'm using a feature of JavaScript's String.split()
method in which the delimited can be captured in the results. This allows me to put placeholder tokens into my data structure, stringify the data structure, and then split the resultant JSON value on the placeholder tokens.
When I do this, I know that the .split()
delimiter is always captured in an odd offset within the resultant array (since I only have one captured group). Which means, when I'm mapping the substrings onto my JSON segments, odd values are highlighted and even values are not:
// First, we're going build a data-structure that contains placeholder
// values with a known pattern.
var policy = this._buildPolicy( ":::firstName:::", ":::lastName:::" );
// Second, we're going to STRINGIFY the data structure and SPLIT the JSON
// payload on the known pattern. And, by including part of the pattern in
// a capturing group, the .split() method will return each "delimiter" as
// an element interleaved with the rest of the natural segments. These
// segments will then be mapped onto an array of parts to be rendered in
// the PRE/CODE UI.
this.policyParts = JSON
.stringify( policy, null, 4 )
.split( /:::(\w+):::/g )
.map(
( segment, i ) => {
// Since we're capturing part of the delimiter, we know that
// the placeholder token is always in the ODD index. These
// segments will be highlighted in the output.
if ( i % 2 ) {
return {
value: this.form[ segment ],
highlight: true
};
}
return {
value: segment
};
}
)
;
As you can see, when mapping the .split()
results onto my policyParts
, I'm looking at the iteration index. When it's odd, I pull the value out of the form view-model and I mark it for highlighting. And, when it's even, I return it as is without highlighting.
Here's the full code for this demo:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="./main.css" />
</head>
<body>
<h1>
Highlighting Dynamic Parts Of A Pretty-Printed JSON Value
</h1>
<div x-data="Demo">
<form>
<input
type="text"
x-model="form.firstName"
@input="updatePolicy()"
placeholder="First name..."
/>
<input
type="text"
x-model="form.lastName"
@input="updatePolicy()"
placeholder="Last name..."
/>
<button type="button" @click="copyPolicy()">
Copy
</button>
</form>
<!--
The JSON for the policy will be accessible in two ways. First, the user can
use the COPY button above; and second, the user will need to be able to copy-
paste the text right out of the page. As such, white-space is relevant.
--
Note: Since PRE tags make strict use of white-space, I'm mangling my HTML tags
such that the line-breaks are part of the tag element and not part of the
interstitial tag space. This provides better readability without introducing
additional line-breaks in the output.
-->
<pre
><code
><template x-for="segment in policyParts"
><span
x-text="segment.value"
:class="{
highlight: segment.highlight
}"
></span
></template
></code
></pre>
</div>
<script type="text/javascript" src="../../vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>
<script type="text/javascript">
function Demo() {
return {
form: {
firstName: "",
lastName: ""
},
policyParts: [],
policyJson: "",
// Public methods.
init: $init,
copyPolicy: copyPolicy,
updatePolicy: updatePolicy,
// Private methods.
_buildPolicy: buildPolicy
}
// ---
// PUBLIC METHODS.
// ---
/**
* I initialize the component.
*/
function $init() {
this.updatePolicy();
}
/**
* I copy the current policy to the user's clipboard (not really).
*/
function copyPolicy() {
console.group( "Mock copy to Clipboard" );
console.log( this.policyJson );
console.groupEnd();
}
/**
* I update the policy using the current form view-model.
*/
function updatePolicy() {
// First, we're going build a data-structure that contains placeholder
// values with a known pattern.
var policy = this._buildPolicy( ":::firstName:::", ":::lastName:::" );
var fallbacks = {
firstName: "YOUR_FIRST_NAME",
lastName: "YOUR_LAST_NAME",
};
// Second, we're going to STRINGIFY the data structure and SPLIT the JSON
// payload on the known pattern. And, by including part of the pattern in
// a capturing group, the .split() method will return each "delimiter" as
// an element interleaved with the rest of the natural segments. These
// segments will then be mapped onto an array of parts to be rendered in
// the PRE/CODE UI.
this.policyParts = JSON
.stringify( policy, null, 4 )
.split( /:::(\w+):::/g )
.map(
( segment, i ) => {
// Since we're capturing part of the delimiter, we know that
// the placeholder token is always in the ODD index. These
// segments will be highlighted in the output.
if ( i % 2 ) {
return {
value: ( this.form[ segment ] || fallbacks[ segment ] ),
highlight: true
};
}
return {
value: segment
};
}
)
;
// Once we have all our JSON parts, we can create the full JSON structure
// but combining all the values.
// --
// Note: I could have just called JSON.stringify(buildPolicy()) again. But
// this approach makes sure that all of the white-space is the same and
// none of the logic is duplicated.
this.policyJson = this.policyParts
.map( part => part.value )
.join( "" )
;
}
// ---
// PRIVATE METHODS.
// ---
/**
* I build the policy data structure using the given values.
*/
function buildPolicy( firstName, lastName ) {
return {
plan: "WOOT.7.17",
version: 3,
holder: {
firstName: firstName,
lastName: lastName
},
expires: "2025-01-01"
};
}
}
</script>
</body>
</html>
At first, I was a bit stumped by this problem because I kept trying to think about how to add highlighting to the rendered JSON string variable. It wasn't until I thought about breaking the JSON value up into segments that the solution presented itself.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →