From d6db20b9272570340f63aea6d8b686a119738577 Mon Sep 17 00:00:00 2001 From: Nik Rowell Date: Fri, 6 Dec 2019 09:54:50 -0700 Subject: [PATCH 1/4] add component extensions / plugin functionality via app.use() --- index.js | 16 ++++++++++++---- test.js | 27 +++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 39eeafb..694d358 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ const isFn = v => typeof v === 'function' // make sure evx and picoapp don't destroy the same events export function component (create) { - return function initialize (node, ctx) { + return function initialize (node, ctx, args = {}) { let subs = [] return { subs, @@ -16,13 +16,13 @@ export function component (create) { subs.push(u) return u } - }), + }, args), node } } } -export function picoapp (components = {}, initialState = {}) { +export function picoapp (components = {}, initialState = {}, plugins = []) { const evx = create(initialState) let cache = [] @@ -37,6 +37,10 @@ export function picoapp (components = {}, initialState = {}) { if (!isObj(index)) throw 'components should be an object' Object.assign(components, index) }, + use (fn) { + if (!isFn(fn)) throw 'plugins should be a function' + plugins.push(fn); + }, hydrate (data) { return evx.hydrate(data) }, @@ -58,7 +62,11 @@ export function picoapp (components = {}, initialState = {}) { node.removeAttribute(attr) // so can't be bound twice try { - const instance = comp(node, evx) + const args = plugins.reduce((res, fn) => { + const obj = fn(node) + return isObj(obj) ? Object.assign(res, obj) : res + }, {}) + const instance = comp(node, evx, args) isFn(instance.unmount) && cache.push(instance) } catch (e) { console.log(`🚨 %cpicoapp - ${modules[m]} failed - ${e.message || e}`, 'color: #E85867') diff --git a/test.js b/test.js index 71e33a5..a54ea03 100644 --- a/test.js +++ b/test.js @@ -2,6 +2,9 @@ import test from 'ava' import { picoapp, component } from './dist/picoapp.js' const createNode = attr => ({ + dataset: { + props: '{"hello": "World"}' + }, getAttribute () { return attr }, @@ -64,7 +67,7 @@ test('mount', t => { }) test('unmount', t => { t.plan(4) - + const app = picoapp({ foo: component((node, ctx) => { ctx.on('foo', () => t.truthy(1)) @@ -76,10 +79,26 @@ test('unmount', t => { }) app.mount() - + app.emit('foo') - + app.unmount() - + app.emit('foo') +}) +test('plugins', t => { + + const app = picoapp({ + foo: component((node, ctx, args) => { + t.true(args.props.hello === 'World') + t.true(typeof args.findOne === 'function') + }) + }) + + app.use(node => ({ + findOne: s => node.querySelector(s), + props: JSON.parse(node.dataset.props || '{}') + })) + + app.mount() }) \ No newline at end of file From f106e3c465d2e90a3ac38fe8804d64f60d25808f Mon Sep 17 00:00:00 2001 From: Nik Rowell Date: Wed, 12 Feb 2020 07:28:35 -0700 Subject: [PATCH 2/4] merge plugin extensions with context on component creation --- index.js | 8 ++++---- test.js | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 694d358..fb6c87d 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ const isFn = v => typeof v === 'function' // make sure evx and picoapp don't destroy the same events export function component (create) { - return function initialize (node, ctx, args = {}) { + return function initialize (node, ctx) { let subs = [] return { subs, @@ -16,7 +16,7 @@ export function component (create) { subs.push(u) return u } - }, args), + }), node } } @@ -62,11 +62,11 @@ export function picoapp (components = {}, initialState = {}, plugins = []) { node.removeAttribute(attr) // so can't be bound twice try { - const args = plugins.reduce((res, fn) => { + const ext = plugins.reduce((res, fn) => { const obj = fn(node) return isObj(obj) ? Object.assign(res, obj) : res }, {}) - const instance = comp(node, evx, args) + const instance = comp(node, {...ext, ...evx}) isFn(instance.unmount) && cache.push(instance) } catch (e) { console.log(`🚨 %cpicoapp - ${modules[m]} failed - ${e.message || e}`, 'color: #E85867') diff --git a/test.js b/test.js index a54ea03..3cd772e 100644 --- a/test.js +++ b/test.js @@ -89,14 +89,12 @@ test('unmount', t => { test('plugins', t => { const app = picoapp({ - foo: component((node, ctx, args) => { - t.true(args.props.hello === 'World') - t.true(typeof args.findOne === 'function') + foo: component((node, ctx) => { + t.true(ctx.props.hello === 'World') }) }) - app.use(node => ({ - findOne: s => node.querySelector(s), + app.use((node, ctx) => ({ props: JSON.parse(node.dataset.props || '{}') })) From bcef0266c1a9adfa4a086cce51eedfa54c2063d1 Mon Sep 17 00:00:00 2001 From: Nik Rowell Date: Wed, 12 Feb 2020 07:40:52 -0700 Subject: [PATCH 3/4] add test assertion to ensure context internals are not overridden by plugins --- index.js | 2 +- test.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index fb6c87d..42db523 100644 --- a/index.js +++ b/index.js @@ -63,7 +63,7 @@ export function picoapp (components = {}, initialState = {}, plugins = []) { try { const ext = plugins.reduce((res, fn) => { - const obj = fn(node) + const obj = fn(node, evx) return isObj(obj) ? Object.assign(res, obj) : res }, {}) const instance = comp(node, {...ext, ...evx}) diff --git a/test.js b/test.js index 3cd772e..8b17fac 100644 --- a/test.js +++ b/test.js @@ -87,14 +87,23 @@ test('unmount', t => { app.emit('foo') }) test('plugins', t => { + t.plan(2) + + function testContext(ctx) { + const internals = ['getState', 'hydrate', 'on', 'emit'] + const preserved = internals.every(key => typeof ctx[key] === 'function') + t.true(preserved) + } const app = picoapp({ foo: component((node, ctx) => { t.true(ctx.props.hello === 'World') + testContext(ctx); }) }) app.use((node, ctx) => ({ + getState: undefined, props: JSON.parse(node.dataset.props || '{}') })) From 6e33ad108dc02f6973951f5f9ba4b753eacc1e71 Mon Sep 17 00:00:00 2001 From: Nik Rowell Date: Wed, 12 Feb 2020 08:19:32 -0700 Subject: [PATCH 4/4] update README to include Plugins docs --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index de5b351..24cbeb8 100644 --- a/README.md +++ b/README.md @@ -185,5 +185,25 @@ app.mount([ ]) ``` +## Plugins + +The `picoapp` instance allows you to extend the component API through plugins. Plugins are functions that return objects, which then get merged into the `context` object passed to your `component`. Note that name conflicts with plugin properties will always be overriden by [picoapp's context](#state-&-events). + +To define plugins, pass a function to the `use` method. The example below adds a `props` object extracted from the component node's `data-props` attribute: +```javascript +app.use(node => { + const props = JSON.parse(node.dataset.props || '{}') + return {props} +}) +``` + +And then acccess plugin extensions from your component: +```javascript +const foo = component(node, ctx) => { + const { images = [] } = ctx.props + console.log(`start preloading ${images.length} images...`) +}) +``` + ## License MIT License © [Eric Bailey](https://estrattonbailey.com)