From 3b05614f5a637e45eed68a660313a11352d28315 Mon Sep 17 00:00:00 2001 From: Nikhil Tilwalli Date: Mon, 21 Sep 2020 16:40:29 -0400 Subject: [PATCH] feat: enable module support --- cypress.json | 8 +++++ cypress/integration/test.spec.js | 32 ++++++++++++++++++++ example/index.js | 25 +++++++++++++++- package.json | 10 ++++++- src/Incorporator.ts | 51 +++++++++++++++++++++++++++++--- src/h.ts | 5 ++-- src/index.ts | 1 + 7 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 cypress.json create mode 100644 cypress/integration/test.spec.js diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000..c80b699 --- /dev/null +++ b/cypress.json @@ -0,0 +1,8 @@ +{ + "baseUrl": "http://localhost:1234", + "chromeWebSecurity": false, + "defaultCommandTimeout": 10000, + "modifyObstructiveCode": false, + "video": false, + "fixturesFolder": false +} diff --git a/cypress/integration/test.spec.js b/cypress/integration/test.spec.js new file mode 100644 index 0000000..d4a85c2 --- /dev/null +++ b/cypress/integration/test.spec.js @@ -0,0 +1,32 @@ +/// + +const { watchFile } = require("fs") + +context('Page load', () => { + beforeEach(() => { + cy.visit('/') + cy.wait(1000) + }) + describe('React integration', () => { + + it('Should mount', () => { + cy.get('#app') + .should('exist', 'success') + }) + it('Should have foo property on button', () => { + cy.get('.clicker') + // .its('foo') + // .should('eq', 3) + .then(($el) => { + const el = $el[0] + cy.wrap(el.foo).should('eq', 3) + }) + }) + it('Should allow toggling className items based on domClass prop', () => { + cy.get('.clicker') + .then(($el) => { + cy.wrap($el[0].className).should('eq', 'clicker hello') + }) + }) + }) +}) diff --git a/example/index.js b/example/index.js index 7e18bea..89cd245 100644 --- a/example/index.js +++ b/example/index.js @@ -1,6 +1,7 @@ import xs from 'xstream'; import {createElement} from 'react'; import {render} from 'react-dom'; +import {setModules} from '../src/Incorporator' import {h, makeComponent} from '../src/index'; function main(sources) { @@ -22,7 +23,12 @@ function main(sources) { const vdom$ = count$.map(i => h('div', [ h('h1', `Hello ${i} times`), - h('button', {sel: btnSel}, 'Reset'), + h('button', { + sel: btnSel, + className: 'clicker', + domProps: {foo: 3}, + domClass: {hello: true, goodbye: false} + }, 'Reset'), ]), ); @@ -33,4 +39,21 @@ function main(sources) { const App = makeComponent(main); +setModules({ + domProps: { + componentDidUpdate: (element, props) => { + Object.entries(props).forEach(([key, val]) => { + element[key] = val; + }); + } + }, + domClass: { + componentDidUpdate: (element, props) => { + Object.entries(props).forEach(([key, val]) => { + val ? element.classList.add(key) : element.classList.remove(key); + }); + } + } +}) + render(createElement(App), document.getElementById('app')); diff --git a/package.json b/package.json index c0dec0c..6796859 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,13 @@ "@types/mocha": "^5.2.7", "@types/node": "^10.5.2", "@types/react": "16.9.3", + "cypress": "^5.2.0", "mocha": "^6.2.0", + "parcel": "^1.12.3", "react": "16.9.0", "react-dom": "16.9.0", "react-test-renderer": "16.9.0", + "start-server-and-test": "^1.11.3", "symbol-observable": "^1.2.0", "ts-node": "^7.0.0", "typescript": "3.6.3", @@ -46,6 +49,11 @@ "compile": "npm run compile-cjs && npm run compile-es6", "compile-cjs": "tsc --module commonjs --outDir ./lib/cjs", "compile-es6": "echo 'TODO' : tsc --module es6 --outDir ./lib/es6", - "test": "$(npm bin)/mocha test/*.ts --require ts-node/register --recursive" + "full-test": "npm test; npm run cypress:run", + "test": "$(npm bin)/mocha test/*.ts --require ts-node/register --recursive", + "serve-test": "start-server-and-test start http://localhost:1234 full-test", + "start": "parcel example/index.html", + "cypress:open": "cypress open", + "cypress:run": "cypress run" } } diff --git a/src/Incorporator.ts b/src/Incorporator.ts index 71ceb44..cf73a8b 100644 --- a/src/Incorporator.ts +++ b/src/Incorporator.ts @@ -1,4 +1,4 @@ -import {PureComponent, createElement} from 'react'; +import {PureComponent, createElement, createRef} from 'react'; import {Scope} from './scope'; type Props = { @@ -12,11 +12,44 @@ type State = { flip: boolean; }; +let moduleEntries: any = [] + +let onMounts: any[] = [] +let onUpdates: any[] = [] +let onUnmounts: any[] = [] + +export function setModules(mods: any) { + if (mods === null || typeof mods !== 'object') return; + moduleEntries = Object.entries(mods) + onMounts = moduleEntries.map(mod => [mod[0], mod[1].componentDidMount]).filter(mod => mod[1]) + onUpdates = moduleEntries.map(mod => [mod[0], mod[1].componentDidUpdate]).filter(mod => mod[1]) + onUnmounts = moduleEntries.map(mod => [mod[0], mod[1].componentWillUnmount]).filter(mod => mod[1]) +} + +export function hasModuleProps (props) { + return props + ? moduleEntries.some(([mkey]) => props.hasOwnProperty(mkey)) + : false +} + +function moduleProcessor (base, ref, props) { + if (ref && ref.current && base.length) { + base.forEach(([key, f]) => { + f(ref.current, props[key]) + }); + } + +} + export default class Incorporator extends PureComponent { + private ref: any; + private moduleProps: string; + constructor(props: Props) { super(props); this.state = {flip: false}; this.selector = props.targetProps.sel; + this.ref = props.targetRef || (moduleEntries.some(e => Object.keys(props.targetProps).some(key => key === e[0])) ? createRef() : null); } private selector: string | symbol; @@ -26,6 +59,12 @@ export default class Incorporator extends PureComponent { this.unsubscribe = this.props.scope.subscribe(this.selector, () => { this.setState((prev: any) => ({flip: !prev.flip})); }); + + moduleProcessor(onMounts, this.ref, this.props.targetProps) + } + + public componentDidUpdate() { + moduleProcessor(onUpdates, this.ref, this.props.targetProps) } private incorporateHandlers

(props: P, scope: Scope): P { @@ -38,19 +77,21 @@ export default class Incorporator extends PureComponent { } private materializeTargetProps() { - const {targetProps, targetRef, scope} = this.props; + const {targetProps, scope} = this.props; let output = {...targetProps}; output = this.incorporateHandlers(output, scope); - if (targetRef) { - output.ref = targetRef; + if (this.ref) { + output.ref = this.ref; } delete output.sel; + moduleEntries.forEach(pair => delete output[pair[0]]) return output; } public render() { const {target} = this.props; const targetProps = this.materializeTargetProps(); + if (targetProps.children) { return createElement(target, targetProps, targetProps.children); } else { @@ -59,6 +100,8 @@ export default class Incorporator extends PureComponent { } public componentWillUnmount() { + moduleProcessor(onUnmounts, this.ref, this.props.targetProps) + this.unsubscribe(); } } diff --git a/src/h.ts b/src/h.ts index 126be65..5158c6a 100644 --- a/src/h.ts +++ b/src/h.ts @@ -7,6 +7,7 @@ import { Attributes, } from 'react'; import {incorporate} from './incorporate'; +import { hasModuleProps } from './Incorporator'; export type PropsExtensions = { sel?: string | symbol; @@ -32,7 +33,7 @@ function hyperscriptProps

( type: ReactType

| keyof ReactHTML, props: PropsLike

, ): ReactElement

{ - if (!props.sel) { + if (!props.sel && !hasModuleProps(props)) { return createElement(type, props); } else { return createElement(incorporate(type), props); @@ -51,7 +52,7 @@ function hyperscriptPropsChildren

( props: PropsLike

, children: Children, ): ReactElement

{ - if (!props.sel) { + if (!props.sel && !hasModuleProps(props)) { return createElementSpreading(type, props, children); } else { return createElementSpreading(incorporate(type), props, children); diff --git a/src/index.ts b/src/index.ts index 7c0473e..5b1161a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,4 +4,5 @@ export {Scope} from './scope'; export {ReactSource} from './ReactSource'; export {h} from './h'; export {incorporate} from './incorporate'; +export {setModules} from './Incorporator' export {StreamRenderer} from './StreamRenderer';