-
Notifications
You must be signed in to change notification settings - Fork 3
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
refactor: rewrite breadcrumbs to use Lit and add tests #32
Conversation
// Grab existing children at the point that the component is added to the page | ||
// Interleave "/" separator with breadcrumbs | ||
this._children = interleave(Array.from(this.children)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
intercept the child elements when the component is first added to the page and interleave them with slash character separators. Store the result as the internal property _children for use in the render method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This interleaving is reactive in Vue (and I think React), but not here. Are we OK with that? Should that be disclosed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsure. It is perhaps indicative of one of the differences between react/vue and custom elements. I toyed around with using dedicated child elements instead of this which is maybe one solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with us making a call that people won't treat the children as reactive and fixing it later.
I think this is a lot easier to get into trouble with in Vue/React than it is manually monkeying with DOM elements. But I defer to your judgement here!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you say not reactive do you mean in the context of using this custom element in React or Vue, or as in if the user updates the props, it wouldn't update the DOM element? @pearofducks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean this implementation in the custom element isn't watching children - so if the user adds/removes breadcrumbs it won't re-interleave them.
<h2 class="sr-only">Her er du</h2> | ||
${this._children} | ||
</nav> | ||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The render method ignores the original children and instead renders out the interleaved ones stored in this._children
packages/utils/index.js
Outdated
return this; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very simple base class that disables Shadow DOM. Is there much point in using this?
assert.equal(breadcrumbs[0].innerText, 'Eiendom'); | ||
assert.equal(breadcrumbs[1].innerText, 'Torget'); | ||
assert.equal(breadcrumbs[2].innerText, 'Oslo'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests look how you would expect but are run (by web test runner) in a browser context so that document
is available and so on.
What does these bring to the table over running ex plain PlayWright and ex tap? |
Without slots, how do we approach a component like Modal and its footer slot/area? Is there an approach similar to what React does there? |
.join('<span class="select-none" aria-hidden="true">/</span>'); | ||
const separator = html`<span class="select-none" aria-hidden="true">/</span>`; | ||
const interleave = (arr) => | ||
[].concat(...arr.map((el) => [el, separator])).slice(0, -1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whats the purpose of the [].concat
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
map creates pairs of elements and separators [el, separator] [el, separator] [el, separator]
, concat joins these array pairs together so you end up with 1 array with [el, separator, el, separator, el, separator]
. Then the slice trims off the last separator so you end up with [el, separator, el, separator, el]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't have to support Edge 16-18 or iOS11 we could instead array.flatMap(el => [el, separator]).slice(1)
I hear you, I don't love it either but what you describe is not easy to setup. Playwright by itself as far as I can see is designed to test against some running page. It very much is not designed to support fixturing individual components yet which means we would need to do this ourselves. (See: microsoft/playwright#7148 for example) To build something on top of tap and playwright it seems like we would need to write something to serve a page and support setting up and tearing down the page for each test and then something to add fixture the components to the page. In each test you would then use the playwright apis to write tests agains the fixtured page. At that point it's probably easier to just use jsdom in tap. Lit recommends web test runner: https://lit.dev/docs/tools/testing/ EDIT: I guess if we were to engineer something from the ground up, nicest would probably be a lib that creates a fastify server that is started on a random port for each test and has apis for stop, start, fixturing components etc. Then as part of your playwright test you would write something like this: test('test a component', () => {
const server = await fixtureServer();
const component = html`<my-component></my-component>`;
server.fixture(component);
await page.goto(server.url);
const title = page.locator('.navbar__inner .navbar__title');
await expect(title).toHaveText('whatever');
}); |
Yea, its a bit of a loss for sure. Probably a couple ways you could approach it, some more hacky than others. You could provide component building blocks devs should use so we can grab them out of the children and place them appropriately for example. Eg. <f-modal>
<f-modal-header>stuff</f-modal-header>
<f-modal-content>more stuff</f-modal-content>
<f-modal-footer>even more stuff</f-modal-footer>
</f-modal> Slots would be nicer. |
Just so I understand the term here. Is what we call "disabling Shadow DOM" the same as defining Shadow DOM to be open? Or are we "disabling" it in another way? |
No, we are talking about disabled vs enabled, not open vs closed Shadow DOM. Ie. We are turning Shadow DOM off for the components rather than enabling Shadow DOM but deciding whether to use open or closed mode. In Lit you can write your components without Shadow DOM by adding: createRenderRoot() {
return this;
} By default, Lit creates the Shadow DOM in open mode and, as I understand it, there's very little value in using the closed Shadow DOM. Ref: https://blog.revillweb.com/open-vs-closed-shadow-dom-9f3d7427d1af Lit gives you access to a property called |
I managed to prototype up a fastify test server that mounts components like so: import fs from 'fs';
import fastify from 'fastify';
export class Server {
_component = '';
constructor() {
this.app = fastify();
this.app.get('/scripts.js', function (request, reply) {
const stream = fs.createReadStream(new URL('../../dist/index.js', import.meta.url), 'utf8');
reply.type('application/javascript');
reply.send(stream);
});
this.app.get('/', (req, reply) => {
reply.type('text/html');
reply.send(`
<html>
<head>
<link href="https://assets.finn.no/pkg/@fabric-ds/css/v1/fabric.min.css" type="text/css" rel="stylesheet" />
<script type="module">
import "./scripts.js";
import { html, render } from 'https://cdn.skypack.dev/lit';
render(
html\`${this._component}\`,
document.querySelector('body')
)
</script>
</head>
<body></body>
</html>
`);
});
}
fixture(markup) {
this._component = markup;
}
async start() {
return this.app.listen('4021', '0.0.0.0');
}
async stop() {
return this.app.close();
}
} You then use it like this: import { test } from 'tap';
import { Server } from './server.js';
const server = new Server();
await server.start();
test('Breadcrumb component renders on the page', async () => {
// GIVEN: A component with 1 breadcrumb
const component = `<f-breadcrumbs
><a href="#/url/1">Eiendom</a></f-breadcrumbs
>`;
// WHEN: the component is added to the page
server.fixture(component);
// THEN: the component is visible in the DOM
// use playwright to assert tests against the page located at http://localhost:4021/
}); Thoughts @trygve-lie ? |
Ok. Which brings me to:
I think we need to address this first. Our main challenge here is actually aligning / sharing CSS between the components and aligning / sharing CSS between the document and components. So there is actually two challenges here. The first challenge is to share CSS between all web components we build. This is doable and is done by having a component exporting all CSS: import { css } from 'lit';
export const buttonStyles = css`
.blue-button {
color: white;
background-color: blue;
}
.blue-button:disabled {
background-color: grey;
}`; And then all web components must import this component: import { buttonStyles } from './button-styles.js';
class MyElement extends LitElement {
static styles = [
buttonStyles,
css`
:host { display: block;
border: 1px solid black;
}`
];
} What I imagine is that we publish a Tailwind Web Component which exports all the styles and all components then import this. To do so we probably need a custom build step / tool to build this component but I imagine it being pretty doable. The above will make sure that all Web Components will have one CSS loaded once which all use. Each component will not have to load or bundle its own set of CSS. This leave us with one problem though; the Web Components will have its own styles in JS while the rest of the document will pull in the styles as CSS. This mean that a page with a Web Component on it will load the same CSS twice. If we able to make it so that Web Components and the document can use the same CSS (loaded once) in a page, we can use Shadow DOM in all Web Components and then also slots. There is no defined way of doing this, but I have a couple of ideas on how we can approach it but I am not sure discussing that belongs in this PR. |
I've pushed a change to the test setup that shows using Tap and Playwright. It seems to work pretty nicely. There is a server.js file that sets up a fastify server to fixture components and serve them on a page and configure playwright to test against this page. Interested to hear your thoughts on this approach @trygve-lie |
Cool, makes sense. Interested to hear what possible solutions to avoid duplication you have come up with. Have to admit, in my head this seems like a tricky problem. |
packages/breadcrumbs/server.js
Outdated
await this._browser.close(); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's lots that could be done to generalise this file but for now its a quick and dirty prototype which proves the setup.
I've updated this PR to use the constructed stylesheet approach we have discussed. For this change to work, this PR needs to be merged and published: fabric-ds/css#68 |
## [0.1.2-next.1](v0.1.1...v0.1.2-next.1) (2022-04-25) ### Bug Fixes * decouple elements and toast api to fix SSR usage ([57ee9d6](57ee9d6))
## [0.1.2](v0.1.1...v0.1.2) (2022-04-25) ### Bug Fixes * decouple elements and toast api to fix SSR usage ([f7e57cb](f7e57cb))
The latest on the custom element prototyping... There are 3 areas to talk about:
I have now done quite a lot of experimentation with Lit and SSR. I have made several working prototypes and experimented with different approaches to including (or building) Fabric and I have come to the following conclusions:
You can see some POCs for different approaches to Lit SSR here: https://github.schibsted.io/richard-walker/ssr_lit_podlet Specifically:
Final Summary For now, I believe we should:
|
🎉 This PR is included in version 0.1.3-next.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
🎉 This PR is included in version 1.0.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
I'm attempting to create a reference implementation for how best to write Lit Element based custom elements going forward. This includes a test setup and some basic tests.
Component Design
No Shadow DOM
As discussed previously, I've disabled Shadow DOM. We theorised in earlier discussions that there wasn't much to be gained at the component level by using the shadow dom (better at the podlet level) and not using it makes working with the Fabric CSS file easier. This also brings things more in line with the Vue and React implementations.
No slots
Not using the Shadow DOM does however mean that slots are no longer available. We can certainly work with this but it is somewhat of a shame since slots offer a powerful way to template.
Fabric CSS loaded globally
Since there's no component level isolation, there's no need to load the Fabric CSS file for every component. We can instead insist that Fabric Element users must include the Fabric CSS file in the page as we do with both Vue and React.
Global CSS
We discussed, during planning, the possibility of building inline styles per component. It's hard to see the benefit of this approach when we already build a single global package for all other use cases. We could inline per component which would likely result in very small inline css per component but when most pages will be including the global CSS file anyway, even a single kb extra is a waste. This is an area we could play around with more, however, in my mind this only really makes sense if we intend to keep Shadow DOM at the component level which so far we are leaning away from.
Idiomatic Lit
I've attempted to write in a Lit Idiomatic way. Additionally, I've created a base class as we discussed which actually does very little. It simply switches off Shadow DOM but could be used in future as needed. I'm a bit on the fence as to whether there's really much point in this base class to keen to hear feedback on this.
Testing
Web Test Runner and @open-wc/testing seem to be the recommended approach to testing Lit Element components. This works well, and was easy to setup.
I tried to use tap instead of the in built test framework stuff that comes included but this proved challenging.
It should be possible to implement but requires work that may or may not be worth the effort. See: https://modern-web.dev/docs/test-runner/test-frameworks/write-your-own/ This could be fun to try to get working for a hack day's project.
Web Test Runner wraps tests inside a browser context and can be configured to use puppeteer or playwright etc. See: https://modern-web.dev/docs/test-runner/browser-launchers/overview/
My final testing recommentation then is basically: