@erickmerchant/framework
Advanced tools
Comparing version 48.4.0 to 49.0.0
40
app.js
@@ -0,32 +1,38 @@ | ||
import {morph} from './morph.js'; | ||
export const createApp = (state) => { | ||
let willCallView = false | ||
const views = [] | ||
let willCallView = false; | ||
const views = []; | ||
const callView = () => { | ||
if (willCallView) return | ||
if (willCallView) return; | ||
willCallView = true | ||
willCallView = true; | ||
Promise.resolve().then(() => { | ||
willCallView = false | ||
willCallView = false; | ||
for (const view of views) view(state) | ||
}) | ||
} | ||
for (const [target, view] of views) morph(target, view(state)); | ||
}); | ||
}; | ||
return { | ||
render(v) { | ||
views.push(v) | ||
render(view, target) { | ||
if (typeof target === 'string') { | ||
target = document.querySelector(target); | ||
} | ||
callView() | ||
views.push([target, view]); | ||
callView(); | ||
}, | ||
set state(val) { | ||
state = val | ||
state = val; | ||
callView() | ||
callView(); | ||
}, | ||
get state() { | ||
return state | ||
} | ||
} | ||
} | ||
return state; | ||
}, | ||
}; | ||
}; |
@@ -6,14 +6,14 @@ const obj = { | ||
'"': '"', | ||
"'": ''' | ||
} | ||
const valuesArr = Object.values(obj) | ||
const keysStr = Object.keys(obj).join('') | ||
const regex = new RegExp(keysStr, 'g') | ||
"'": ''', | ||
}; | ||
const valuesArr = Object.values(obj); | ||
const keysStr = Object.keys(obj).join(''); | ||
const regex = new RegExp(keysStr, 'g'); | ||
export const escape = (str) => { | ||
try { | ||
return str.replace(regex, (match) => valuesArr[keysStr.indexOf(match)]) | ||
return str.replace(regex, (match) => valuesArr[keysStr.indexOf(match)]); | ||
} catch { | ||
return str | ||
return str; | ||
} | ||
} | ||
}; |
252
html.js
@@ -1,6 +0,6 @@ | ||
const weakMap = new WeakMap() | ||
const weakMap = new WeakMap(); | ||
const throwAssertionError = (actual, expected) => { | ||
throw Error(`Expected ${expected}. Found ${actual}.`) | ||
} | ||
throw Error(`Expected ${expected}. Found ${actual}.`); | ||
}; | ||
@@ -16,55 +16,55 @@ export const tokenTypes = { | ||
constant: 'constant', | ||
end: 'end' | ||
} | ||
end: 'end', | ||
}; | ||
const valueTrue = { | ||
const TRUE = { | ||
type: tokenTypes.value, | ||
value: true | ||
} | ||
value: true, | ||
}; | ||
const END = { | ||
type: tokenTypes.end | ||
} | ||
type: tokenTypes.end, | ||
}; | ||
const createIsChar = (regex) => (char) => char && regex.test(char) | ||
const createIsChar = (regex) => (char) => char && regex.test(char); | ||
const isSpaceChar = createIsChar(/\s/) | ||
const isNameChar = createIsChar(/[:@a-zA-Z0-9-]/) | ||
const isQuoteChar = createIsChar(/["']/) | ||
const isSpaceChar = createIsChar(/\s/); | ||
const isNameChar = createIsChar(/[:@a-zA-Z0-9-]/); | ||
const isQuoteChar = createIsChar(/["']/); | ||
const tokenizer = { | ||
*tokenize(acc, strs, vlength) { | ||
let str, i, char | ||
let str, i, char; | ||
const nextChar = () => { | ||
char = str.charAt(i++) | ||
} | ||
char = str.charAt(i++); | ||
}; | ||
for (let index = 0, length = strs.length; index < length; index++) { | ||
str = strs[index] | ||
i = 0 | ||
str = strs[index]; | ||
i = 0; | ||
nextChar() | ||
nextChar(); | ||
let tag = acc.tag | ||
let tag = acc.tag; | ||
while (char) { | ||
if (!tag) { | ||
let value = '' | ||
let value = ''; | ||
if (char === '<') { | ||
let end = false | ||
let end = false; | ||
nextChar() | ||
nextChar(); | ||
if (char === '/') { | ||
end = true | ||
end = true; | ||
nextChar() | ||
nextChar(); | ||
} | ||
while (isNameChar(char)) { | ||
value += char | ||
value += char; | ||
nextChar() | ||
nextChar(); | ||
} | ||
@@ -74,11 +74,11 @@ | ||
type: !end ? tokenTypes.tag : tokenTypes.endtag, | ||
value | ||
} | ||
value, | ||
}; | ||
tag = value | ||
tag = value; | ||
} else { | ||
while (char && char !== '<') { | ||
value += char | ||
value += char; | ||
nextChar() | ||
nextChar(); | ||
} | ||
@@ -89,10 +89,10 @@ | ||
type: tokenTypes.text, | ||
value | ||
} | ||
value, | ||
}; | ||
} | ||
} | ||
} else if (isSpaceChar(char)) { | ||
nextChar() | ||
nextChar(); | ||
} else if (char === '/') { | ||
nextChar() | ||
nextChar(); | ||
@@ -104,23 +104,23 @@ if (char === '>') { | ||
type: tokenTypes.endtag, | ||
value: tag | ||
} | ||
] | ||
value: tag, | ||
}, | ||
]; | ||
tag = false | ||
tag = false; | ||
nextChar() | ||
nextChar(); | ||
} | ||
} else if (char === '>') { | ||
yield END | ||
yield END; | ||
tag = false | ||
tag = false; | ||
nextChar() | ||
nextChar(); | ||
} else if (isNameChar(char)) { | ||
let value = '' | ||
let value = ''; | ||
while (isNameChar(char)) { | ||
value += char | ||
value += char; | ||
nextChar() | ||
nextChar(); | ||
} | ||
@@ -130,37 +130,37 @@ | ||
type: tokenTypes.key, | ||
value | ||
} | ||
value, | ||
}; | ||
if (char === '=') { | ||
nextChar() | ||
nextChar(); | ||
let quote = '' | ||
let value = '' | ||
let quote = ''; | ||
let value = ''; | ||
if (isQuoteChar(char)) { | ||
quote = char | ||
quote = char; | ||
nextChar() | ||
nextChar(); | ||
while (char !== quote) { | ||
if (char) { | ||
value += char | ||
value += char; | ||
nextChar() | ||
nextChar(); | ||
} else { | ||
throwAssertionError('', quote) | ||
throwAssertionError('', quote); | ||
} | ||
} | ||
nextChar() | ||
nextChar(); | ||
yield { | ||
type: tokenTypes.value, | ||
value | ||
} | ||
value, | ||
}; | ||
} else if (char) { | ||
throwAssertionError(char, '"') | ||
throwAssertionError(char, '"'); | ||
} | ||
} else { | ||
yield valueTrue | ||
yield TRUE; | ||
} | ||
@@ -170,3 +170,3 @@ } | ||
acc.tag = tag | ||
acc.tag = tag; | ||
@@ -176,8 +176,8 @@ if (index < vlength) { | ||
type: tokenTypes.variable, | ||
value: index | ||
} | ||
value: index, | ||
}; | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -193,32 +193,32 @@ const parse = (read, parent, tag, variables) => { | ||
attributes: 0, | ||
children: null | ||
} | ||
} | ||
children: null, | ||
}, | ||
}; | ||
let token | ||
let token; | ||
while ((token = read())) { | ||
if (token === END) break | ||
if (token === END) break; | ||
if (token.type === tokenTypes.key) { | ||
const key = token.value | ||
const key = token.value; | ||
token = read() | ||
token = read(); | ||
const firstChar = key.charAt(0) | ||
const special = ':' === firstChar || '@' === firstChar | ||
const firstChar = key.charAt(0); | ||
const special = ':' === firstChar || '@' === firstChar; | ||
let value = token.value | ||
let constant = false | ||
let value = token.value; | ||
let constant = false; | ||
if (token.type === tokenTypes.value) { | ||
constant = true | ||
constant = true; | ||
} else if (token.type === tokenTypes.variable && !special && !html.dev) { | ||
value = variables[value] | ||
value = variables[value]; | ||
constant = true | ||
constant = true; | ||
} | ||
if (constant) { | ||
child.offsets.attributes++ | ||
child.offsets.attributes++; | ||
@@ -228,6 +228,6 @@ child.attributes.unshift({ | ||
key, | ||
value | ||
}) | ||
value, | ||
}); | ||
} else { | ||
child.dynamic = true | ||
child.dynamic = true; | ||
@@ -237,7 +237,7 @@ child.attributes.push({ | ||
key, | ||
value | ||
}) | ||
value, | ||
}); | ||
} | ||
} else { | ||
throwAssertionError(token.type, END.type) | ||
throwAssertionError(token.type, END.type); | ||
} | ||
@@ -248,21 +248,21 @@ } | ||
if (token.type === tokenTypes.endtag && token.value === child.tag) { | ||
break | ||
break; | ||
} else if (token.type === tokenTypes.tag) { | ||
const dynamic = parse(read, child, token.value, variables) | ||
const dynamic = parse(read, child, token.value, variables); | ||
child.dynamic ||= dynamic | ||
child.dynamic ||= dynamic; | ||
} else if (token.type === tokenTypes.text) { | ||
child.children.push({ | ||
type: tokenTypes.text, | ||
value: token.value | ||
}) | ||
value: token.value, | ||
}); | ||
} else if (token.type === tokenTypes.variable) { | ||
child.dynamic = true | ||
child.dynamic = true; | ||
child.offsets.children ??= child.children.length | ||
child.offsets.children ??= child.children.length; | ||
child.children.push({ | ||
type: tokenTypes.variable, | ||
value: token.value | ||
}) | ||
value: token.value, | ||
}); | ||
} | ||
@@ -272,34 +272,34 @@ } | ||
if (child.dynamic) { | ||
parent.offsets.children ??= parent.children.length | ||
parent.offsets.children ??= parent.children.length; | ||
} | ||
parent.children.push(child) | ||
parent.children.push(child); | ||
child.offsets.children ??= child.children.length | ||
child.offsets.children ??= child.children.length; | ||
return child.dynamic | ||
} | ||
return child.dynamic; | ||
}; | ||
let id = 1 | ||
let id = 1; | ||
export const html = (strs, ...variables) => { | ||
let template = weakMap.get(strs) | ||
let template = weakMap.get(strs); | ||
if (!template) { | ||
const acc = { | ||
tag: false | ||
} | ||
tag: false, | ||
}; | ||
const tokens = tokenizer.tokenize(acc, strs, variables.length) | ||
const read = () => tokens.next().value | ||
const tokens = tokenizer.tokenize(acc, strs, variables.length); | ||
const read = () => tokens.next().value; | ||
const children = [] | ||
const offsets = {children: null} | ||
let token | ||
const children = []; | ||
const offsets = {children: null}; | ||
let token; | ||
while ((token = read())) { | ||
if (token.type === tokenTypes.tag) { | ||
parse(read, {children, offsets}, token.value, variables) | ||
parse(read, {children, offsets}, token.value, variables); | ||
} else if (token.type === tokenTypes.text && token.value.trim()) { | ||
throwAssertionError(token.type, tokenTypes.node) | ||
throwAssertionError(token.type, tokenTypes.node); | ||
} | ||
@@ -309,10 +309,10 @@ } | ||
if (children.length !== 1) { | ||
throwAssertionError(children.length, 1) | ||
throwAssertionError(children.length, 1); | ||
} | ||
template = children[0] | ||
template = children[0]; | ||
template.view = id++ | ||
template.view = id++; | ||
weakMap.set(strs, template) | ||
weakMap.set(strs, template); | ||
} | ||
@@ -322,14 +322,14 @@ | ||
...template, | ||
variables | ||
} | ||
} | ||
variables, | ||
}; | ||
}; | ||
html.dev = false | ||
html.dev = false; | ||
export const cache = (result) => { | ||
if (html.dev) return result | ||
if (html.dev) return result; | ||
result.dynamic = false | ||
result.dynamic = false; | ||
return result | ||
} | ||
return result; | ||
}; |
@@ -1,5 +0,3 @@ | ||
export {createDOMView} from './dom-view.js' | ||
export {cache, html} from './html.js'; | ||
export {cache, html} from './html.js' | ||
export {createApp} from './app.js' | ||
export {createApp} from './app.js'; |
{ | ||
"name": "@erickmerchant/framework", | ||
"version": "48.4.0", | ||
"version": "49.0.0", | ||
"description": "A front-end framework.", | ||
@@ -23,3 +23,3 @@ "homepage": "https://github.com/erickmerchant/framework#readme", | ||
"app.js", | ||
"dom-view.js", | ||
"morph.js", | ||
"html.js" | ||
@@ -36,4 +36,4 @@ ], | ||
"@erickmerchant/eslint-config": "^3.0.0", | ||
"@erickmerchant/prettier-config": "^1.2.0", | ||
"jsdom": "^17.0.0", | ||
"@erickmerchant/prettier-config": "^2.0.0", | ||
"jsdom": "^18.0.0", | ||
"tap": "^15.0.9" | ||
@@ -40,0 +40,0 @@ }, |
@@ -7,7 +7,6 @@ # @erickmerchant/framework | ||
```js | ||
```javascript | ||
import { | ||
cache | ||
createApp, | ||
createDOMView, | ||
html, | ||
@@ -23,14 +22,12 @@ } from 'https://cdn.skypack.dev/@erickmerchant/framework?min' | ||
```js | ||
const target = document.querySelector('#app') | ||
```javascript | ||
const target = document.querySelector('#app'); | ||
const cls = getCls() | ||
const cls = getCls(); | ||
const view = (state) => html` | ||
<div id="app" class=${cls} /> | ||
` | ||
`; | ||
const domView = createDOMView(target, view) | ||
app.render(domView) | ||
app.render(view, target); | ||
``` | ||
@@ -46,3 +43,3 @@ | ||
```js | ||
```javascript | ||
const view = (state) => html` | ||
@@ -59,3 +56,3 @@ <div id="app"> | ||
</div> | ||
` | ||
`; | ||
``` | ||
@@ -69,4 +66,4 @@ | ||
```js | ||
const title = 'The heading' | ||
```javascript | ||
const title = 'The heading'; | ||
@@ -77,3 +74,3 @@ const heading = cache( | ||
` | ||
) | ||
); | ||
@@ -92,3 +89,3 @@ const view = (state) => html` | ||
</div> | ||
` | ||
`; | ||
``` | ||
@@ -100,7 +97,9 @@ | ||
```js | ||
let hasFoo = true | ||
Please note that `class` will get expanded to `className` and `for` will get expanded to `htmlFor`. | ||
let barValue = "I'm the bar value" | ||
```javascript | ||
let hasFoo = true; | ||
let barValue = "I'm the bar value"; | ||
const view = (state) => html` | ||
@@ -117,3 +116,3 @@ <form id="app"> | ||
</form> | ||
` | ||
`; | ||
``` | ||
@@ -125,6 +124,6 @@ | ||
```js | ||
```javascript | ||
const onClick = (e) => { | ||
e.preventDefault() | ||
} | ||
e.preventDefault(); | ||
}; | ||
@@ -135,13 +134,15 @@ const view = (state) => html` | ||
</div> | ||
` | ||
`; | ||
``` | ||
The framework always uses event delegation. For instance with this click handler above a single event handler is added to the document with capture set to true. When a click occurs the target is checked to see if it was registered as having a handler. If it was then the handler is called with the event object. | ||
## Changing state | ||
```js | ||
```javascript | ||
const onClick = (e) => { | ||
e.preventDefault() | ||
e.preventDefault(); | ||
app.state = {count: app.state.count + 1} | ||
} | ||
app.state = {count: app.state.count + 1}; | ||
}; | ||
@@ -153,3 +154,3 @@ const view = (state) => html` | ||
</div> | ||
` | ||
`; | ||
``` | ||
@@ -159,4 +160,4 @@ | ||
```js | ||
import {stringify, html} from '@erickmerchant/framework/stringify.js' | ||
```javascript | ||
import {stringify, html} from '@erickmerchant/framework/stringify.js'; | ||
@@ -174,7 +175,7 @@ const view = (state) => html` | ||
</div> | ||
` | ||
`; | ||
const staticHTML = stringify(view({items})) | ||
const staticHTML = stringify(view({items})); | ||
res.write(staticHTML) | ||
res.write(staticHTML); | ||
``` |
@@ -1,3 +0,3 @@ | ||
import {escape} from './escape.js' | ||
import {tokenTypes} from './html.js' | ||
import {escape} from './escape.js'; | ||
import {tokenTypes} from './html.js'; | ||
@@ -18,63 +18,63 @@ const selfClosing = [ | ||
'track', | ||
'wbr' | ||
] | ||
'wbr', | ||
]; | ||
export const stringify = (obj) => { | ||
const {tag, attributes, children, variables} = obj | ||
const {tag, attributes, children, variables} = obj; | ||
let result = `<${tag}` | ||
const isSelfClosing = selfClosing.includes(tag) | ||
let result = `<${tag}`; | ||
const isSelfClosing = selfClosing.includes(tag); | ||
const reducedAttributes = [] | ||
const reducedAttributes = []; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attribute = attributes[i] | ||
const attribute = attributes[i]; | ||
if (attribute.key.startsWith('@')) continue | ||
if (attribute.key.startsWith('@')) continue; | ||
const hasColon = attribute.key.startsWith(':') | ||
const hasColon = attribute.key.startsWith(':'); | ||
if (hasColon) { | ||
attribute.key = attribute.key.substring(1) | ||
attribute.key = attribute.key.substring(1); | ||
} | ||
reducedAttributes.push(attribute) | ||
reducedAttributes.push(attribute); | ||
} | ||
for (const attr of reducedAttributes) { | ||
let value = attr.value | ||
let value = attr.value; | ||
if (attr.type === tokenTypes.variable) { | ||
value = variables[value] | ||
value = variables[value]; | ||
} | ||
if (value === true) { | ||
result += ` ${attr.key}` | ||
result += ` ${attr.key}`; | ||
} else if (value !== false) { | ||
result += ` ${attr.key}="${escape(value)}"` | ||
result += ` ${attr.key}="${escape(value)}"`; | ||
} | ||
} | ||
result += '>' | ||
result += '>'; | ||
if (!isSelfClosing) { | ||
let i = 0 | ||
let i = 0; | ||
const descendants = [] | ||
const descendants = []; | ||
for (let i = 0; i < children.length; i++) { | ||
let child = children[i] | ||
let child = children[i]; | ||
if (child?.type === tokenTypes.variable) { | ||
child = variables[child.value] | ||
child = variables[child.value]; | ||
} | ||
if (!Array.isArray(child)) { | ||
child = [child] | ||
child = [child]; | ||
} | ||
for (let c of child) { | ||
c ??= '' | ||
c ??= ''; | ||
descendants.push(c) | ||
descendants.push(c); | ||
} | ||
@@ -84,3 +84,3 @@ } | ||
while (i < descendants.length) { | ||
const child = descendants[i] | ||
const child = descendants[i]; | ||
@@ -91,4 +91,4 @@ if (child) { | ||
case tokenTypes.text: | ||
result += tag !== 'style' ? escape(child.value) : child.value | ||
break | ||
result += tag !== 'style' ? escape(child.value) : child.value; | ||
break; | ||
@@ -98,18 +98,18 @@ case tokenTypes.node: | ||
variables: child.view ? child.variables : variables, | ||
...child | ||
}) | ||
break | ||
...child, | ||
}); | ||
break; | ||
} | ||
} else { | ||
result += tag !== 'style' ? escape(child) : child | ||
result += tag !== 'style' ? escape(child) : child; | ||
} | ||
} | ||
i++ | ||
i++; | ||
} | ||
result += `</${tag}>` | ||
result += `</${tag}>`; | ||
} | ||
return result | ||
} | ||
return result; | ||
}; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
19816
535
168
1