Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
8ecbddb
Move Element to use new class mixins
LeaVerou Oct 14, 2025
f14aca3
Prettier + fix typos + fix JSDocs
DmitrySharabin Oct 14, 2025
6d3a93e
Fix issues with components initialization (#54)
DmitrySharabin Oct 15, 2025
024455f
Do not duplicate init logic
LeaVerou Oct 16, 2025
66d8819
Don't be thrown by inheritance for init logic
LeaVerou Oct 16, 2025
ab67045
DRY-fy mixins
LeaVerou Oct 16, 2025
2124b77
Merge branch 'main' into class-mixins
DmitrySharabin Oct 17, 2025
415aa2c
Add MD file with mixins and hooks
DmitrySharabin Oct 20, 2025
354ccd2
Update mixins-and-hooks.md
LeaVerou Oct 21, 2025
5c0190e
Update mixins-and-hooks.md
LeaVerou Oct 21, 2025
4f5afed
Update mixins-and-hooks.md
DmitrySharabin Oct 21, 2025
e42cce2
Rename `mixinsApplied` to `mixinsActive`
LeaVerou Oct 21, 2025
79bed60
Update compose-functions.js
LeaVerou Oct 21, 2025
6594b01
Merge branch 'class-mixins' of https://github.com/nudeui/element into…
LeaVerou Oct 21, 2025
5738943
Merge branch 'main' into class-mixins
LeaVerou Oct 21, 2025
a7bdcec
`copyProperties()` improvements
LeaVerou Oct 21, 2025
5c24193
More experiments
LeaVerou Oct 22, 2025
cf15fbb
Fix composed functions bug
LeaVerou Oct 22, 2025
39de478
Adopt CSS improvements
LeaVerou Oct 29, 2025
4a5cd25
Address @DmitrySharabin's adoptCSS feedback
LeaVerou Oct 29, 2025
a61f099
Address @DmitrySharabin feedback
LeaVerou Oct 29, 2025
b844b0c
Try class mixins again
LeaVerou Oct 29, 2025
e1184aa
Add missed `async`
DmitrySharabin Oct 29, 2025
32af2e3
Some fixes
DmitrySharabin Oct 29, 2025
aa50d79
Simplify imports
DmitrySharabin Oct 29, 2025
4b5c599
Address feedback
DmitrySharabin Oct 29, 2025
76a62cb
Apply another fix
DmitrySharabin Oct 29, 2025
2a8a202
Remove leftover `defer`
LeaVerou Oct 30, 2025
2576240
Fix bug
LeaVerou Oct 30, 2025
119cbe3
Remove pointless `async`
LeaVerou Oct 30, 2025
47eeb55
Convert static `init` to use symbol
LeaVerou Oct 30, 2025
b0b0824
Move common mixins to separate file
LeaVerou Oct 30, 2025
522ed39
Add constructors so that mixins as subclass factories work properly
LeaVerou Oct 30, 2025
fc1ab75
Address @DmitrySharabin's feedback
LeaVerou Oct 30, 2025
18193c0
Attempt to rename `Element.js` to `element.js`
LeaVerou Oct 30, 2025
09325a4
Address @DmitrySharabin's feedback
LeaVerou Oct 30, 2025
51534c7
Update README.md
LeaVerou Oct 30, 2025
8ebf334
Update README.md
LeaVerou Oct 30, 2025
9d64d49
Update slots.js
LeaVerou Oct 30, 2025
159832a
Address feedback from @DmitrySharabin
LeaVerou Oct 30, 2025
036b361
Rename `get-supers.js` to `super.js`
LeaVerou Oct 31, 2025
cc06666
Rename `getSupers()` to `getSuperclasses()` and make it not return th…
LeaVerou Oct 31, 2025
3e71e10
Add `getSuper()` helper
LeaVerou Oct 31, 2025
c4c7e14
Fixes for `class-mixins`. Second take (#62)
DmitrySharabin Oct 31, 2025
ad53619
Add missed class to `supers`
DmitrySharabin Nov 3, 2025
1c4230d
Another iteration (#63)
DmitrySharabin Nov 3, 2025
50d54bf
Use symbols to simplify
LeaVerou Nov 22, 2025
e283885
Refactor class extension logic
LeaVerou Nov 22, 2025
de80a61
Allow adding mixins multiple times
LeaVerou Nov 22, 2025
a14a865
Add support for skipped properties in extendObject
LeaVerou Nov 22, 2025
f7f8e95
Support calling `applyMixin()` multiple times
LeaVerou Nov 22, 2025
e1cbe45
Rename `mixinsActive` to `mixinsApplied`
LeaVerou Nov 22, 2025
a6a1912
Refactor mixin application to use symbol for mixinsApplied
LeaVerou Nov 22, 2025
63b906d
Refactor mixin satisfaction logic and exports
LeaVerou Nov 22, 2025
4b10e18
`getSymbols` → `newSymbols`
LeaVerou Nov 22, 2025
56e3360
Move `attachInternals()` intercept to separate util function
LeaVerou Nov 22, 2025
6bdfd47
Move mixins to `mixins/` to separate from supporting code
LeaVerou Nov 22, 2025
2f4d667
Add onApply symbol for mixin initialization hooks
LeaVerou Nov 22, 2025
4a775d2
Fix role bugs
LeaVerou Nov 22, 2025
c6100a4
Refactor style mixins imports and remove unused files
LeaVerou Nov 22, 2025
cbd7803
Refactor global styles mixin and fetchCSS logic
LeaVerou Nov 23, 2025
c237718
Fix issues identified by @DmitrySharabin in #65
LeaVerou Nov 24, 2025
d09403a
Move leftover mixins to `/mixins`
LeaVerou Nov 24, 2025
3cc13c4
Update with-hooks.js
LeaVerou Nov 24, 2025
9b03c4c
Skip if equal values
LeaVerou Nov 24, 2025
6ab62e6
Update src/common-mixins.js
LeaVerou Nov 24, 2025
1cd7469
Update extend-class.js
LeaVerou Nov 24, 2025
29215b8
Merge branch 'class-mixins' of https://github.com/nudeui/element into…
LeaVerou Nov 24, 2025
4fa0773
Update src/mixins/form-associated.js
LeaVerou Nov 24, 2025
9782637
Update src/mixins/styles/global.js
LeaVerou Nov 24, 2025
12ef062
Update form-associated.js
LeaVerou Nov 24, 2025
2d48c34
Merge branch 'class-mixins' of https://github.com/nudeui/element into…
LeaVerou Nov 24, 2025
5ec9269
Move `ConflictPolicy` to separate file
LeaVerou Nov 24, 2025
233f8fa
First stab at supporting a separate conflictPolicy per mixin
LeaVerou Nov 24, 2025
c491956
Update extend-object.js
LeaVerou Nov 24, 2025
8106417
`extend()` → `composeFunction()`
LeaVerou Nov 24, 2025
4bf9de2
Update conflict-policy.js
LeaVerou Nov 24, 2025
c936628
Refactor ConflictPolicy to improve strategy handling
LeaVerou Nov 24, 2025
63274cf
Refactor getSuper to always return superclass prototype
LeaVerou Nov 24, 2025
e96e9fa
Always define mixin lifecycle hooks on class itself
LeaVerou Nov 24, 2025
e8d826b
Improve conflict policy handling in class extension
LeaVerou Nov 24, 2025
af7a105
Merge arrays and objects too
LeaVerou Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 67 additions & 107 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ for creating reactive web components that behave just like native HTML elements.

</header>

Elements can extend `NudeElement` to get the nicest, most declarative syntax,
or import individual mixins as helper functions and use them with any `HTMLElement` subclass.
Elements can extend `Element` to get the nicest, most declarative syntax,
or import individual mixins and use them with any `HTMLElement` subclass.

**Note:** This is a work in progress, developed in the open.
Try it and please report issues and provide feedback!
> [!NOTE]
> This is a work in progress, developed in the open.
> Try it and please report issues and provide feedback!

## Features

Expand All @@ -24,18 +25,20 @@ Try it and please report issues and provide feedback!

## Usage

### No hassle, less control: the `NudeElement` class
### No hassle, less control: the `Element` class

Defining your element as a subclass of `NudeElement` gives you the nicest, most declarative syntax.
Defining your element as a subclass of `Element` gives you the nicest, most declarative syntax.
This includes all commonly used mixins automatically, though they are only activated when their relevant properties are used in your element subclass.

```js
import NudeElement from "nude-element";
import Element from "nude-element";

class MySlider extends NudeElement {
class MySlider extends Element {
constructor () {
// ...
}

// Automatically activates the props mixin
static props = {
min: {
type: Number,
Expand Down Expand Up @@ -67,6 +70,7 @@ class MySlider extends NudeElement {
},
};

// Automatically activates the events mixin
static events = {
// Propagate event from shadow DOM element
change: {
Expand All @@ -81,6 +85,7 @@ class MySlider extends NudeElement {
},
};

// Automatically activates the formAssociated mixin
static formAssociated = {
like: el => el._el.slider,
role: "slider",
Expand All @@ -90,126 +95,81 @@ class MySlider extends NudeElement {
}
```

### More hassle, more control: Composable mixins
### A little hassle, a little more control: The `NudeElement` class

If Nude Element taking over your parent class seems too intrusive,
you can implement the same API via one-off composable helper functions aka mixins,
at the cost of handling some of the plumbing yourself.
`Element` inherits from `NudeElement`, which is nearly identical with one exception:
Instead of including all commonly used mixins automatically,
it includes no mixins at all.
To add mixins, you extend it and add a `mixins` static property.

Each mixin modifies the base class in a certain way (e.g. adds properties & methods) and returns an init function,
to be called once for each element,
either at the end of its constructor or when it’s first connected.
This is what the example above would look like:
This can be useful when you’re trying to keep bundle size to a minimum, since even if mixins are only activated when your subclass uses them,
they will won't be tree-shaken away, since bundlers don’t understand how this works.

```js
import {
defineProps,
defineEvents,
defineFormAssociated,
} from "nude-element";
import { NudeElement, Props, Events, FormAssociated } from "nude-element";

class MySlider extends HTMLElement {
constructor () {
// ...
class MySlider extends NudeElement {
static mixins = [Props, Events, FormAssociated];

eventHooks.init.call(this);
formAssociatedHooks.init.call(this);
propHooks.init.call(this);
}
// ...
}
```

let propHooks = defineProps(MySlider, {
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 1,
},
step: {
type: Number,
default () {
return Math.abs((this.max - this.min) / 100);
},
},
defaultValue: {
type: Number,
default () {
return (this.min + this.max) / 2;
},
reflect: {
from: "value",
},
},
value: {
type: Number,
defaultProp: "defaultValue",
reflect: false,
},
});

let eventHooks = defineEvents(MySlider, {
// Propagate event from shadow DOM element
change: {
from () {
return this._el.slider;
}
},

// Fire event when specific prop changes (even programmatically)
valuechange: {
propchange: "value",
},
});

let formAssociatedHooks = defineFormAssociated(MySlider, {
like: el => el._el.slider,
role: "slider",
valueProp: "value",
changeEvent: "valuechange",
});
### With custom base class: Subclass factories

If you need to use a custom base class (e.g. `LitElement`), rather than `HTMLElement`, all Nude mixins are also available as subclass factories,
include `Element` and `NudeElement`:

```js
import { ElementMixin } from "nude-element";
import { LitElement } from "lit";

class MySlider extends ElementMixin(LitElement) {
// ...
}
```

Each mixin will also look for a static `hooks` property on the element class and add its lifecycle hooks to it if it exists,
so you can make things a little easier by defining such a property:
Individual mixins are also available as subclass factories:

```js
import { defineProps } from "nude-element";
import Hooks from "nude-element/hooks";
import { Props, Events, FormAssociated } from "nude-element/mixins";
import { LitElement } from "lit";

class MyElement extends HTMLElement {
// Caution: if MyElement has subclasses, this will be shared among them!
static hooks = new Hooks();
class MySlider extends Props(Events(FormAssociated(LitElement))) {
// ...
}
```

### More hassle, more control: Composable mixins

If Nude Element taking over your parent class seems too intrusive,
you can pull in mixins and apply them to any base class you want in-place without affecting the inheritance chain,
at the cost of handling some of the plumbing yourself.

There are three parts:
1. Apply `applyMixins` to your class to apply the mixins to it
2. Make sure to call `this.init()` in your constructor, since `applyMixins` cannot modify your constructor, so that’s the only way to run initialization logic

```js
import { Props, Events, FormAssociated, applyMixins } from "nude-element";

class MySlider extends HTMLElement {
constructor () {
super();

// Then you can call the hooks at the appropriate times:
this.constructor.hooks.run("init", this);
// Your own init logic here...

this.init?.();
}
}

defineProps(MyElement, {
// Props…
});
static {
applyMixins(this, [Props, Events, FormAssociated]);
}
}
```

Read more:
Individual mixin docs:
- [Using Props](src/props/)
- [Events](src/events/)
- [Form-associated elements](src/formAssociated/)
- [Mixins](src/mixins/)


## Known Hooks

These hooks are automatically managed when you use the `NudeElement` class.
If you choose to import mixins directly, you need to manage when to call them yourself.
- [Slots](src/slots/)

- `prepare`: Runs once per class, as soon as a mixin is added
- `setup`: Runs once per class, before any element is fully constructed
- `start`: Runs on element constructor
- `constructed`: Runs after element constructor (async)
- `init`: Runs when element is connected for the first time
- `disconnected`: Runs when element is disconnected
13 changes: 13 additions & 0 deletions mixins-and-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
| Mixin | Hooks uses | Needs `super`? | Imports mixins |
|-------|-------------|-----------------|-----------------|
| mounted | 1) First `connectedCallback` for class; 2) First `connectedCallback` | | |
| shadowStyles | 1) First `connectedCallback` for class; 2) First `connectedCallback` | ✅ (for now, fakes it with `getSupers()`) | `mounted` |
| globalStyles | 1) First `connectedCallback` for class; 2) First `connectedCallback` | ✅ (for now, fakes it with `getSupers()`) | `mounted` |
| formAssociated | First `connectedCallback` | | `mounted` |
| defineProps | First `connectedCallback` | | |
| defineEvents | 1) First `constructor` (once per class); 2) First `connectedCallback` | | `defineProps` |
| defineSlots | First `connectedCallback` | | |
| `has-slotted` | First `connectedCallback` | | |
| named-manual | First `connectedCallback` | | |
| slots | 1) First `constructor` (once per class); 2) First `connectedCallback` | | |
| states | First `connectedCallback` | | `mounted` |
86 changes: 0 additions & 86 deletions src/Element.js

This file was deleted.

Loading