Skip to content

Remove Ow #615

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

Merged
merged 1 commit into from
Jan 22, 2025
Merged

Remove Ow #615

merged 1 commit into from
Jan 22, 2025

Conversation

clintcs
Copy link
Collaborator

@clintcs clintcs commented Jan 21, 2025

🚀 Description

I've been meaning to revisit our use of Ow—especially owSlot() and owSlotType()—for a while now. After doing some research I found the functionality in owSlot() and owSlotType() would be much better as a custom directive.

Modal is the best showcase for how a directive simplifies things. No more firstUpdated() assertions. Fewer refs. And no more "slotchange" listeners with assertions.

Instead, a single directive is placed on the slot we're asserting against:

<li class="action">
  <slot
    name="secondary"
-   @slotchange=${this.#onFooterMenuSecondarySlotChange}
-  ${ref(this.#footerMenuSecondarySlotElementRef)}
+  ${assertSlot([GlideCoreButton], true)}
  ></slot>
</li>

assertSlot() takes two arguments—both optional:

assertSlot(slotted?: (typeof Element | typeof Text)[] | null, isOptional?: boolean)

The first is an array of allowed node types—similar to owSlotType(). The second is a boolean. If a slot is optional, it doesn't have to have content. But, if it does, then the content needs to satisfy the types from the first argument.

Modal's "secondary" slot is an example of a slot that's optional but requires specific content.

Accordion's default slot is an example of a non-optional slot that allows arbitrary content. While assertSlot() simplifies Modal the most, every component will see some simplification—at least a reduction of a few lines:

<slot
  class=${classMap({
    'default-slot': true,
    indented: this.hasPrefixIcon,
  })}
-  @slotchange=${this.#onDefaultSlotChange}
-  ${assertSlot()}
  ${ref(this.#defaultSlotElementRef)}
></slot>

What do you think?


After implementing assertSlot() throughout, Ow became even less useful.

I initially thought Ow was worth using (or at least trying out) to reduce indentation and improve readability. Menu is a case where Ow arguably carried its weight. But most other uses of Ow (aside from it's use via owSlot() and owSlotType()) were marginally useful at best—for example, this use of Ow in Dropdown.

Then there's the fact that we can't replace every conditional with an Ow assertion because some conditionals aren't strictly for type narrowing but exist because the property or element in question may legitimately be undefined.

Plus Ow adds about 9 KB to our package and complicates our Web Test Runner configuration. So it felt like it was time to remove it altogether.

Yay? Nay?

📋 Checklist

🔬 How to Test

📸 Images/Videos of Functionality

N/A

Copy link

changeset-bot bot commented Jan 21, 2025

🦋 Changeset detected

Latest commit: ecdf00d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@crowdstrike/glide-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

@clintcs clintcs force-pushed the remove-ow branch 3 times, most recently from f3cd80b to 3a229f1 Compare January 21, 2025 23:30
@clintcs clintcs marked this pull request as ready for review January 21, 2025 23:31
Copy link
Collaborator

@ynotdraw ynotdraw left a comment

Choose a reason for hiding this comment

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

I like the directive, because it moves the assertions closer to the markup. When a developer is looking at a slot, they can see we assert that slot for certain types of content.

Contrast that with what we have prior to this PR - there's a bit more scrolling around and diving in to understand (checking firstUpdated, among other places).


From what I could tell, there shouldn't be any issue, but I'll ask anyway - any concerns from your end around async slotted content coming in like we had in the past?

CONTRIBUTING.md Outdated
Copy link
Collaborator

Choose a reason for hiding this comment

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

So much easier/cleaner to document too!

@@ -2,6 +2,7 @@

import { expect, fixture, html } from '@open-wc/testing';
import GlideCoreAccordion from './accordion.js';
import expectUnhandledRejection from './library/expect-unhandled-rejection.js';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Dig the name of this helper - very clear of its intent 👍

@@ -3,7 +3,7 @@
import { expect, fixture, html } from '@open-wc/testing';
import GlideCoreButtonGroup from './button-group.js';
import GlideCoreButtonGroupButton from './button-group.button.js';
import expectArgumentError from './library/expect-argument-error.js';
import expectWindowError from './library/expect-window-error.js';
Copy link
Collaborator

Choose a reason for hiding this comment

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

This one too!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hah. Yeah. Figured they'd do better as two separate libraries so the name of each could be exact. They're also general enough to be usable not just for when slots throws.

Comment on lines 241 to 265
it('throws when a typed and required default slot only contains text', async () => {
const stub = sinon.stub(console, 'error');
const spy = sinon.spy();
const onerror = window.onerror;

// eslint-disable-next-line unicorn/prefer-add-event-listener
window.onerror = spy;

await fixture<GlideCoreWithSlot>(
html`<glide-core-with-slot .slotted=${[HTMLButtonElement]}>
</glide-core-with-slot>`,
);

expect(spy.callCount).to.equal(1);

expect(spy.args.at(0)?.at(0)).to.equal(
'Uncaught TypeError: Expected GlideCoreWithSlot to have a slotted element that extends HTMLButtonElement.',
);

// eslint-disable-next-line unicorn/prefer-add-event-listener
window.onerror = onerror;

stub.restore();
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

Reading the test title, I would've expected the default slot to contain text here, but I don't see any text! Oh - unless it's whitespace being added here in the test?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Exactly. I'll throw some text into the slot to make that clearer.

Copy link
Collaborator Author

@clintcs clintcs Jan 22, 2025

Choose a reason for hiding this comment

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

  • Throw some text into the slot.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

f3be546

slotted?: (typeof Element | typeof Text)[] | null,
isOptional?: boolean,
) {
return noChange;
Copy link
Collaborator

Choose a reason for hiding this comment

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

TIL - noChange

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Though only useful with custom directives as far as I can tell.

? `Expected the "${part.element.name}" slot of ${host.constructor.name} to have a slotted element that extends ${slotted
.map(({ name }) => name)
.join(' or ')}.`
: `Expected ${host.constructor.name} to have a slotted element that extends ${slotted
Copy link
Collaborator

Choose a reason for hiding this comment

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

I was wondering how host.constructor.name's will appear for consumers, but looking at dist/ I think we'll be okay?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep. Should appear the same as it does now: GlideCoreCheckbox. We skip mangling class names in Terser for this reason.

Just saw the comment in that file referencing Ow. I'll tweak it before merging.

Copy link
Collaborator Author

@clintcs clintcs Jan 22, 2025

Choose a reason for hiding this comment

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

  • Tweak comment

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

f3be546

@clintcs
Copy link
Collaborator Author

clintcs commented Jan 22, 2025

I like the directive, because it moves the assertions closer to the markup. When a developer is looking at a slot, they can see we assert that slot for certain types of content.

Contrast that with what we have prior to this PR - there's a bit more scrolling around and diving in to understand (checking firstUpdated, among other places).

100%. I would have presented it to the group first for comment. But it struck me as an obvious and significant improvement.

From what I could tell, there shouldn't be any issue, but I'll ask anyway - any concerns from your end around async slotted content coming in like we had in the past?

Great question. No issue. Both Menu Options and Dropdown allow asynchronous slotted content by allowing Text. Though that was the case with Ow as well. Both also have a related test.


As with Ow, these components do assume that consumers are rendering their asynchronous content via a whitespace-producing template—like so:

<glide-core-dropdown>
  ${repeat(
    this.options,
    () => html`<glide-core-dropdown-option></glide-core-dropdown-option>`,
  )}
</glide-core-dropdown>

They make that assumption because we'd otherwise be forced to make the slots optional, and then we could only assert against the type of slotted content and not the existence thereof.

For example, if we allowed a consumer to render asynchronous content programmatically:

import {html, render} from 'lit-html';

const dropdown = document.querySelector('glide-core-dropdown');
const options = html`<glide-core-dropdown-option></glide-core-dropdown-option>;

render(options, dropdown);
<glide-core-dropdown></glide-core-dropdown>

There'd be no slotted whitespace and we'd have to make the slots optional by passing a second argument to assertSlot(). So we'd no longer be able to warn consumers if nothing is slotted.

We'll presumably have to make the slots optional eventually. But, when I initially added slot assertions via Ow, I thought it best to warn consumers of missing slotted content until a use case for programmatic slotted content presented itself.

@clintcs
Copy link
Collaborator Author

clintcs commented Jan 22, 2025

Pushed an amended commit resolving the merge conflicts from #617.

@clintcs
Copy link
Collaborator Author

clintcs commented Jan 22, 2025

Pushed another amended commit resolving conflicts from #619.

Copy link
Collaborator

@danwenzel danwenzel left a comment

Choose a reason for hiding this comment

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

This is great!

@clintcs
Copy link
Collaborator Author

clintcs commented Jan 22, 2025

This is great!

Thanks for taking a look!

@clintcs clintcs merged commit ca3dc13 into main Jan 22, 2025
7 checks passed
@clintcs clintcs deleted the remove-ow branch January 22, 2025 19:35
@github-actions github-actions bot mentioned this pull request Jan 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants