diff --git a/src/components/utilities/sprintf/examples/index.js b/src/components/utilities/sprintf/examples/index.js
index 97820eb9840086466e21653dcca9843576531279..9d489c9718b8d4b8194a6e552685d24c4c85d3b0 100644
--- a/src/components/utilities/sprintf/examples/index.js
+++ b/src/components/utilities/sprintf/examples/index.js
@@ -5,18 +5,18 @@ export default [
{
name: 'Basic',
items: [
- {
- id: 'sprintf-basic',
- name: 'Basic',
- description: 'Basic sprintf',
- component: SprintfBasicExample,
- },
{
id: 'sprintf-interpolated-content',
name: 'Interpolated content',
description: 'Interpolated content passed to scoped slots',
component: SprintfInterpolatedExample,
},
+ {
+ id: 'sprintf-basic',
+ name: 'Basic placeholders',
+ description: 'Basic sprintf placeholders',
+ component: SprintfBasicExample,
+ },
],
},
];
diff --git a/src/components/utilities/sprintf/examples/sprintf.basic.example.vue b/src/components/utilities/sprintf/examples/sprintf.basic.example.vue
index e49168fbe0bf752b8fad8c8f80087dd8588bc9be..b4c962d2fb5371b3f561c4614e347643624000b2 100644
--- a/src/components/utilities/sprintf/examples/sprintf.basic.example.vue
+++ b/src/components/utilities/sprintf/examples/sprintf.basic.example.vue
@@ -1,8 +1,19 @@
+
+
- Author
+ {{ authorName }}
diff --git a/src/components/utilities/sprintf/sprintf.md b/src/components/utilities/sprintf/sprintf.md
index 9b315cc87a58737a58995710ad472461f6e15ff2..d5b2b4bedaeb4f18b6a17c965f153cee8c89f4bc 100644
--- a/src/components/utilities/sprintf/sprintf.md
+++ b/src/components/utilities/sprintf/sprintf.md
@@ -1,44 +1,30 @@
-# sprintf
+# GlSprintf
-## Basic
+## Overview
The `GlSprintf` component lets you do `sprintf`-style string interpolation with
child components. Each placeholder in the translated string, provided via the
-`message` prop, becomes a slot that you can use to insert any component in the
-rendered output.
+`message` prop, becomes a slot that you can use to insert any components or
+markup in the rendered output.
> NOTE: `gl-sprintf` does not translate the message for you; you must provide
> it already translated. In the following examples, it is assumed that
> a `gettext`-style `__` translation function is available in your Vue
> templates.
-```html
-
-
-
- Author
-
-
-
-```
-
-The example above renders to this HTML:
-
-```html
-Written by Author
-```
-
-## Interpolated content
+## Displaying messages with text between placeholders (e.g., links, buttons)
Sentences should not be split up into different messages, otherwise they may
-not be translatable in certain languages. To help with this, `GlSprintf` is
-able to interpolate between placeholders suffixed with `Start` and `End`, and
-pass whatever is between them to the scoped slot of the base name, via the
-`content` property.
+not be translatable into certain languages. To help with this, `GlSprintf`
+interprets placeholders suffixed with `Start` and `End` to indicate the
+boundaries of a component to display within the message. Any text between
+them is passed, via the `content` scoped slot property, to the slot name common
+to the placeholders.
-For example:
+For example, using `linkStart` and `linkEnd` placeholders in a message defines
+a `link` scoped slot:
```html
@@ -66,13 +52,9 @@ will render as:
```
-The example above is formatted for readability, and so its whitespace
-is not technically correct, however, `GlSprintf` _will_ preserve whitespace
-correctly.
-
-Note that _any_ arbitrary Vue component(s) can be used within a scoped slot,
-and that the content passed to it can be used in any way at all; for instance,
-as regular text, or in component attributes or slots.
+Note that _any_ arbitrary HTML tags or Vue component(s) can be used within
+a scoped slot, and that the content passed to it can be used in any way at all;
+for instance, as regular text, or in component attributes or slots.
Here's a more complex example, which `` lets you do in a breeze:
@@ -83,7 +65,10 @@ Here's a more complex example, which `` lets you do in a breeze:
{{ content }}
-
+
+ {{ content }}
+
{{ content }}
+
@@ -94,8 +79,103 @@ cannot easily be used. In addition, a JS-only solution is more likely to be
prone to XSS attacks, as the Vue compiler isn't available to help protect
against them.
+## Displaying components within a message
+
+Use slots to replace placeholders in the message with the slots' contents.
+There is a slot for every placeholder in the message. For example, the `author`
+slot name can be used when there is an `%{author}` placeholder in the message:
+
+```html
+
+
+
+
+
+
+ {{ authorName }}
+
+
+
+
+```
+
+The example above renders to this HTML:
+
+```html
+Written by Some author
+```
+
## Usage caveats
+### White space
+
+`GlSprintf` does not handle white space in scoped slots specially; it is passed
+through and rendered just like regular text. This means that white space in the
+scoped slot templates *themselves*, including newlines and indentation, are
+passed through untouched (assuming the template compiler you're using doesn't
+trim text nodes at compile time; `vue-template-compiler` preserves white space
+by default, for instance).
+
+Most of the time you don't need to worry about this, since
+[browsers normalize white space][1] automatically, but here's an example, using
+punctuation, where you might want to be conscious of the white space in the
+template:
+
+```html
+
+
+
+
+ {{ content }}
+
+
+
+
+```
+
+As written, the literal markup rendered would be:
+
+```html
+ Foo
+ bar
+ !
+
+```
+
+where the white space (including newlines) before and after `bar` is exactly
+the newlines and indentation in the source template. The browser will render
+this as:
+
+ Foo
+ bar
+ !
+
+
+Note the single space between `bar` and `!`. To avoid that, remove the
+white space in the template, or use `v-text`:
+
+```html
+
+
+
+ {{ content }}
+
+
+
+
+
+```
+
+### Miscellaneous
+
While there are a lot of caveats here, you don't need to worry about reading
them _unless_ you find `GlSprintf` isn't rendering what you'd expect.
@@ -103,25 +183,27 @@ them _unless_ you find `GlSprintf` isn't rendering what you'd expect.
a component's root, it must be wrapped with at least one other root element,
otherwise Vue will throw a `Multiple root nodes returned from render
function` error.
-- If a slot for a given named interpolation _isn't_ provided, the interpolation
+- If a slot for a given placeholder _isn't_ provided, the placeholder
will be rendered as-is, e.g., literally `Written by %{author}` if the
`author` slot _isn't_ provided, or literally `%{linkStart}foo%{linkEnd}` if
the `link` slot isn't provided.
- Content between `Start` and `End` placeholders is effectively thrown away if
the scoped slot of the correct name doesn't consume the `content` property in
some way, though the slot's components should still be rendered.
-- If there's no named interpolation in the message for a provided named slot,
- the content of that slot is silently thrown away.
+- If there's no placeholder in the message for a provided named slot, the
+ content of that slot is silently thrown away.
- If only one of the `Start` or `End` placeholders is in the message, or they
- are in the wrong order, they are treated as plain slots. This allows you to
+ are in the wrong order, they are treated as plain slots, i.e., it is assumed
+ there is no text to extract and pass to the scoped slot. This allows you to
use plain slots whose names end in `Start` or `End`, e.g., `backEnd`, or
- `fromStart`, without interpolating content into them.
-- Interpolation between `Start` and `End` placeholders is only done one level
+ `fromStart` in isolation, without their `Start`/`End` counterparts.
+- Text extraction between `Start` and `End` placeholders is only done one level
deep. This is intentional, so as to avoid building complex sprintf messages
that would better be implemented in components. As an example,
`${linkStart}test%{icon}%{linkEnd}`, if provided both the `link` and `icon`
slots, would pass `test%{icon}` as a literal string as content to the `link`
scoped slot.
+- For more examples and edge cases, please see the test suite for `GlSprintf`.
- To be successfully used in `GlSprintf`, slot names should:
* start with a letter (`[A-Za-z]`)
* only contain alpha-numeric characters (`[A-Za-z0-9]`), underscore (`_`) and
@@ -135,3 +217,5 @@ them _unless_ you find `GlSprintf` isn't rendering what you'd expect.
This component uses [`String.prototype.startsWith()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith) and [`String.prototype.endsWith()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith) under the hood. Make sure those methods are polyfilled if you plan on using the component on IE11.
> NOTE: These methods are already polyfilled in GitLab: [`app/assets/javascripts/commons/polyfills.js#L15-16`](https://gitlab.com/gitlab-org/gitlab/blob/dc60dee6ed6234dda9f032195577cd8fad9646d8/app/assets/javascripts/commons/polyfills.js#L15-16)
+
+[1]: https://www.w3.org/TR/css-text-3/#white-space-phase-1
diff --git a/src/components/utilities/sprintf/sprintf.stories.js b/src/components/utilities/sprintf/sprintf.stories.js
index 999215e9f5dd969f9c22fa9f4b0f10058e82b773..8af651c4c26025c7d752a7641aff168321931b48 100644
--- a/src/components/utilities/sprintf/sprintf.stories.js
+++ b/src/components/utilities/sprintf/sprintf.stories.js
@@ -20,42 +20,44 @@ function generateProps({ message = 'Written by %{author}' } = {}) {
documentedStoriesOf('utilities|sprintf', readme)
.addDecorator(withKnobs)
- .add('default', () => ({
- props: generateProps(),
+ .add('sentence with link', () => ({
+ props: generateProps({
+ message: 'Click %{linkStart}here%{linkEnd} to reticulate splines.',
+ }),
components,
template: `
-
- Author
+
+ {{ content }}
`,
}))
- .add('with a button', () => ({
+ .add('basic placeholder', () => ({
props: generateProps(),
components,
+ data: () => ({ authorName: 'Some author' }),
template: `
- Author
+ {{ authorName }}
`,
}))
- .add('interpolated content', () => ({
- props: generateProps({
- message: 'Click %{linkStart}here%{linkEnd} to reticulate splines.',
- }),
+ .add('basic button placeholder', () => ({
+ props: generateProps(),
components,
+ data: () => ({ authorName: 'Some author' }),
template: `
-
- {{ content }}
+
+ {{ authorName }}
diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-basic-button-placeholder-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-basic-button-placeholder-1-snap.png
new file mode 100644
index 0000000000000000000000000000000000000000..806a90d2fe8a718e18a5045f0ccc48db40b31551
Binary files /dev/null and b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-basic-button-placeholder-1-snap.png differ
diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-basic-placeholder-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-basic-placeholder-1-snap.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec2e2491d08744caed735a2fad34fae36e2b5ab4
Binary files /dev/null and b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-basic-placeholder-1-snap.png differ
diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-default-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-default-1-snap.png
deleted file mode 100644
index c2c57b668ff3de7fb4aa38d23cf3b490650c590e..0000000000000000000000000000000000000000
Binary files a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-default-1-snap.png and /dev/null differ
diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-interpolated-content-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-sentence-with-link-1-snap.png
similarity index 100%
rename from tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-interpolated-content-1-snap.png
rename to tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-sentence-with-link-1-snap.png
diff --git a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-with-a-button-1-snap.png b/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-with-a-button-1-snap.png
deleted file mode 100644
index 59b3da1d0aa2452a94b2eb65bc84fc27c0ef047e..0000000000000000000000000000000000000000
Binary files a/tests/__image_snapshots__/storyshots-spec-js-image-storyshots-utilities-sprintf-with-a-button-1-snap.png and /dev/null differ