Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for adding dom element properties #11

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"baseUrl": "http://localhost:1234",
"chromeWebSecurity": false,
"defaultCommandTimeout": 10000,
"modifyObstructiveCode": false,
"video": false,
"fixturesFolder": false
}
39 changes: 39 additions & 0 deletions cypress/integration/test.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// <reference types="cypress" />

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')
})
})
})
})
39 changes: 32 additions & 7 deletions example/index.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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$,
Expand All @@ -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'));
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
82 changes: 82 additions & 0 deletions src/Modulizer.ts
Original file line number Diff line number Diff line change
@@ -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<any, any> {
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);
}
}
18 changes: 15 additions & 3 deletions src/h.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = P & PropsExtensions & Attributes;

type Children = string | Array<ReactNode>;
Expand All @@ -32,7 +44,7 @@ function hyperscriptProps<P = any>(
type: ElementType<P> | keyof ReactHTML,
props: PropsLike<P>
): ReactElement<P> {
if (!props.sel) {
if (!shouldIncorporate(props)) {
return createElement(type, props);
} else {
return createElement(incorporate(type), props);
Expand All @@ -51,7 +63,7 @@ function hyperscriptPropsChildren<P = any>(
props: PropsLike<P>,
children: Children
): ReactElement<P> {
if (!props.sel) {
if (!shouldIncorporate(props)) {
return createElementSpreading(type, props, children);
} else {
return createElementSpreading(incorporate(type), props, children);
Expand Down
8 changes: 6 additions & 2 deletions src/incorporate.ts
Original file line number Diff line number Diff line change
@@ -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<any, React.ComponentType<any>> = new Map();
let Incorporator = defaultIncorporator
export function setIncorporator(f: any) {
Incorporator = f
}

const wrapperComponents: Map<any, React.ComponentType<any>> = new Map();
export function incorporate(type: any) {
if (!wrapperComponents.has(type)) {
wrapperComponents.set(
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';