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

V2 #239

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open

V2 #239

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
27b53f0
remove controller or component suffix from registered elements
keithamus May 2, 2022
1006b8c
improve convention node on controller suffixes
keithamus May 3, 2022
dbda5d0
improve your-first-component docs around suffixes
keithamus May 3, 2022
5ee8fd5
add exact example for tag name in your-first-component
keithamus May 3, 2022
b76ac7d
drop the the
keithamus May 3, 2022
ec70eda
remove autoshadowroot
keithamus May 4, 2022
52d3d40
ensure register tests extend HTMLElement
keithamus May 16, 2022
b76529a
refactor attr into attrable
keithamus May 24, 2022
af53b8f
refactor bind into actionable
keithamus May 24, 2022
67f4341
rename parse to parseActionAttribute
keithamus May 24, 2022
d142b92
bump size to 2.6kb
keithamus May 24, 2022
21c81fb
add new attr docs
keithamus May 24, 2022
bb32c3b
refactor target into targetable
keithamus May 25, 2022
d79fc59
use targetable in controller
keithamus May 26, 2022
b7fa0ae
fix register type
keithamus May 26, 2022
5f60201
drop size limit to 2.5kb
keithamus May 26, 2022
ab99a16
improve @target docs around not using decorators
keithamus May 26, 2022
bdb1240
refine docs around your first component and how controller works
keithamus May 26, 2022
01348fa
add composition tests for providable
keithamus May 26, 2022
dfbedf6
export all v2 functions
keithamus May 26, 2022
1409b84
Update attrs.md
drwsmth Aug 25, 2022
d6c8d14
Add deprecatedDataPrefixedAttrs decorator
keithamus Sep 7, 2022
2a0fef3
3kb!
keithamus Sep 7, 2022
5c7c6b0
use shadowroot.host instead of weakmap
keithamus Sep 29, 2022
ac03dab
revert changes to v1 docs
keithamus Oct 12, 2022
a874af9
Add tests to validate that properties update synchronously
koddsson Oct 24, 2022
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
{
"path": "lib/index.js",
"import": "{controller, attr, target, targets}",
"limit": "2.5kb"
"limit": "3.0kb"
},
{
"path": "lib/abilities.js",
Expand Down
58 changes: 58 additions & 0 deletions src/actionable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type {CustomElementClass, CustomElement} from './custom-element.js'
import type {ControllableClass} from './controllable.js'
import {registerTag, observeElementForTags, parseElementTags} from './tag-observer.js'
import {controllable, attachShadowCallback} from './controllable.js'
import {createAbility} from './ability.js'

const parseActionAttribute = (tag: string): [tagName: string, event: string, method: string] => {
const eventSep = tag.lastIndexOf(':')
const methodSep = Math.max(0, tag.lastIndexOf('#')) || tag.length
return [tag.slice(eventSep + 1, methodSep), tag.slice(0, eventSep), tag.slice(methodSep + 1) || 'handleEvent']
}
registerTag(
'data-action',
parseActionAttribute,
(el: Element, controller: Element | ShadowRoot, tag: string, event: string) => {
el.addEventListener(event, handleEvent)
}
)

const actionables = new WeakSet<CustomElement>()
// Bind a single function to all events to avoid anonymous closure performance penalty.
function handleEvent(event: Event) {
const el = event.currentTarget as Element
for (const [tag, type, method] of parseElementTags(el, 'data-action', parseActionAttribute)) {
if (event.type === type) {
type EventDispatcher = CustomElement & Record<string, (ev: Event) => unknown>
const controller = el.closest<EventDispatcher>(tag)!
if (actionables.has(controller) && typeof controller[method] === 'function') {
controller[method](event)
}
const root = el.getRootNode()
if (root instanceof ShadowRoot) {
const shadowController = root.host as EventDispatcher
if (shadowController.matches(tag) && actionables.has(shadowController)) {
if (typeof shadowController[method] === 'function') {
shadowController[method](event)
}
}
}
}
}
}

export const actionable = createAbility(
<T extends CustomElementClass>(Class: T): T & ControllableClass =>
class extends controllable(Class) {
constructor() {
super()
actionables.add(this)
observeElementForTags(this)
}

[attachShadowCallback](root: ShadowRoot) {
super[attachShadowCallback]?.(root)
observeElementForTags(root)
}
}
)
100 changes: 0 additions & 100 deletions src/attr.ts

This file was deleted.

130 changes: 130 additions & 0 deletions src/attrable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type {CustomElementClass} from './custom-element.js'
import type {ControllableClass} from './controllable.js'
import {controllable} from './controllable.js'
import {dasherize, mustDasherize} from './dasherize.js'
import {createMark} from './mark.js'
import {createAbility} from './ability.js'

const attrChangedCallback = Symbol()
const serializeAttributeName = Symbol()

export interface Attrable {
[key: PropertyKey]: unknown
[serializeAttributeName](name: PropertyKey): string
[attrChangedCallback](changed: Map<PropertyKey, unknown>): void
}

export interface AttrableClass {
new (): Attrable
}

export const deprecatedDataPrefixedAttrs = createAbility(
<T extends CustomElementClass>(Class: T): T =>
class extends controllable(Class) {
[serializeAttributeName](name: PropertyKey) {
return `data-${dasherize(name)}`
}
}
)

const Identity = (v: unknown) => v
let setFromMutation = false
const attrs = new WeakMap<Element, Map<string, PropertyKey>>()

const handleMutations = (mutations: MutationRecord[]) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes') {
const name = mutation.attributeName!
const el = mutation.target as Element & {[key: PropertyKey]: unknown}
const key = attrs.get(el)?.get(name)
if (key) {
setFromMutation = true
el[key] = el.getAttribute(name)
setFromMutation = false
}
}
}
}
const observer = new MutationObserver(handleMutations)

const [attr, getAttr, initializeAttrs] = createMark<Element & Attrable>(
({name}) => mustDasherize(name, '@attr'),
(instance: Element & Attrable, {name, kind, access}) => {
let cast: typeof Identity | typeof Boolean | typeof Number | typeof String = Identity
let initialValue: unknown
if (access.get) {
initialValue = access.get.call(instance)
} else if ('value' in access && kind !== 'method') {
initialValue = access.value
}
let value = initialValue
const attributeName = instance[serializeAttributeName](name)
const setCallback = (kind === 'method' ? access.value : access.set) || Identity
const getCallback = access.get || (() => value)
if (!attrs.get(instance)) attrs.set(instance, new Map())
attrs.get(instance)!.set(attributeName, name)
if (typeof value === 'number') {
cast = Number
} else if (typeof value === 'boolean') {
cast = Boolean
} else if (typeof value === 'string') {
cast = String
}
const queue = new Map()
const requestAttrChanged = async (newValue: unknown) => {
queue.set(name, newValue)
if (queue.size > 1) return
await Promise.resolve()
const changed = new Map(queue)
queue.clear()
instance[attrChangedCallback](changed)
}
return {
get() {
const has = instance.hasAttribute(attributeName)
if (has) {
return cast === Boolean ? has : cast(instance.getAttribute(attributeName))
}
return cast(getCallback.call(instance))
},
set(newValue: unknown) {
const isInitial = newValue === null
if (isInitial) newValue = initialValue
const same = Object.is(value, newValue)
value = newValue
setCallback.call(instance, value)
if (setFromMutation || same || isInitial) return
requestAttrChanged(newValue)
}
}
}
)

export {attr, getAttr, attrChangedCallback}
export const attrable = createAbility(
<T extends CustomElementClass>(Class: T): T & ControllableClass & AttrableClass =>
class extends controllable(Class) {
[key: PropertyKey]: unknown
constructor() {
super()
initializeAttrs(this)
const attributeFilter = Array.from(getAttr(this)).map(name => this[serializeAttributeName](name))
observer.observe(this, {attributeFilter})
}

[serializeAttributeName](name: PropertyKey) {
return dasherize(name)
}

[attrChangedCallback](changed: Map<PropertyKey, unknown>) {
if (!this.isConnected) return
for (const [name, value] of changed) {
if (typeof value === 'boolean') {
this.toggleAttribute(this[serializeAttributeName](name), value)
} else {
this.setAttribute(this[serializeAttributeName](name), String(value))
}
}
}
}
)
11 changes: 0 additions & 11 deletions src/auto-shadow-root.ts

This file was deleted.

Loading