Created
January 16, 2025 21:38
-
-
Save rodydavis/c311c5ff959f6f9509ad062c6601a7f5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { computed, signal } from "@preact/signals-core"; | |
import { html } from "@lit-labs/preact-signals"; | |
import { LitTemplateMixin } from "./mixins.js"; | |
import { mix } from "./mixwith.js"; | |
import { styleMap } from "lit/directives/style-map.js"; | |
class Example extends mix(HTMLElement).with(LitTemplateMixin) { | |
tags = signal<string[]>([ | |
"Docker", | |
"Kubernetes", | |
"AWS", | |
"GraphQL", | |
"MongoDB", | |
"PostgreSQL", | |
"Redis", | |
"Git", | |
"Webpack", | |
"Vite", | |
"Cypress", | |
"Storybook", | |
"Tailwind", | |
"Prisma", | |
"Nginx", | |
]); | |
builder = computed( | |
() => html` | |
<style> | |
::view-transition-group(*) { | |
animation-duration: 0.4s; | |
} | |
* { | |
box-sizing: border-box; | |
} | |
.search { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 12px; | |
width: 400px; | |
min-height: 48px; | |
padding: 12px; | |
border: 1px solid #e4e4e7; | |
border-radius: 12px; | |
font-size: 16px; | |
margin-bottom: 24px; | |
button { | |
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, | |
rgba(0, 0, 0, 0) 0px 0px 0px 0px, | |
rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, | |
rgba(0, 0, 0, 0.1) 0px 2px 4px -2px; | |
background-color: #ffffff; | |
border: 1px solid #e4e4e7; | |
order: 0 !important; | |
} | |
button span { | |
display: inline-block; | |
} | |
} | |
.tags { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 12px; | |
padding: 12px; | |
background: white; | |
border-radius: 16px; | |
border: 1px solid #e4e4e7; | |
width: 400px; | |
} | |
button { | |
padding: 8px; | |
background: #f4f4f5; | |
border-radius: 8px; | |
font-size: 16px; | |
color: #27272a; | |
cursor: pointer; | |
border: none; | |
font-weight: 500; | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
span { | |
display: none; | |
} | |
} | |
</style> | |
<h1>TAGS</h1> | |
<div class="search"></div> | |
<div class="tags"> | |
${this.tags.value.map( | |
(e, i) => html` <button | |
style=${styleMap({ | |
"view-transition-name": `tag-${i}`, | |
order: i, | |
})} | |
> | |
${e}<span>X</span> | |
</button>` | |
)} | |
</div> | |
` | |
); | |
transition(cb: () => void) { | |
if ("startViewTransition" in document) { | |
// @ts-ignore | |
document.startViewTransition(() => { | |
cb(); | |
}); | |
} else { | |
cb(); | |
} | |
} | |
connectedCallback(): void { | |
super.connectedCallback(); | |
const root = this.getRoot(); | |
const search = root.querySelector(".search")!; | |
const tagsContainer = document.querySelector(".tags")!; | |
tagsContainer.addEventListener("click", (e) => { | |
const target = e.target as HTMLElement; | |
const tag = target.closest("button"); | |
if (tag) { | |
this.transition(() => { | |
search.appendChild(tag); | |
}); | |
} | |
}); | |
search.addEventListener("click", (e) => { | |
const target = e.target as HTMLElement; | |
const span = target.closest("span"); | |
if (span) { | |
const tag = span.closest("button"); | |
if (tag) { | |
this.transition(() => { | |
tagsContainer.appendChild(tag); | |
}); | |
} | |
} | |
}); | |
} | |
} | |
export default Example; | |
customElements.define("x-example", Example); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<script type="module" src="./example.js"></script> | |
</head> | |
<body> | |
<x-example></x-example> | |
</body> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
ReadonlySignal, | |
Signal, | |
computed, | |
effect, | |
signal, | |
} from "@preact/signals-core"; | |
import { CSSResult, render, TemplateResult, unsafeCSS, html } from "lit"; | |
import { Constructable, Mixin } from "./mixwith.js"; | |
export { type TemplateResult, css, CSSResult } from "lit"; | |
export { html } from "@lit-labs/preact-signals"; | |
export { | |
type ReadonlySignal, | |
Signal, | |
signal, | |
computed, | |
effect, | |
batch, | |
untracked, | |
} from "@preact/signals-core"; | |
export type Style = string | CSSResult | CSSStyleSheet; | |
export const CleanupMixin = (superclass: Constructable<HTMLElement>) => | |
class extends superclass { | |
constructor(...args: any[]) { | |
super(...args); | |
} | |
cleanup: (() => void)[] = []; | |
initCleanup() { | |
this.cleanup.forEach((cleanup) => cleanup()); | |
} | |
disconnectedCallback() { | |
this.initCleanup(); | |
} | |
}; | |
export const ReactiveAttributesMixin = Mixin( | |
(superclass: Constructable<HTMLElement>) => | |
class extends CleanupMixin(superclass) { | |
constructor(...args: any[]) { | |
super(...args); | |
} | |
attrs = new Map<string, Signal>(); | |
attr(key: string, fallback?: string, reflect?: boolean): Signal<string> { | |
const val = this.getAttribute(key); | |
const s = signal(val ?? fallback ?? ""); | |
this.attrs.set(key, s); | |
if (reflect === true) { | |
this.cleanup.push( | |
effect(() => { | |
this.setAttribute(key, s.value); | |
}) | |
); | |
} | |
return s; | |
} | |
attributeChangedCallback( | |
_name: string, | |
_olvValue: string, | |
_newValue: string | |
) { | |
if (this.attrs.has(_name)) { | |
const s = this.attrs.get(_name)!; | |
if (s.value !== _newValue) s.value = _newValue; | |
} | |
} | |
} | |
); | |
export const FormInternalsMixin = (superclass: Constructable<HTMLElement>) => | |
class extends superclass { | |
static formAssociated = true; | |
constructor(...args: any[]) { | |
super(...args); | |
// @ts-ignore | |
this.internals_ = this.attachInternals(); | |
} | |
}; | |
export const LitTemplateMixin = Mixin( | |
(superclass: Constructable<HTMLElement>) => | |
class extends ReactiveAttributesMixin(superclass) { | |
builder: ReadonlySignal<TemplateResult> = signal(html``); | |
getRoot(): HTMLElement | ShadowRoot { | |
return this; | |
} | |
connectedCallback() { | |
// @ts-ignore | |
if (super.connectedCallback) super.connectedCallback(); | |
this.cleanup.push( | |
effect(() => { | |
render(this.builder.value, this.getRoot()); | |
}) | |
); | |
} | |
} | |
); | |
export const ShadowRootMixin = Mixin( | |
(superclass: Constructable<HTMLElement>) => | |
class extends CleanupMixin(superclass) { | |
styles: ReadonlySignal<Style | Array<Style>> = computed(() => []); | |
sheets = computed(() => { | |
const value = this.styles.value; | |
const styles: CSSStyleSheet[] = []; | |
const array = Array.isArray(value) ? value : [value]; | |
for (const style of array) { | |
if (style instanceof CSSStyleSheet) { | |
styles.push(style); | |
} else { | |
const result = | |
typeof style == "string" // | |
? unsafeCSS(style) | |
: (style as CSSResult); | |
const sheet = result.styleSheet; | |
if (sheet) { | |
styles.push(sheet); | |
} else { | |
const sheet = new CSSStyleSheet(); | |
sheet.replaceSync(result.cssText); | |
styles.push(sheet); | |
} | |
} | |
} | |
return styles; | |
}); | |
getRoot(): ShadowRoot { | |
let root = this.shadowRoot; | |
if (!root) { | |
root = this.attachShadow({ mode: "open" }); | |
} | |
return root; | |
} | |
connectedCallback() { | |
// @ts-ignore | |
if (super.connectedCallback) super.connectedCallback(); | |
this.cleanup.push( | |
effect(() => { | |
const root = this.getRoot(); | |
root.adoptedStyleSheets = this.sheets.value; | |
}) | |
); | |
} | |
} | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// deno-lint-ignore-file no-explicit-any ban-types | |
"use strict"; | |
// @source https://github.com/justinweinberg/mixwith.ts | |
export type Constructable<T = {}> = new (...args: any[]) => T; | |
export type mixin<C extends Constructable, T> = (args: C) => T; | |
// used by apply() and isApplicationOf() | |
const _appliedMixin = "__mixwith_appliedMixin"; | |
/** | |
* Applies `mixin` to `superclass`. | |
* | |
* `apply` stores a reference from the mixin application to the unwrapped mixin | |
* to make `isApplicationOf` and `hasMixin` work. | |
* | |
* This function is useful for mixin wrappers that want to automatically enable | |
* {@link hasMixin} support. | |
* | |
* @typeParam C - The constructor type of the superclass. | |
* @typeParam T - The resulting type of the mixin. | |
* | |
* @param {C} superclass - The superclass to which the mixin will be applied. | |
* @param {mixin<C, T>} mixin - The mixin function that provides additional behavior to the superclass. | |
* | |
* @returns {T} - A new class with the mixin's behavior applied. | |
*/ | |
export const apply = <C extends Constructable, T>( | |
superclass: C, | |
mixin: mixin<C, T> | |
): T => { | |
const application = mixin(superclass); | |
(application as any).prototype[_appliedMixin] = unwrap(mixin); | |
return application; | |
}; | |
/** | |
* Returns `true` iff `proto` is a prototype created by the application of | |
* `mixin` to a superclass. | |
* | |
* `isApplicationOf` works by checking that `proto` has a reference to `mixin` | |
* as created by `apply`. | |
* | |
* @typeParam T - The type of the mixin. | |
* @param {object} proto A prototype object created by {@link apply}. | |
* @param {T} mixin A mixin function used with {@link apply}. | |
* @return {boolean} whether `proto` is a prototype created by the application of | |
* `mixin` to a superclass | |
*/ | |
export const isApplicationOf = <T>(proto: object, mixin: T) => | |
Object.hasOwn(proto, _appliedMixin) && | |
(proto as any)[_appliedMixin] === unwrap(mixin); | |
/** | |
* Checks if the provided mixin has been applied to the given prototype object. | |
* | |
* @typeParam T - The type of the mixin. | |
* @param {object} proto - The prototype object to check. | |
* @param {T} mixin A mixin function used with {@link apply}. | |
* @returns {boolean} - Returns true if the mixin has been applied, otherwise false. | |
*/ | |
export const hasMixin = <T>(o: object, mixin: T): boolean => { | |
while (o != null) { | |
if (isApplicationOf(o, mixin)) return true; | |
o = Object.getPrototypeOf(o); | |
} | |
return false; | |
}; | |
// used by wrap() and unwrap() | |
const _wrappedMixin = "__mixwith_wrappedMixin"; | |
/** | |
* Sets up the function `mixin` to be wrapped by the function `wrapper`, while | |
* allowing properties on `mixin` to be available via `wrapper`, and allowing | |
* `wrapper` to be unwrapped to get to the original function. | |
* | |
* `wrap` does two things: | |
* 1. Sets the prototype of `mixin` to `wrapper` so that properties set on | |
* `mixin` inherited by `wrapper`. | |
* 2. Sets a special property on `mixin` that points back to `mixin` so that | |
* it can be retreived from `wrapper` | |
* | |
* @function | |
* @typeParam C - The type of the constructor of the mixin. | |
* @typeParam T - The type of the mixin instance. | |
* @param {mixin<C, T>} mixin - The mixin to be wrapped. | |
* @param {mixin<C, T>} wrapper - The wrapper mixin. | |
* @returns {mixin<C, T>} - Returns the wrapper mixin. | |
*/ | |
export const wrap = <C extends Constructable, T>( | |
mixin: mixin<C, T>, | |
wrapper: mixin<C, T> | |
): mixin<C, T> => { | |
Object.setPrototypeOf(wrapper, mixin); | |
if (!(mixin as any)[_wrappedMixin]) { | |
(mixin as any)[_wrappedMixin] = mixin; | |
} | |
return wrapper; | |
}; | |
/** | |
* Unwraps the function `wrapper` to return the original function wrapped by | |
* one or more calls to `wrap`. Returns `wrapper` if it's not a wrapped | |
* function. | |
* | |
* @typeParam T - The type of the wrapped mixin. | |
* @param {T} wrapper - The wrapped mixin. | |
* @returns {T} - Returns the original mixin if available, otherwise the wrapper itself. | |
*/ | |
export const unwrap = <T>(wrapper: T): T => { | |
return ((wrapper as any)[_wrappedMixin] as T) || wrapper; | |
}; | |
/** | |
* Decorates `mixin` so that it only applies if it's not already on the | |
* prototype chain. | |
* | |
* @typeParam C - The constructor type representing the original superclass. | |
* @typeParam T - The return type of the mixin function. | |
* @param {mixin<C, T>} mixin - The mixin function to be deduplicated. | |
* @returns {mixin<C, T>} - A deduplicated mixin that extends the original superclass if needed. | |
*/ | |
export function DeDupe<C extends Constructable, T>( | |
mixin: mixin<C, T> | |
): mixin<C, T> { | |
const dupeWrapper = (superclass: C) => | |
hasMixin(superclass.prototype, mixin) | |
? (superclass as unknown as T) | |
: mixin(superclass); | |
return wrap(mixin, dupeWrapper as mixin<C, T>); | |
} | |
// used by HasInstance() | |
const _instancedMixin = "__mixwith_instanceOf"; | |
/** | |
* Adds a Symbol.hasInstance implementation to the provided mixin object to enable the use of the | |
* `instanceof` operator with instances of classes that include the mixin. | |
* | |
* @typeParam T - The type of the mixin object. | |
* @param {T} mixin - The mixin object to be enhanced with the Symbol.hasInstance implementation. | |
* @returns {T} - The mixin object with the Symbol.hasInstance implementation. | |
*/ | |
export const HasInstance = <T>(mixin: T) => { | |
if (!(mixin as any)[_instancedMixin]) { | |
(mixin as any)[_instancedMixin] = true; | |
Object.defineProperty(mixin, Symbol.hasInstance, { | |
value(o: object) { | |
return hasMixin(o, mixin); | |
}, | |
}); | |
} | |
return mixin; | |
}; | |
/** | |
* A basic mixin decorator that applies the mixin using the {@link apply} function so that it | |
* can be used with {@link isApplicationOf}, {@link hasMixin}, and other mixin decorator functions. | |
* | |
* @typeParam C - The constructor type representing the original superclass. | |
* @typeParam T - The return type of the mixin function. | |
* @param {mixin<C, T>} mixin - The mixin to wrap. | |
* @returns {mixin<C, T>} - A new mixin function. | |
*/ | |
export const BareMixin = <C extends Constructable, T>( | |
mixin: mixin<C, T> | |
): mixin<C, T> => wrap(mixin, (s) => apply(s, mixin)); | |
/** | |
* Decorates a mixin function to add deduplication, application caching, and instanceof support. | |
* | |
* @typeParam C - The constructor type representing the original superclass. | |
* @typeParam T - The return type of the mixin function. | |
* @param {mixin<C, T>} mixin - The mixin to wrap. | |
* @returns {mixin<C, T>} - A new mixin function. | |
*/ | |
export const Mixin = <C extends Constructable, T>( | |
mixin: mixin<C, T> | |
): mixin<C, T> => DeDupe(BareMixin(HasInstance(mixin))); | |
/** | |
* A fluent interface to apply a list of mixins to a superclass. | |
* | |
* ```typescript | |
* class X extends mix(Object).with(A, B, C) {} | |
* ``` | |
* | |
* The mixins are applied in order to the superclass, so the prototype chain | |
* will be: X->C'->B'->A'->Object. | |
* | |
* This is purely a convenience function. The above example is equivalent to: | |
* | |
* ```typescript | |
* C = Mixin(C) | |
* B = Mixin(B) | |
* A = Mixin(A) | |
* class X extends C(B(A(Object))) {} | |
* ``` | |
* | |
* @function | |
* @param {C} superclass - The superclass to which the mixin will be applied. If not defined, it defaults to `class {}`. | |
* @return {MixinBuilder<C>} - A builder object to apply mixins to the superclass. | |
*/ | |
export const mix = <C extends Constructable>(superclass?: C): MixinBuilder<C> => | |
new MixinBuilder(superclass); | |
/** | |
* MixinBuilder helper class (returned by mix()). | |
*/ | |
export class MixinBuilder<Base extends Constructable> { | |
superclass?: Base; | |
constructor(superclass?: Base) { | |
this.superclass = superclass; | |
} | |
/** | |
* Applies a chain of mixins to a base class. The method supports up to six mixins. | |
* The mixins are applied in reverse sequence (e.g. the right most mixin is applied first, etc.) | |
* @method | |
* @param {mixin<Base, A>} a - A mixin to apply. | |
* @param {mixin<A, B>} [b] - A mixin to apply (optional). | |
* @param {mixin<B, C>} [c] - A mixin to apply (optional). | |
* @param {mixin<C, D>} [d] - A mixin to apply (optional). | |
* @param {mixin<D, E>} [e] - A mixin to apply (optional). | |
* @param {mixin<E, F>} [f] - A mixin to apply (optional). | |
* | |
* @returns {Base & A & B & C & D & E & F} - A new class constructor that includes the functionalities | |
* of all mixins and the base class. | |
*/ | |
with< | |
A extends Constructable, | |
B extends Constructable, | |
C extends Constructable, | |
D extends Constructable, | |
E extends Constructable, | |
F extends Constructable | |
>( | |
a: mixin<Base, A>, | |
b?: mixin<A, B>, | |
c?: mixin<B, C>, | |
d?: mixin<C, D>, | |
e?: mixin<D, E>, | |
f?: mixin<E, F> | |
): Base & A & B & C & D & E & F { | |
this.superclass = this.superclass ?? (class {} as Base); | |
const a_m = Mixin(a); | |
const b_m = b ? Mixin(b) : undefined; | |
const c_m = c ? Mixin(c) : undefined; | |
const d_m = d ? Mixin(d) : undefined; | |
const e_m = e ? Mixin(e) : undefined; | |
const f_m = f ? Mixin(f) : undefined; | |
if (f_m && e_m && d_m && c_m && b_m && a_m) { | |
return f_m(e_m(d_m(c_m(b_m(a_m(this.superclass)))))) as Base & | |
A & | |
B & | |
C & | |
D & | |
E & | |
F; | |
} else if (e_m && d_m && c_m && b_m && a_m) { | |
return e_m(d_m(c_m(b_m(a_m(this.superclass))))) as Base & | |
A & | |
B & | |
C & | |
D & | |
E & | |
F; | |
} else if (d_m && c_m && b_m && a_m) { | |
return d_m(c_m(b_m(a_m(this.superclass)))) as Base & | |
A & | |
B & | |
C & | |
D & | |
E & | |
F; | |
} else if (c_m && b_m && a_m) { | |
return c_m(b_m(a_m(this.superclass))) as Base & A & B & C & D & E & F; | |
} else if (b_m && a_m) { | |
return b_m(a_m(this.superclass)) as Base & A & B & C & D & E & F; | |
} else { | |
return a_m(this.superclass) as Base & A & B & C & D & E & F; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"dependencies": { | |
"lit": "^3.0.0", | |
"@lit/reactive-element": "^2.0.0", | |
"lit-element": "^4.0.0", | |
"lit-html": "^3.0.0" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"files": { | |
"example.ts": { | |
"position": 0 | |
}, | |
"index.html": { | |
"position": 1 | |
}, | |
"package.json": { | |
"position": 2, | |
"hidden": true | |
}, | |
"mixwith.ts": { | |
"position": 3 | |
}, | |
"mixins.ts": { | |
"position": 4 | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment