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

feat: mounted, each & props #26

Merged
merged 6 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions example/Counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { div, p, button, classNames } from '../lib/one'
import styles from './styles.module.css'

function view({ state }) {
return div([
p(`Count: ${state.count}`, {
className: classNames(
styles.paragraph,
state.count < 0 && styles.low,
state.count > 3 && styles.high,
),
}),
Button('Dec', () => state.count--),
Button('Inc', () => state.count++),
])
}

function Button(text, onclick) {
return button(text, { onclick, className: styles.button })
}

const state = { count: 0 }

const Counter = {
selector: '.counter',
state,
view,
}

export { Counter }
61 changes: 61 additions & 0 deletions example/Profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { button, div, h2, p, span } from '../lib/one'
import { getUser } from './services/get-user'
import styles from './styles.module.css'

function view({ state }) {
async function onclick() {
state.isLoading = true
state.user = await getUser(1)
state.notFound = false
state.isLoading = false
}

return div([
h2('Profile'),
!state.user && !state.notFound || state.isLoading ? div([
skeleton('20px', '100px'),
skeleton('20px', '80px'),
skeleton('20px', '180px'),
]) : div(
state.notFound ? [
div('User not found', { className: styles.warning }),
button('Try again', { onclick, className: styles.button }),
] :
Object.keys(state.user).map(key => {
if (key === 'id') return
return p([
span(`${uppercaseFirst(key)}: `),
span(state.user[key], { className: styles.bold }),
])
})
),
], { className: styles.profile })
}

function uppercaseFirst(str) {
return `${str.charAt(0).toUpperCase()}${str.substring(1)}`
}

async function mounted({ state, props }) {
const user = await getUser(props.get('id'))

state.isLoading = false
state.notFound = !Boolean(user)
state.user = user ? user : undefined
}

function skeleton(height = '1em', width = '100%') {
return div([], {
className: styles.skeleton,
style: `height: ${height}; width: ${width};`
})
}

const Profile = {
selector: '.profile',
view,
state: { user: undefined, notFound: false, isLoading: true },
mounted,
}

export { Profile }
31 changes: 8 additions & 23 deletions example/app.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,8 @@
import { mount, button, p, div, classNames } from '../lib/one'
import styles from './styles.module.css'

function view({ state }) {
return div([
p(`Count: ${state.count}`, {
className: classNames(
styles.paragraph,
state.count < 0 && styles.low,
state.count > 3 && styles.high,
),
}),
button('Dec', { onclick() { state.count-- }, className: styles.button }),
button('Inc', { onclick() { state.count++ }, className: styles.button }),
])
}

const state = { count: 0 }

document.querySelectorAll('.counter').forEach(el => {
mount({ el, view, state })
})

import { each } from '../lib/one'
import { Counter } from './Counter'
import { Profile } from './Profile'

each([
Counter,
Profile,
])
1 change: 1 addition & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
<div class="counter"></div>
<div class="counter"></div>
<div class="counter"></div>
<div class="profile" id="5"></div>
<script type="module" src="app.js"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions example/services/get-user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
function getUser(id) {
return new Promise(resolve => {
setTimeout(() => {
resolve(findUser(id))
}, 2250)
})

function findUser(id) {
return users.find(user => user.id === parseInt(id))
}
}

// Mock data
// { id, username, age, address }
const users = [
{ id: 1, username: 'John', age: 32, address: '123 Main St' },
{ id: 2, username: 'Jane', age: 25, address: '456 Main St' },
{ id: 3, username: 'Bob', age: 45, address: '789 Main St' },
]

export { getUser }
29 changes: 29 additions & 0 deletions example/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,32 @@
}
}

.profile {
width: 240px;

.warning {
margin-bottom: 8px;
}
}

.skeleton {
background-color: #333;
border-radius: 4px;
width: 120px;
height: 20px;
margin-bottom: 1em;
}

.bold {
font-weight: bold;
}

.warning {
border-radius: 4px;
background-color: #ffeb3b;
color: #333;
padding: 4px 6px;
font-size: 80%;
font-weight: bold;
text-transform: uppercase;
}
66 changes: 51 additions & 15 deletions lib/one.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function createElement([tag, children, attributes]) {
children.forEach((c) => {
elm.append(Array.isArray(c) ? createElement(c) : c)
})
} else if (typeof children === "string") {
} else {
elm.append(children)
}

Expand Down Expand Up @@ -43,6 +43,10 @@ function walk(shadow, live) {
Array.from(shadow.children).forEach((sChild, index) => {
const lChild = lc[index]

if (sChild.tagName !== lChild.tagName) {
sChild.replaceWith(lChild)
}

// Add children that are not present in the shadow
if (sChild.children.length < lChild.children.length) {
const scc = Array.from(sChild.children)
Expand All @@ -63,10 +67,6 @@ function walk(shadow, live) {
})
}

if (sChild.tagName !== lChild.tagName) {
sChild.replaceWith(lChild.cloneNode(true))
}

// Add attributes that are not present in the shadow
Array.from(lChild.attributes).forEach((attr) => {
if (sChild.getAttribute(attr.name) !== attr.value) {
Expand Down Expand Up @@ -103,7 +103,7 @@ function watch(obj, key, fn) {
let path = obj
let dest = key

if (path.includes('.')) {
if (key.includes('.')) {
const parts = path.split('.')

dest = parts.pop()
Expand All @@ -118,15 +118,25 @@ function watch(obj, key, fn) {
}

/**
* @param {{ el: HTMLElement, state: object, view: function }} context
* @todo Add a way to hook into unmouting
* @todo Add a way to run operations after the view is mounted
* @todo Add a way to run operations before the view is unmounted
* @description Mounts a view to an element.
* @param {object} context
* @param {HTMLElement} context.el The element to mount to
* @param {object} context.state The state object
* @param {function} context.view The view function
* @param {function} [context.mounted] The mounted function
*/
function mount({ el, state, view }) {
function mount(context) {
const { el, state, view, mounted } = context
const props = {
get: (key) => {
return el.attributes.getNamedItem(key)?.value
},
length: el.attributes.length,
}

const proxy = state ? new Proxy({ ...state }, {
set(obj, prop, value) {
if (obj[prop] == null) {
if (!obj.hasOwnProperty(prop)) {
throw new Error(`The property "${prop}" does not exist on the state object.`)
}
if (obj[prop] === value) return true
Expand All @@ -136,7 +146,7 @@ function mount({ el, state, view }) {
walk(
el.$one.shadow,
createElement(
view({ el, state: obj })
view({ el, state: proxy, props })
)
)

Expand All @@ -145,16 +155,41 @@ function mount({ el, state, view }) {
}) : {}

const v = createElement(
view({ el, state: proxy })
view({ el, state: proxy, props })
)

el.$one = { shadow: v }

if (el.children[0]) {
if (el.children?.[0]) {
el.children[0].replaceWith(v)
} else {
el.append(v)
}

mounted?.({ el, state: proxy, props })
}

/**
* @description Mounts a view to all elements matching the selector.
* @typedef {object} Context
* @property {string} context.selector The selector to match
* @property {object} context.state The state object
* @property {function} context.view The view function
* @property {function} [context.mounted] The mounted function
*
* @param {Array<Context>} context
*/
function each(contexts) {
contexts.forEach((c) => {
const { selector, state, view, mounted } = c
const elms = document.querySelectorAll(selector)
if (!elms.length) {
console.warn(`One: No elements found for selector "${selector}"`)
return
}

elms.forEach((el) => mount({ el, state, view, mounted }))
})
}

/**
Expand Down Expand Up @@ -280,6 +315,7 @@ function h6(children, attrs) {

export {
mount,
each,
router,
watch,
classNames,
Expand Down