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

Add dotcom-ui-data-embed #812

Merged
merged 15 commits into from
May 14, 2020
Merged

Add dotcom-ui-data-embed #812

merged 15 commits into from
May 14, 2020

Conversation

NickColley
Copy link
Contributor

@NickColley NickColley commented May 12, 2020

In order to allow users to share data between the server and client introduce a new dotcom-ui-data-embed package.

Some parts of this package can be reused and consumed by the dotcom-ui-flags and dotcom-ui-app-context packages.

I have spiked this feature in the next-video-page application that was blocked without this feature.

Screenshot of kitchen sink example output

Screenshot of kitchen sink example output

Closes #799
Closes #761

@NickColley NickColley marked this pull request as ready for review May 12, 2020 12:38
@NickColley NickColley requested a review from a team as a code owner May 12, 2020 12:38
Copy link
Contributor

@andygout andygout left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nice, clean implementation. 👍

And I think the naming decisions ended well as this (to me at least) all reads really clearly.

I have added a few comments.

Is it possible to add a screengrab from Dev Tools of some dummy data being embedded in the DOM?

I am also going to have a go of running this branch locally.

@@ -64,6 +67,7 @@ module.exports = (_, response, next) => {
</section>
</div>
</Layout>
<DataEmbed id={DATA_EMBED_ID} data={{ foo: true, bar: 'qux' }} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this is not passed as an argument to Shell like flags and appContext? Would be good to keep the same pattern if possible.

In the Shell component, flags and appContext end up at the bottom of the body tags so as to be non-blocking: https://github.com/Financial-Times/dotcom-page-kit/blob/master/packages/dotcom-ui-shell/src/components/Shell.tsx#L76-L77. Is there a difference between the DataEmbed being here rather than there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There can be multiple instances of DataEmbed if a user wants, so right now it's up to them how to get data to DataEmbed.

In theory we could replicate the middleware and server packages of app-context but that felt more than what was initially agreed in the original issue.

Then DataEmbed would just be a generic AppContext.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory we could replicate the middleware and server packages

Feels like over-engineering? 😬 We don't expect many apps to interact with this tool directly

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There can be multiple instances of DataEmbed

So using this usage of Page Kit as an example, it would look something like:

const document = React.createElement(
	Shell,
	{ ...shellProps },
	[
		React.createElement(Layout, { ...layoutProps, contents: html }),
		React.createElement(ClientEmbed, id='custom-context-foobar' data={{ foo: true, bar: 'qux' }}),
		React.createElement(ClientEmbed, id='custom-context-barfoo' data={{ foo: false, bar: 'quux' }})
	]
);
  • This will require the document element created by the consuming app to have that many more arguments (at the moment it is quite clean with just Shell and Layout), but admittedly it is currently only one app that will be using ClientEmbed so is not a huge consideration.
  • In keeping to the principle of least surprise, users may well expect for all the client-embedded data (flags, appContext, custom) to sit alongside one another, and that this could be achieved by passing the custom data as part of the props given to the Shell component - is this easily possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this easily possible?

I think it is, but we'd want to mirror the dotcom-server-app-context and dotcom-middleware-app-context package functionality so that it is integrated in a similar way to app context which we discussed above.

So we could go ahead and flesh that functionality out, or intentionally keep this simpler and observe how people use the package before committing to something in page kit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Okay - let's keep it as is and monitor.

@@ -0,0 +1,88 @@
# @financial-times/dotcom-ui-data-embed

This package provides methods for putting data into your server-side rendered pages and safely retrieving it again in the browser.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This package provides methods for putting data into your server-side rendered pages and safely retrieving it again in the browser.
This package provides methods for putting data into your server-side rendered pages and retrieving it again in the browser.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the issue of safety factor here? (Honest question - I could have easily missed something.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is me mirroring language in app-context and flags, since the extra safety comes from the DataEmbedStore class rather than returning a regular object.


const dataEmbedClient = dataEmbed.init({ id: 'data-embed' })

if (dataEmbedClient.get('property')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are flags and appContext also frozen, i.e. would this be a breaking change?

Would have deep-freezing been an option (as opposed to just freezing the top-level properties) and offered any benefits for nested objects that may be used in the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm mostly worried about the performance cost of shipping a deep-freeze library and I'm not too sure of the consequences of us not doing this. I suppose we want to encourage people to make copies of the data rather than relying on the ability to mutate it deeply...

Should we investigate the bundle cost of doing a deep-freeze?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of freezing flags and appContext, that should be unchanged as they don't reuse the DataEmbedStore.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it just as a .freeze as flags and appContext currently are and worry about it if the nested properties being unfrozen ever becomes an issue.

I asked more for my curiosity.

}

get(key: string) {
return this.data.hasOwnProperty(key) ? this.data[key] : undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this.data[key] return undefined if the key does not exist as a property on this.data? I.e. is the conditional check necessary here?

Copy link
Contributor Author

@NickColley NickColley May 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll admit a lot of this code is me looking for patterns in flags and app-context and consolidating the logic.

So while I didn't make the decision, I can only assume it's to avoid you being able to try to 'get' properties built into the Object.prototype e.g. constructor or __proto__.

So I'm not too sure, but perhaps could be simplified alongside flags and app-context later on?

https://masteringjs.io/tutorials/fundamentals/hasownproperty

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is always the assumption of good faith that there is a good reason behind any pre-existing code. Also happy to leave it as is for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, definitely should have checked this, thanks for the prompt. :)

import loadDataEmbed from './loadDataEmbed'
import DataEmbedStore from './DataEmbedStore'

const init = ({ id }: { id: string }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it's convention, but are index files usually for just listing the modules by importing and exporting them? I.e. should this function live in its own file and be imported?

I say this when it could easily be possible that we have functions in index files elsewhere in this repo. Am just thinking out loud.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows what app-context and flags are doing. I agree it's a bit odd though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough - happy to leave as is.

Validation error: data should NOT have additional properties, received "isNotInTheSchema"

If you want to share application specific data with the client, consider using @financial-times/dotcom-ui-data-embed.
`.trim()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backticks can be frustrating. I wonder if there's a way to do multiline without inconsistent formatting and trim()… 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider adding a dependency like https://www.npmjs.com/package/outdent but I felt like that was a bit much for this small usecase. What do you think?

Copy link
Contributor Author

@NickColley NickColley May 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could always just do something like

'string\n' +
'second-string'

or

[
    'string',
    'second-string'
].join('\n')

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree with your feeling about importing a package just for this.

You could even have one really long line…

None is particularly nice so I will leave it to you to decide (which may as well be how it is currently).


If you want to share application specific data with the client, consider using @financial-times/dotcom-ui-data-embed.
`.trim()
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same backticks issue as above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I looked at this again I noticed there was an opportunity to improve the error message so we only output the additional information when they specifically encounter the error for adding additional properties. So I've managed to address the odd formatting as well at the same time...

Copy link
Contributor

@magsallen magsallen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work! I've added a few small comments but it's looking really good

packages/dotcom-ui-data-embed/README.md Outdated Show resolved Hide resolved
packages/dotcom-ui-data-embed/README.md Outdated Show resolved Hide resolved

### Client-side integration

Once you have you data embedded in your page you can use the [data embed client](#client-side-api) in your client-side code. The client provides methods for safely retrieving the status of individual context properties.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should individual context properties be individual data properties? Just wondering if the terminology has shifted since this was written

packages/dotcom-ui-data-embed/README.md Outdated Show resolved Hide resolved

Requires an `id` parameter within an options object, this `id` should match your embed `id` within the page.

## data embed client API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's super obvious what data embed client API means here. Would something like Component API work or have I misunderstood the structure?

packages/dotcom-ui-flags/src/server/formatFlagsJSON.ts Outdated Show resolved Hide resolved
packages/dotcom-ui-flags/src/server/formatFlagsJSON.ts Outdated Show resolved Hide resolved
packages/dotcom-server-app-context/README.md Show resolved Hide resolved
@@ -64,6 +67,7 @@ module.exports = (_, response, next) => {
</section>
</div>
</Layout>
<DataEmbed id={DATA_EMBED_ID} data={{ foo: true, bar: 'qux' }} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory we could replicate the middleware and server packages

Feels like over-engineering? 😬 We don't expect many apps to interact with this tool directly

@magsallen
Copy link
Contributor

The spike in next-video-page looks good 👌 Once these changes are released we should ask the Content Innovation team to test/review the new approach in that app, then we can refine this package if needed before rolling v0.8.x out more widely.

@NickColley NickColley force-pushed the dotcom-ui-data-embed branch from 3204cf6 to f34248d Compare May 13, 2020 09:09
Nick Colley added 12 commits May 13, 2020 10:20
Since `dotcom-ui-app-context` and `dotcom-ui-flags` share functionality this commit moves JSON output into the embed component which is the same as how it works in app-context. This'll hopefully make it easier to use the new package in the future.

May be a breaking change if formatFlagsJSON is part of the public API.
Also rename the file since there's no JSX syntax used in this file anymore.
Also rename the file since there's no JSX syntax used in this file anymore.
This allows users to parse the data embedded in the page.
Mirrors the classes found in flags and app-context.

Expose through fleshed out init method.
@NickColley NickColley force-pushed the dotcom-ui-data-embed branch from f34248d to ff280ae Compare May 13, 2020 09:21

### `init({ id }:{ id: string })`

Initialises and returns a new [data embed client](#data-embed-client-api) which can be used to safely access the status of individual contexts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would 'properties' or 'values' work in place of 'contexts' here? I'd rather not introduce any confusion with appContext, and calling things 'context' in the past has called all manner of confusion 🙈

expect(instance.get('baz')).toBe('qux')
})

it('returns undefined for contexts which do not exist', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See pedantic 'contexts' comment above

@NickColley NickColley merged commit 4be5e22 into master May 14, 2020
@NickColley NickColley deleted the dotcom-ui-data-embed branch May 14, 2020 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Create a dotcom-ui-client-embed package for sharing data between the server and the client
3 participants