Add key casing modifiers (#618)
* Add casing modifiers to attributes * Lower case key * Refactor text utils * cleanup ternary * fix case example * Finish --------- Co-authored-by: Delaney Gillilan <delaneygillilan@gmail.com>
This commit is contained in:
parent
4aafd4262f
commit
d97cd5f40a
|
@ -4,8 +4,10 @@
|
|||
|
||||
### Added
|
||||
|
||||
- Added a `__case` modifier to the `data-signals-*`, `data-computed-*`, `data-ref-*`, `data-indicator-*`, `data-persist-*`, `data-bind-*`, `data-class-*`, and `data-on-*` attributes, allowing you to modify the casing of the key by adding `.kebab`, `.snake` or `.pascal`.
|
||||
- Added a `retrying` event type that is dispatched when the SSE plugin is trying to reconnect ([#583](https://github.com/starfederation/datastar/issues/583)).
|
||||
|
||||
### Fixed
|
||||
### Changed
|
||||
|
||||
- Fixed a bug in which class names were being converted to kebab case when used in `data-class-*` attributes ([#610](https://github.com/starfederation/datastar/issues/610)).
|
||||
- Class names are no longer converted to kebab case when used in the `data-class-*` attribute ([#610](https://github.com/starfederation/datastar/issues/610)).
|
||||
- Event names are no longer converted to kebab case when used in the `data-on-*` attribute.
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
|||
import { Hash, elUniqId } from '../utils/dom'
|
||||
import { camelize, lcFirst } from '../utils/text'
|
||||
import { camelize } from '../utils/text'
|
||||
import { debounce } from '../utils/timing'
|
||||
import { effect } from '../vendored/preact-core'
|
||||
import { DSP, DSS } from './consts'
|
||||
|
@ -217,10 +217,10 @@ export class Engine {
|
|||
|
||||
const hasKey = key.length > 0
|
||||
if (hasKey) {
|
||||
// Keys starting with a dash are not converted to camel case in the dataset
|
||||
key = key.startsWith('-') ? key.slice(1) : lcFirst(key)
|
||||
key = camelize(key)
|
||||
}
|
||||
const value = el.dataset[camelCasedKey] || ''
|
||||
const hasValue = value.length > 0
|
||||
|
||||
// Create the runtime context
|
||||
const that = this // I hate javascript
|
||||
|
@ -251,7 +251,6 @@ export class Engine {
|
|||
}
|
||||
|
||||
const valReq = plugin.valReq || Requirement.Allowed
|
||||
const hasValue = value.length > 0
|
||||
if (hasValue) {
|
||||
if (valReq === Requirement.Denied) {
|
||||
throw runtimeErr(`${plugin.name}ValueNotAllowed`, ctx)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { kebabize } from '../utils/text'
|
||||
import { snake } from '../utils/text'
|
||||
import { DATASTAR } from './consts'
|
||||
import { type InitContext, PluginType, type RuntimeContext } from './types'
|
||||
|
||||
|
@ -12,9 +12,8 @@ interface Metadata {
|
|||
|
||||
function dserr(type: string, reason: string, metadata: Metadata = {}) {
|
||||
const e = new Error()
|
||||
reason = reason[0].toUpperCase() + reason.slice(1)
|
||||
e.name = `${DATASTAR} ${type} error`
|
||||
const r = kebabize(reason).replaceAll('-', '_')
|
||||
const r = snake(reason)
|
||||
const q = new URLSearchParams({
|
||||
metadata: JSON.stringify(metadata),
|
||||
}).toString()
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
PluginType,
|
||||
Requirement,
|
||||
} from '../../../../engine/types'
|
||||
import { trimDollarSignPrefix } from '../../../../utils/text'
|
||||
import { modifyCasing, trimDollarSignPrefix } from '../../../../utils/text'
|
||||
import {
|
||||
DATASTAR_SSE_EVENT,
|
||||
type DatastarSSEEvent,
|
||||
|
@ -20,8 +20,8 @@ export const Indicator: AttributePlugin = {
|
|||
name: 'indicator',
|
||||
keyReq: Requirement.Exclusive,
|
||||
valReq: Requirement.Exclusive,
|
||||
onLoad: ({ value, signals, el, key }) => {
|
||||
const signalName = key ? key : trimDollarSignPrefix(value)
|
||||
onLoad: ({ el, key, mods, signals, value }) => {
|
||||
const signalName = key ? modifyCasing(key, mods) : trimDollarSignPrefix(value)
|
||||
const signal = signals.upsertIfMissing(signalName, false)
|
||||
const watcher = (event: CustomEvent<DatastarSSEEvent>) => {
|
||||
const {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
type NestedValues,
|
||||
PluginType,
|
||||
} from '../../../../engine/types'
|
||||
import { trimDollarSignPrefix } from '../../../../utils/text'
|
||||
import { modifyCasing, trimDollarSignPrefix } from '../../../../utils/text'
|
||||
|
||||
const SESSION = 'session'
|
||||
|
||||
|
@ -17,10 +17,12 @@ export const Persist: AttributePlugin = {
|
|||
type: PluginType.Attribute,
|
||||
name: 'persist',
|
||||
mods: new Set([SESSION]),
|
||||
onLoad: ({ key, value, signals, effect, mods }) => {
|
||||
onLoad: ({ key, effect, mods, signals, value }) => {
|
||||
key = modifyCasing(key, mods)
|
||||
if (key === '') {
|
||||
key = DATASTAR
|
||||
}
|
||||
|
||||
const storage = mods.has(SESSION) ? sessionStorage : localStorage
|
||||
let paths = value.split(/\s+/).filter((p) => p !== '')
|
||||
paths = paths.map((p) => trimDollarSignPrefix(p))
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
PluginType,
|
||||
Requirement,
|
||||
} from '../../../../engine/types'
|
||||
import { modifyCasing } from '../../../../utils/text'
|
||||
|
||||
const name = 'computed'
|
||||
export const Computed: AttributePlugin = {
|
||||
|
@ -10,7 +11,8 @@ export const Computed: AttributePlugin = {
|
|||
name,
|
||||
keyReq: Requirement.Must,
|
||||
valReq: Requirement.Must,
|
||||
onLoad: ({ key, signals, genRX }) => {
|
||||
onLoad: ({ key, mods, signals, genRX }) => {
|
||||
key = modifyCasing(key, mods)
|
||||
const rx = genRX()
|
||||
signals.setComputed(key, rx)
|
||||
},
|
||||
|
|
|
@ -3,18 +3,20 @@ import {
|
|||
type NestedValues,
|
||||
PluginType,
|
||||
} from '../../../../engine/types'
|
||||
import { jsStrToObject } from '../../../../utils/text'
|
||||
import { jsStrToObject, modifyCasing } from '../../../../utils/text'
|
||||
|
||||
export const Signals: AttributePlugin = {
|
||||
type: PluginType.Attribute,
|
||||
name: 'signals',
|
||||
removeOnLoad: () => true,
|
||||
onLoad: (ctx) => {
|
||||
const { key, value, genRX, signals, mods } = ctx
|
||||
const { key, mods, signals, value, genRX } = ctx
|
||||
const ifMissing = mods.has('ifmissing')
|
||||
// BEN: investigate use of `ifMissing` in the following line
|
||||
if (key !== '' && !ifMissing) {
|
||||
const k = modifyCasing(key, mods)
|
||||
const v = value === '' ? value : genRX()()
|
||||
signals.setValue(key, v)
|
||||
signals.setValue(k, v)
|
||||
} else {
|
||||
const obj = jsStrToObject(ctx.value)
|
||||
ctx.value = JSON.stringify(obj)
|
||||
|
|
|
@ -9,24 +9,27 @@ import {
|
|||
PluginType,
|
||||
Requirement,
|
||||
} from '../../../../engine/types'
|
||||
import { kebabize } from '../../../../utils/text'
|
||||
import { kebab } from '../../../../utils/text'
|
||||
|
||||
export const Attr: AttributePlugin = {
|
||||
type: PluginType.Attribute,
|
||||
name: 'attr',
|
||||
valReq: Requirement.Must,
|
||||
onLoad: ({ el, genRX, key, effect }) => {
|
||||
onLoad: ({ el, key, effect, genRX }) => {
|
||||
const rx = genRX()
|
||||
if (key === '') {
|
||||
return effect(async () => {
|
||||
const binds = rx<NestedValues>()
|
||||
for (const [attr, val] of Object.entries(binds)) {
|
||||
// BEN: add support for boolean attributes?
|
||||
el.setAttribute(attr, val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
key = kebabize(key)
|
||||
// Attributes are always kebab-case
|
||||
key = kebab(key)
|
||||
|
||||
return effect(async () => {
|
||||
let value = false
|
||||
try {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
PluginType,
|
||||
Requirement,
|
||||
} from '../../../../engine/types'
|
||||
import { trimDollarSignPrefix } from '../../../../utils/text'
|
||||
import { modifyCasing, trimDollarSignPrefix } from '../../../../utils/text'
|
||||
|
||||
const dataURIRegex = /^data:(?<mime>[^;]+);base64,(?<contents>.*)$/
|
||||
const updateEvents = ['change', 'input', 'keydown']
|
||||
|
@ -20,8 +20,8 @@ export const Bind: AttributePlugin = {
|
|||
keyReq: Requirement.Exclusive,
|
||||
valReq: Requirement.Exclusive,
|
||||
onLoad: (ctx) => {
|
||||
const { el, value, key, signals, effect } = ctx
|
||||
const signalName = key ? key : trimDollarSignPrefix(value)
|
||||
const { el, key, mods, signals, value, effect } = ctx
|
||||
const signalName = key ? modifyCasing(key, mods) : trimDollarSignPrefix(value)
|
||||
|
||||
let setFromSignal = () => {}
|
||||
let el2sig = () => {}
|
||||
|
|
|
@ -8,12 +8,13 @@ import {
|
|||
PluginType,
|
||||
Requirement,
|
||||
} from '../../../../engine/types'
|
||||
import { modifyCasing } from '../../../../utils/text'
|
||||
|
||||
export const Class: AttributePlugin = {
|
||||
type: PluginType.Attribute,
|
||||
name: 'class',
|
||||
valReq: Requirement.Must,
|
||||
onLoad: ({ key, el, genRX, effect }) => {
|
||||
onLoad: ({ el, key, mods, effect, genRX }) => {
|
||||
const cl = el.classList
|
||||
const rx = genRX()
|
||||
return effect(() => {
|
||||
|
@ -28,6 +29,7 @@ export const Class: AttributePlugin = {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
key = modifyCasing(key, mods)
|
||||
const shouldInclude = rx<boolean>()
|
||||
if (shouldInclude) {
|
||||
cl.add(key)
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '../../../../engine/types'
|
||||
import { onElementRemoved } from '../../../../utils/dom'
|
||||
import { tagHas, tagToMs } from '../../../../utils/tags'
|
||||
import { kebabize } from '../../../../utils/text'
|
||||
import { modifyCasing } from '../../../../utils/text'
|
||||
import { debounce, delay, throttle } from '../../../../utils/timing'
|
||||
|
||||
const lastSignalsMarshalled = new Map<string, any>()
|
||||
|
@ -25,7 +25,7 @@ export const On: AttributePlugin = {
|
|||
valReq: Requirement.Must,
|
||||
argNames: [EVT],
|
||||
removeOnLoad: (rawKey: string) => rawKey.startsWith('onLoad'),
|
||||
onLoad: ({ el, rawKey, key, value, genRX, mods }) => {
|
||||
onLoad: ({ el, key, mods, rawKey, value, genRX }) => {
|
||||
const rx = genRX()
|
||||
let target: Element | Window | Document = el
|
||||
if (mods.has('window')) target = window
|
||||
|
@ -70,7 +70,7 @@ export const On: AttributePlugin = {
|
|||
if (mods.has('passive')) evtListOpts.passive = true
|
||||
if (mods.has('once')) evtListOpts.once = true
|
||||
|
||||
const eventName = kebabize(key).toLowerCase()
|
||||
const eventName = modifyCasing(key, mods)
|
||||
switch (eventName) {
|
||||
case 'load': {
|
||||
callback()
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
PluginType,
|
||||
Requirement,
|
||||
} from '../../../../engine/types'
|
||||
import { trimDollarSignPrefix } from '../../../../utils/text'
|
||||
import { modifyCasing, trimDollarSignPrefix } from '../../../../utils/text'
|
||||
|
||||
// Sets the value of the element
|
||||
export const Ref: AttributePlugin = {
|
||||
|
@ -16,8 +16,8 @@ export const Ref: AttributePlugin = {
|
|||
name: 'ref',
|
||||
keyReq: Requirement.Exclusive,
|
||||
valReq: Requirement.Exclusive,
|
||||
onLoad: ({ el, key, value, signals }) => {
|
||||
const signalName = key ? key : trimDollarSignPrefix(value)
|
||||
onLoad: ({ el, key, mods, signals, value }) => {
|
||||
const signalName = key ? modifyCasing(key, mods) : trimDollarSignPrefix(value)
|
||||
signals.setValue(signalName, el)
|
||||
return () => signals.setValue(signalName, null)
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ export const Text: AttributePlugin = {
|
|||
keyReq: Requirement.Denied,
|
||||
valReq: Requirement.Must,
|
||||
onLoad: (ctx) => {
|
||||
const { el, genRX, effect } = ctx
|
||||
const { el, effect, genRX } = ctx
|
||||
const rx = genRX()
|
||||
if (!(el instanceof HTMLElement)) {
|
||||
runtimeErr('TextInvalidElement', ctx)
|
||||
|
|
|
@ -1,19 +1,33 @@
|
|||
import type { Modifiers } from '../engine/types'
|
||||
|
||||
export const isBoolString = (str: string) => str.trim() === 'true'
|
||||
|
||||
export const lcFirst = (str: string) =>
|
||||
str[0].toLowerCase() + str.slice(1)
|
||||
|
||||
export const kebabize = (str: string) =>
|
||||
export const kebab = (str: string) =>
|
||||
str.replace(
|
||||
/[A-Z]+(?![a-z])|[A-Z]/g,
|
||||
($, ofs) => (ofs ? '-' : '') + $.toLowerCase(),
|
||||
)
|
||||
|
||||
export const camelize = (str: string) =>
|
||||
kebabize(str).replace(/-./g, (x) => x[1].toUpperCase())
|
||||
kebab(str).replace(/-./g, (x) => x[1].toUpperCase())
|
||||
|
||||
export const snake = (str: string) => kebab(str).replace(/-/g, '_')
|
||||
|
||||
export const pascal = (str: string) =>
|
||||
camelize(str).replace(/^./, (x) => x[0].toUpperCase())
|
||||
|
||||
export const jsStrToObject = (raw: string) =>
|
||||
new Function(`return Object.assign({}, ${raw})`)()
|
||||
|
||||
export const trimDollarSignPrefix = (str: string) =>
|
||||
str.startsWith('$') ? str.slice(1) : str
|
||||
|
||||
const caseFns: Record<string, (s: string) => string> = { kebab, snake, pascal }
|
||||
|
||||
export function modifyCasing(str: string, mods: Modifiers) {
|
||||
for (const c of mods.get('case') || []) {
|
||||
const fn = caseFns[c]
|
||||
if (fn) str = fn(str)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import "time"
|
|||
const (
|
||||
DatastarKey = "datastar"
|
||||
Version = "1.0.0-beta.3"
|
||||
VersionClientByteSize = 40722
|
||||
VersionClientByteSizeGzip = 14879
|
||||
VersionClientByteSize = 40864
|
||||
VersionClientByteSizeGzip = 14915
|
||||
|
||||
//region Default durations
|
||||
|
||||
|
|
|
@ -107,6 +107,7 @@ func setupExamples(ctx context.Context, router chi.Router, signals sessions.Stor
|
|||
{ID: "timing"},
|
||||
{ID: "aliased_data_attributes"},
|
||||
{ID: "merge_fragment"},
|
||||
{ID: "key_casing"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
## Key Casing
|
||||
|
||||
## Demo
|
||||
|
||||
<div id="key-casing"
|
||||
data-signals-foo-foo="1"
|
||||
data-signals-foo-bar__case.kebab="1"
|
||||
data-signals-foo-baz__case.snake="1"
|
||||
data-signals-foo-boo__case.pascal="1"
|
||||
>
|
||||
<pre data-text="ctx.signals.JSON()"></pre>
|
||||
</div>
|
||||
|
||||
## Explanation
|
||||
|
||||
```
|
||||
<div
|
||||
data-signals-foo-foo="1"
|
||||
data-signals-foo-bar__case.kebab="1"
|
||||
data-signals-foo-baz__case.snake="1"
|
||||
data-signals-foo-boo__case.pascal="1"
|
||||
>
|
||||
</div>
|
||||
```
|
|
@ -64,7 +64,9 @@ The `data-signals` attribute can also be used to merge multiple signals using a
|
|||
|
||||
The value above is written in Javascript object notation, but JSON, which is a subset and which most templating languages have built-in support for, is also allowed.
|
||||
|
||||
Note that `data-*` attributes are case-insensitive. If you want to use uppercase characters in signal names, you’ll need to kebabize them or use the object syntax. So the signal name `mySignal` must be written as `data-signals-my-signal` or `data-signals="{mySignal: 1}"`.
|
||||
Note that `data-*` attributes are case-insensitive. If you want to use uppercase characters in signal names, you'll need to kebabize them or use object syntax. So the signal name `mySignal` must be written as `data-signals-my-signal` or `data-signals="{mySignal: 1}"`.
|
||||
|
||||
You can further modify the casing of keys in `data-*` attributes using the `__case` modifier, followed by `.kebab`, `.snake`, or `.pascal`.
|
||||
|
||||
#### Modifiers
|
||||
|
||||
|
|
Loading…
Reference in New Issue