🐣 Tiny no-framework component toolkit. 900b gzipped.
tl;dr - this library automatically instantiates JavaScript modules on specific DOM elements in a website if they exist on the page. This is helpful for projects like Shopify or Wordpress that aren't using a framework like React or Vue.
picoapp
also contains functionality that make it a great companion to any PJAX library – like operator – where page transitions can make conventional JS patterns cumbersome.
npm i picoapp --save
Define data attributes on the DOM nodes you need to bind to:
<button data-component='button'>I've been clicked 0 times</button>
Create a corresponding component:
// button.js
import { component } from 'picoapp'
export default component((node, ctx) => {
let count = 0
node.onclick = () => {
node.innerHTML = `I've been clicked ${++count} times`
}
})
Import your component and create a picoapp
instance:
import { picoapp } from 'picoapp'
import button from './button.js'
const app = picoapp({ button })
To bind your component to the DOM node, call mount()
:
app.mount()
picoapp
uses a very simple concept of state, which is shared and updated using
events or hydrate
helpers. Internally, picoapp uses
evx, so check that library out for
more info.
You can define initial state:
const app = picoapp({ button }, { count: 0 })
And consume it on the context
object passed to your component
:
export default component((node, ctx) => {
// ctx.getState().count
})
To interact with state, you will primarily use events. Passing an object
when
emitting an event will merge that object into the global state
. Event
listeners are then passed the entire state
object for consumption.
export default component((node, ctx) => {
ctx.on('incremenent', state => {
node.innerHTML = `I've been clicked ${state.count} times`
})
node.onclick = () => {
ctx.emit('increment', { count: ctx.getState().count + 1 })
}
})
You can also pass a function to an emitter in order to reference the previous state:
ctx.emit('increment', state => {
return {
count: state.count + 1
}
})
Just like evx, picoapp
supports
multi-subscribe, wildcard, and property keyed events as well:
ctx.on([ 'count', 'otherProp' ], state => {}) // fires on `count` & `otherProp`
ctx.on('*', state => {}) // fires on all state updates
ctx.on('someProp', ({ someProp }) => {}) // fires on all someProp updates
If you need to update state, but don't need to fire an event, you can use
ctx.hydrate
:
export default component((node, ctx) => {
ctx.hydrate({ count: 12 })
})
picoapp
components are instantiated as soon as they're found in the DOM after
calling mount()
. Sometimes you'll also need to un-mount a component, say to
destroy a slideshow or global event listener after an AJAX page transition.
To do so, return a function from your component:
import { component } from 'picoapp'
export default component((node, ctx) => {
ctx.on('incremenent', state => {
node.innerHTML = `I've been clicked ${state.count} times`
})
function handler (e) {
ctx.emit('increment', { count: ctx.getState().count + 1 })
}
node.addEventListener('click', handler)
return (node) => {
node.removeEventListener('click', handler)
}
})
And then, call unmount()
. If the component no longer exists in the DOM, its
unmount
handler will be called.
app.unmount()
Regardless of if you define an unmount
handler, any event subscriptions you
made in your component will be destroyed.
unmount()
is also synchronous, so given a PJAX library like
operator, you can do this after
every route transition:
router.on('after', state => {
app.unmount() // cleanup
app.mount() // init new components
})
The picoapp
instance also has access to start and the event bus:
app.emit('event', { data: 'global' })
app.on('event', state => {})
So you can add arbitrary state to the global state
object directly:
app.hydrate({ count: 5 })
And then access it from anywhere:
app.getState() // { count: 5 }
If you need to add components – maybe asynchronously – you can use add
:
app.add({
lazyImage: component(context => {})
})
If data-component
isn't your style, or you'd like to use different types of
"components", pass your attributes to mount()
:
Given the below, picoapp
will scan the DOM for both data-component
and
data-util
attributes and init their corresponding JS modules:
app.mount([
'data-component',
'data-util'
])
MIT License © Eric Bailey