Skip to content

Commit

Permalink
feat: adds class, dataset, and aria attribute jsx prop transformers.
Browse files Browse the repository at this point in the history
  • Loading branch information
geotrev committed Jan 17, 2022
1 parent a2f6c02 commit 7fa434c
Show file tree
Hide file tree
Showing 16 changed files with 140 additions and 72 deletions.
6 changes: 3 additions & 3 deletions src/__tests__/utilities.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,17 @@ describe("forEach", () => {
})
})

describe("toKebabCase", () => {
describe("camelToKebab", () => {
it("converts camelCase", () => {
const camelName = "imACoolCat"
const kebabName = "im-a-cool-cat"
expect(utils.toKebabCase(camelName)).toEqual(kebabName)
expect(utils.camelToKebab(camelName)).toEqual(kebabName)
})

it("converts PascalCase", () => {
const PascalName = "ImACoolCat"
const kebabName = "im-a-cool-cat"
expect(utils.toKebabCase(PascalName)).toEqual(kebabName)
expect(utils.camelToKebab(PascalName)).toEqual(kebabName)
})
})

Expand Down
2 changes: 1 addition & 1 deletion src/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const External = {
export const Internal = {
// Primitives
rotomId: Symbol("#rotomId"),
vDOM: Symbol("#vDOM"),
vnode: Symbol("#vnode"),
isFirstRender: Symbol("#isFirstRender"),

// Methods
Expand Down
4 changes: 2 additions & 2 deletions src/properties/initialize-property-value.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { validateType } from "./validate-type"
import {
isFunction,
isUndefined,
toKebabCase,
camelToKebab,
sanitizeString,
} from "../utilities"

Expand Down Expand Up @@ -61,7 +61,7 @@ export const initializePropertyValue = (

if (reflected) {
const initialAttrValue = initialValue ? String(initialValue) : ""
const attribute = toKebabCase(propName)
const attribute = camelToKebab(propName)
RotomInstance.setAttribute(attribute, initialAttrValue)
}
}
6 changes: 3 additions & 3 deletions src/properties/upgrade-property.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Internal, External } from "../enums"
import { isUndefined, toKebabCase, sanitizeString } from "../utilities"
import { isUndefined, camelToKebab, sanitizeString } from "../utilities"
import { initializePropertyValue } from "./initialize-property-value"
import { validateType } from "./validate-type"

Expand Down Expand Up @@ -61,7 +61,7 @@ export const upgradeProperty = (
)

if (reflected) {
const attribute = toKebabCase(propName)
const attribute = camelToKebab(propName)
const attrValue = String(value)
RotomInstance.setAttribute(attribute, attrValue)
}
Expand All @@ -76,7 +76,7 @@ export const upgradeProperty = (
)

if (reflected) {
const attribute = toKebabCase(propName)
const attribute = camelToKebab(propName)
RotomInstance.removeAttribute(attribute)
}
}
Expand Down
22 changes: 12 additions & 10 deletions src/renderers/jsx/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import {
attributesModule,
datasetModule,
} from "snabbdom"
import { isFunction } from "./utilities"
import { isFunction } from "../../utilities/is-type"
import { transformJsxProps } from "./transformers"

const createEmptyVNode = (element, Internal) =>
h("!", {
hooks: {
post: () => {
element[Internal.vDOM] = null
element[Internal.vnode] = null
},
},
})
Expand All @@ -34,7 +35,8 @@ const patch = init([
export function renderer({ Internal, External }) {
function getRenderState(element) {
if (isFunction(element[External.render])) {
return sign(element[External.render]())
const vnode = transformJsxProps(element[External.render]())
return sign(vnode)
} else {
throw new Error(
`[RotomElement]: You must include a render method in element: '${element.constructor.name}'`
Expand All @@ -43,15 +45,15 @@ export function renderer({ Internal, External }) {
}

function getInitialRenderState(element) {
const vNode = toVNode(document.createElement("div"))
element[Internal.vDOM] = patch(vNode, getRenderState(element))
element.shadowRoot.appendChild(element[Internal.vDOM].elm)
const vnode = toVNode(document.createElement("div"))
element[Internal.vnode] = patch(vnode, getRenderState(element))
element.shadowRoot.appendChild(element[Internal.vnode].elm)
element[Internal.runLifecycle](External.onMount)
}

function getNextRenderState(element) {
element[Internal.vDOM] = patch(
element[Internal.vDOM],
element[Internal.vnode] = patch(
element[Internal.vnode],
getRenderState(element)
)
element[Internal.runLifecycle](External.onUpdate)
Expand All @@ -71,8 +73,8 @@ export function renderer({ Internal, External }) {
destroy(element) {
if (!window || !window.document) return

element[Internal.vDOM] = patch(
element[Internal.vDOM],
element[Internal.vnode] = patch(
element[Internal.vnode],
createEmptyVNode(element, Internal)
)
},
Expand Down
1 change: 1 addition & 0 deletions src/renderers/jsx/transformers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { transformJsxProps } from "./transform-jsx-props"
33 changes: 33 additions & 0 deletions src/renderers/jsx/transformers/set-prop-to-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { forEach } from "../../../utilities"

/**
* Given a matcher to a vnode property, apply it to the
* appropriate snabbdom module.
* @param {Object} vnode
* @param {RegExp} matcher
* @param {Function} transformer
*/
export function setPropToModule(vnode, matcher, transformer) {
const matches = []

if (vnode.data) {
for (const key in vnode.data) {
if (matcher.test(key))
matches.push({
key,
value: vnode.data[key],
node: vnode,
})
}

forEach(matches, transformer)
}

if (Array.isArray(vnode.children)) {
forEach(vnode.children, (child) =>
setPropToModule(child, matcher, transformer)
)
}

return vnode
}
43 changes: 43 additions & 0 deletions src/renderers/jsx/transformers/transform-jsx-props.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { kebabToCamel } from "../../../utilities"
import { setPropToModule } from "./set-prop-to-module"

/**
* Transform JSX props to snabbdom module data structure.
* @param {Object} vnode
* @returns {Object} vnode
*/
export function transformJsxProps(vnode) {
setPropToModule(vnode, /^aria-/, ({ key, value, node }) => {
// debugger
if (node.data.attrs) {
node.data.attrs[key] = value
} else {
node.data.attrs = { [key]: value }
}
delete node.data[key]
})

setPropToModule(vnode, /^data-/, ({ key, value, node }) => {
const abbrevKey = kebabToCamel(key.slice(5))

if (node.data.dataset) {
node.data.dataset[abbrevKey] = value
} else {
node.data.dataset = { [abbrevKey]: value }
}

delete node.data[key]
})

setPropToModule(vnode, /^className$/, ({ value, node }) => {
if (node.data.props) {
node.data.props.className = value
} else {
node.data.props = { className: value }
}

delete node.data.className
})

return vnode
}
14 changes: 0 additions & 14 deletions src/renderers/jsx/utilities/index.js

This file was deleted.

16 changes: 8 additions & 8 deletions src/renderers/template/renderer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { patch, render, create } from "omdomdom"
import { isString, isFunction } from "./utilities"
import { isString, isFunction } from "../../utilities"

export function renderer({ Internal, External }) {
function getRenderState(element) {
Expand All @@ -23,16 +23,16 @@ export function renderer({ Internal, External }) {
}

function getInitialRenderState(element) {
element[Internal.vDOM] = create(getRenderState(element))
render(element[Internal.vDOM], element.shadowRoot)
element[Internal.vnode] = create(getRenderState(element))
render(element[Internal.vnode], element.shadowRoot)
element[Internal.runLifecycle](External.onMount)
}

function getNextRenderState(element) {
let nextVDOM = create(getRenderState(element))
patch(nextVDOM, element[Internal.vDOM])
let nextVnode = create(getRenderState(element))
patch(nextVnode, element[Internal.vnode])
element[Internal.runLifecycle](External.onUpdate)
nextVDOM = null
nextVnode = null
}

return {
Expand All @@ -58,8 +58,8 @@ export function renderer({ Internal, External }) {
}

element[Internal.isFirstRender] = true
patch(emptyVNode, element[Internal.vDOM])
element[Internal.vDOM] = null
patch(emptyVNode, element[Internal.vnode])
element[Internal.vnode] = null

const children = element.shadowRoot.childNodes
if (children.length) {
Expand Down
21 changes: 0 additions & 21 deletions src/renderers/template/utilities/index.js

This file was deleted.

8 changes: 4 additions & 4 deletions src/rotom-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
isEmptyObject,
isString,
isFunction,
toKebabCase,
camelToKebab,
createUUID,
} from "./utilities"

Expand All @@ -28,7 +28,7 @@ export function rotomFactory(renderer) {

this[Internal.patch] = this[Internal.patch].bind(this)
this[Internal.isFirstRender] = true
this[Internal.vDOM] = null
this[Internal.vnode] = null
this[Internal.rotomId] = createUUID()
}

Expand All @@ -41,7 +41,7 @@ export function rotomFactory(renderer) {
let attributes = []
for (let propName in properties) {
if (!properties[propName].reflected) continue
attributes.push(toKebabCase(propName))
attributes.push(camelToKebab(propName))
}
return attributes
}
Expand Down Expand Up @@ -140,7 +140,7 @@ export function rotomFactory(renderer) {
}

/**
* Called during disconnectedCallback. Clean up the vDOM
* Called during disconnectedCallback. Clean up the vnode
* and remove remaining nodes in the shadowRoot.
*/
[Internal.destroy]() {
Expand Down
2 changes: 1 addition & 1 deletion src/utilities/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { createUUID } from "./create-uuid"
export { forEach } from "./for-each"
export * from "./is-type"
export { toKebabCase } from "./transform-case"
export { camelToKebab, kebabToCamel } from "./transform-case"
export { sanitizeString } from "./sanitize-string"
export { log } from "./log"
18 changes: 17 additions & 1 deletion src/utilities/transform-case.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,25 @@
* @param {string} value
* @returns {string}
*/
export const toKebabCase = (value) =>
export const camelToKebab = (value) =>
value &&
value
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
.map((x) => x.toLowerCase())
.join("-")

/**
* Converts a kebab-case string to camelCase.
* @param {string} value
* @returns {string}
*/
export const kebabToCamel = (value) =>
value &&
value
.split("-")
.map((word, i) =>
i
? word[0].toUpperCase() + word.slice(1).toLowerCase()
: word.toLowerCase()
)
.join("")
8 changes: 8 additions & 0 deletions test/jsx/fixtures/render-schedule-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export class RenderScheduleTest extends RotomElement {
<br />
It should have one render per button press, despite having multiple
property updates.
<div
data-foo-bar="baz"
aria-hidden="true"
aria-labelledby="#0"
className="visually-hidden"
>
Don't read me!
</div>
</p>
<button on={{ click: this.handleClick }}>Click to update</button>
</div>
Expand Down
Loading

0 comments on commit 7fa434c

Please sign in to comment.