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..ac00c59 --- /dev/null +++ b/cypress/integration/test.spec.js @@ -0,0 +1,39 @@ +/// + +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') + }) + }) + it('Should return element when ref function is sent', () => { + cy.get('h1') + .then(($el) => { + const el = $el[0] + cy.wrap(el.foo).should('eq', 'bar') + }) + }) + }) +}) diff --git a/example/index.js b/example/index.js index 7e18bea..00a2ffb 100644 --- a/example/index.js +++ b/example/index.js @@ -1,7 +1,7 @@ import xs from 'xstream'; import {createElement} from 'react'; import {render} from 'react-dom'; -import {h, makeComponent} from '../src/index'; +import {h, makeComponent, useModules} from '../src/index'; function main(sources) { const init$ = xs.of(() => 0); @@ -19,12 +19,20 @@ function main(sources) { .merge(init$, increment$, reset$) .fold((state, fn) => fn(state)); - const vdom$ = count$.map(i => - h('div', [ - h('h1', `Hello ${i} times`), - h('button', {sel: btnSel}, 'Reset'), - ]), - ); + const getRef = el => { + el.foo='bar'; + } + const vdom$ = count$.map(i => { + return h('div', [ + h('h1', {ref: getRef}, `Hello ${i} times`), + h('button', { + sel: btnSel, + className: 'clicker', + domProps: {foo: 3}, + domClass: {hello: true, goodbye: false} + }, 'Reset') + ]) + }); return { react: vdom$, @@ -33,4 +41,21 @@ function main(sources) { const App = makeComponent(main); +useModules({ + 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 a7c8bad..064072e 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,9 @@ "react": "16.13.1", "react-dom": "16.13.1", "react-test-renderer": "16.13.1", + "cypress": "^5.2.0", + "parcel": "^1.12.3", + "start-server-and-test": "^1.11.3", "symbol-observable": "^1.2.0", "ts-node": "^7.0.0", "typescript": "3.6.3", @@ -53,6 +56,11 @@ "compile-cjs": "tsc --module commonjs --outDir ./lib/cjs", "compile-es6": "echo 'TODO' : tsc --module es6 --outDir ./lib/es6", "prepublishOnly": "npm run compile", - "test": "mocha test/*.ts --require ts-node/register --recursive" + "test": "mocha test/*.ts --require ts-node/register --recursive", + "full-test": "npm test; npm run cypress:run", + "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/Modulizer.ts b/src/Modulizer.ts new file mode 100644 index 0000000..e57997f --- /dev/null +++ b/src/Modulizer.ts @@ -0,0 +1,82 @@ +import {Component, ComponentType, forwardRef, createRef, createElement} from 'react'; +import Incorporator from './Incorporator' + +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 usesModules() { + return moduleEntries.length > 0 +} + +export function hasModuleProps (props) { + return props + ? moduleEntries.some(([mkey]) => props.hasOwnProperty(mkey)) + : false +} + +function moduleProcessor (base, current, props) { + if (current && base.length) { + base.forEach(([key, f]) => { + const prop = props[key] + if (prop) f(current, prop) + }); + } +} + +export class Modulizer extends Component { + private ref: any; + private element: any; + private setRef: any; + constructor(props) { + super(props); + this.element = null + + const {targetProps, targetRef} = props + const useRef = hasModuleProps(targetProps) + if (targetRef) { + if (typeof targetRef === 'function' && useRef) { + this.setRef = element => { + this.element = element; + targetRef(element); + }; + + this.ref = this.setRef; + } else { + this.ref = targetRef; + } + } else { + this.ref = useRef ? createRef() : null; + } + } + + public componentDidMount() { + moduleProcessor(onMounts, this.element || (this.ref && this.ref.current), this.props.targetProps) + } + + public componentDidUpdate() { + moduleProcessor(onUpdates, this.element || (this.ref && this.ref.current), this.props.targetProps) + } + + public componentWillUnmount() { + moduleProcessor(onUnmounts, this.element || (this.ref && this.ref.current), this.props.targetProps) + } + + render() { + const targetProps = {...this.props.targetProps} + moduleEntries.forEach(pair => delete targetProps[pair[0]]) + const output: any = {...this.props, targetRef: this.ref, targetProps}; + + return createElement(Incorporator, output); + } +} diff --git a/src/h.ts b/src/h.ts index d4a51bf..25fa4e2 100644 --- a/src/h.ts +++ b/src/h.ts @@ -6,12 +6,24 @@ import { ReactHTML, Attributes, } from 'react'; -import {incorporate} from './incorporate'; +import {incorporate, setIncorporator} from './incorporate'; +import {setModules, hasModuleProps, Modulizer} from './Modulizer'; +import Incorporator from './Incorporator'; export type PropsExtensions = { sel?: string | symbol; }; +let shouldIncorporate = props => props.sel + +export function useModules(modules: any) { + if (!modules || typeof modules !== 'object') return + + setModules(modules); + shouldIncorporate = props => props.sel || hasModuleProps(props) + setIncorporator(Modulizer) +} + type PropsLike

= P & PropsExtensions & Attributes; type Children = string | Array; @@ -32,7 +44,7 @@ function hyperscriptProps

( type: ElementType

| keyof ReactHTML, props: PropsLike

): ReactElement

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

( props: PropsLike

, children: Children ): ReactElement

{ - if (!props.sel) { + if (!shouldIncorporate(props)) { return createElementSpreading(type, props, children); } else { return createElementSpreading(incorporate(type), props, children); diff --git a/src/incorporate.ts b/src/incorporate.ts index 8556b6f..89d5b20 100644 --- a/src/incorporate.ts +++ b/src/incorporate.ts @@ -1,10 +1,14 @@ import {createElement, forwardRef} from 'react'; import {Scope} from './scope'; import {ScopeContext} from './context'; -import Incorporator from './Incorporator'; +import {default as defaultIncorporator} from './Incorporator' -const wrapperComponents: Map> = new Map(); +let Incorporator = defaultIncorporator +export function setIncorporator(f: any) { + Incorporator = f +} +const wrapperComponents: Map> = new Map(); export function incorporate(type: any) { if (!wrapperComponents.has(type)) { wrapperComponents.set( diff --git a/src/index.ts b/src/index.ts index 7c0473e..4749d99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,6 @@ export {makeComponent, makeCycleReactComponent} from './convert'; export {ScopeContext} from './context'; export {Scope} from './scope'; export {ReactSource} from './ReactSource'; -export {h} from './h'; +export {h, useModules} from './h'; export {incorporate} from './incorporate'; export {StreamRenderer} from './StreamRenderer';