Skip to content

Commit

Permalink
feat: mounted, each & props (#26)
Browse files Browse the repository at this point in the history
* fix(walk): check tagName earlier

* fix(watch): check path and not obj

* feat: add `mounted`

* feat: each && props

* example: trying out each, mounted & props

* docs: remove @todo's from `mount()`
  • Loading branch information
sebkolind committed Dec 7, 2023
1 parent 6fc93df commit 89fb378
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 32 deletions.
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;
}
46 changes: 37 additions & 9 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 @@ -44,7 +44,7 @@ function walk(shadow, live) {
const lChild = lc[index]

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

// Add children that are not present in the shadow
Expand Down Expand Up @@ -118,21 +118,25 @@ function watch(obj, key, fn) {
}

/**
* @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
* @todo Add a way to hook into unmouting
* @todo Add a way to run operations after the view is init
* @todo Add a way to run operations before the view is uninit
*/
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 @@ -142,7 +146,7 @@ function mount(context) {
walk(
el.$one.shadow,
createElement(
view({ el, state: obj })
view({ el, state: proxy, props })
)
)

Expand All @@ -151,7 +155,7 @@ function mount(context) {
}) : {}

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

el.$one = { shadow: v }
Expand All @@ -162,7 +166,30 @@ function mount(context) {
el.append(v)
}

mounted?.({ el, state: proxy })
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 @@ -288,6 +315,7 @@ function h6(children, attrs) {

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

0 comments on commit 89fb378

Please sign in to comment.