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

Parity with recompose #24

Open
wants to merge 4 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
47 changes: 24 additions & 23 deletions demo/demos/ContextDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import * as React from 'react';
import { fromRenderProp } from '../../src/ChainableComponent';
// how to create a context hoc?
import Step from '../Step';
import { withContext } from '../../src/lib/withContext';
import { withProvider } from '../../src/lib/withProvider';

const { Consumer, Provider } = React.createContext("Default Value");

const withContext = fromRenderProp(Consumer);

const DisplayContext =
withContext.render(
withContext(Consumer).render(
context => {
return (
<span>
Expand All @@ -19,40 +17,43 @@ const DisplayContext =
}
);

const withText = withProvider(Provider)

const ContextDemo = () => (
<div>
{DisplayContext}
<Provider value="Overriden value">
{DisplayContext}
</Provider>
{withText("From withProvider").render(() => DisplayContext)}
</div>
);

export default () => (
<Step title="React 16 Context Demo">
<pre className='code-sample'>
{`import { fromRenderProp } from 'chainable-components';
const { Consumer, Provider } = React.createContext("Default Value");
const withStringContext = fromRenderProp(Consumer);
{`const DisplayContext =
withContext(Consumer).render(
context => {
return (
<span>
Current context is:
<pre>{JSON.stringify(context, null, 2)}</pre>
</span>
);
}
);

const DisplayContext =
withStringContext({ children: a => '' }).render(
context => {
return (
<span>
Current context is:
<pre>{JSON.stringify(context, null, 2)}</pre>
</span>
);
}
const withText = withProvider(Provider)

const ContextDemo = () => (
<div>
{DisplayContext}
<Provider value="Overriden value">
<div>
{DisplayContext}
</Provider>
</div>
<Provider value="Overriden value">
{DisplayContext}
</Provider>
{withText("From withProvider").render(() => DisplayContext)}
</div>
);`}
</pre>
<ContextDemo />
Expand Down
32 changes: 32 additions & 0 deletions demo/demos/WithHandlersDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';
import { withState } from '../../src/lib/withState';
import { ChainableComponent } from '../../src/ChainableComponent';
import Step from '../Step';
import { DoBuilder } from '../../src';
import { withHandler } from '../../src/lib/withHandler';

export const WithHandlersDemo =
DoBuilder
.bind('b', withState(0))
.bind('a', withState(0))
.bindL('handler', ({a, b}) => {
console.log('handler', a.value, b.value)
return withHandler(() => alert(`A's count is : ${a.value}`), [b.value])
})
.done()
.render(({a, b, handler}) => (
<div>
<div>a: {a.value} <button onClick={() => a.update(a.value + 1)}>+</button></div>
<div>b: {b.value} <button onClick={() => b.update(b.value + 1)}>+</button></div>
<button onClick={handler}>alert</button>
</div>
))

export default () => (
<Step title="WithHandlersDemo Demo">
<pre className='code-sample'>
{``}
</pre>
{WithHandlersDemo}
</Step>
);
39 changes: 39 additions & 0 deletions demo/demos/WithReducerDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import {
ChainableComponent,
fork
} from '../../src/ChainableComponent';
import Step from '../Step';
import { withState } from '../../src';
import { WithStateContext } from '../../src/lib/withState';
import { withReducer } from '../../src/lib/withReducer';

type Action = 'increment' | 'decrement';

export const WithReducerDemo =
withReducer((s: number, a: Action) => {
return a === 'increment' ? s + 1 : s -1;
}, 0).render(({state, dispatch}) => (
<div>
<button onClick={() => dispatch('decrement')}>-</button>
{state}
<button onClick={() => dispatch('increment')}>+</button>
</div>
))

export default () => (
<Step title="Fork Demo">
<pre className="code-sample">
{`withReducer((s: number, a: Action) => {
return a === 'increment' ? s + 1 : s -1;
}, 0).render(({state, dispatch}) => (
<div>
<button onClick={() => dispatch('decrement')}>-</button>
{state}
<button onClick={() => dispatch('increment')}>+</button>
</div>
))`}
</pre>
{WithReducerDemo}
</Step>
);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"ramda": "^0.25.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-hot-loader": "3.0.0",
"react-hot-loader": "^4.8.4",
"react-router": "^4.2.0",
"recompose": "^0.28.2",
"source-map-loader": "^0.2.2",
Expand Down
37 changes: 37 additions & 0 deletions src/Dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

type Dependency<A> = {
_type: '@@ChainableComponentsDependency',
value: A,
equals: (l: A, r: A) => boolean
}

function isDep<T>(a: any): a is Dependency<T> {
return a.type === '@@ChainableComponentsDependency'
}

type Dependencies = (any | Dependency<any>)[]

export function equals(l: Dependencies, r: Dependencies): boolean {
return l.every((dep, i) => {
const rightDep = r[i]
if(isDep(dep)) {
if(isDep(rightDep)) {
return dep.equals(dep.value, rightDep.value)
} else {
return false;
}
} else {
return dep === rightDep;
}
})
}

export function itemEquals(l: any, r: any): boolean {
if(l && isDep(l) ) {
}
return l.equals(l.value, r.value)
}

export function depEquals<A>(l: Dependency<A>, r: Dependency<A>): boolean {
return l.equals(l.value, r.value)
}
6 changes: 6 additions & 0 deletions src/lib/withContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Consumer } from 'react';
import { ChainableComponent, fromRenderProp, RenderPropsProps } from '../ChainableComponent';

export function withContext<A>(consumer: Consumer<A>): ChainableComponent<A> {
return fromRenderProp<A>(consumer)
}
14 changes: 14 additions & 0 deletions src/lib/withHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ChainableComponent } from '../ChainableComponent';
import { withLifecycle } from './withLifecycle';
import { equals } from '../Dependencies';

export function withHandler<F extends Function>(
cb: F,
dependencies: any[]
): ChainableComponent<F> {
return withLifecycle<[F, any[]]>({
init: () => [cb, dependencies],
deriveContext: ([f, deps]) =>
equals(deps, dependencies) ? [f, deps] : [cb, dependencies]
}).map(a => a[0]);
}
54 changes: 42 additions & 12 deletions src/lib/withLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,69 @@ import {
* Configuration for the withLifecycle chainable component
*/
export type WithLifecycleOptions<C> = {
componentDidMount: () => C;
componentWillUnmount?: (c: C) => void;
init: () => C;
componentDidMount?: (c: C) => C;
componentWillUnmount?: (c: C) => C;
deriveContext?: (c:C) => C;
componentWillUpdate?: (c: C) => C;
shouldComponentUpdate?: (c: C) => boolean;
};

type WithLifecycleProps<C> = RenderPropsProps<WithLifecycleOptions<C>, {}>;
type WithLifecycleProps<C> = RenderPropsProps<WithLifecycleOptions<C>, C>;

const iff = <A>(a: A | undefined, f: (a:A) => void): void => a && f(a)

const fold = <A, Z>(z: Z, a: A | undefined, f: (a:A) => Z): Z => a ? f(a) : z

/**
* A Render Prop component that mimics the react Component API
*/
export class WithLifecycle<C> extends React.Component<
WithLifecycleProps<C>,
{}
{ context: C }
> {
constructor(props: WithLifecycleProps<C>) {
super(props);
this.state = {
context: this.props.init()
}
}

static getDerivedStateFromProps<C>(nextProps: WithLifecycleProps<C>, state: { context: C }) {
return nextProps.deriveContext ? { context: nextProps.deriveContext(state.context) } : state
}

componentDidMount() {
this.context = this.props.componentDidMount();
iff(this.props.componentDidMount, dm => {
this.setState(s => {
context: dm(s.context)
})
})
}

componentWillUnmount() {
this.props.componentWillUnmount &&
this.props.componentWillUnmount(this.context);
iff(this.props.componentWillUnmount, wu => {
this.setState(s => {
context: wu(s.context)
})
})
}
shouldComponentUpdate() {
return this.props.shouldComponentUpdate
? this.props.shouldComponentUpdate(this.context)
: true;
return fold(true, this.props.shouldComponentUpdate, scu => {
return scu(this.state.context)
})
}

UNSAFE_componentWillUpdate(nextProps: WithLifecycleProps<C>) {
iff(nextProps.componentWillUpdate, wu => {
this.setState(s => {
context: wu(s.context)
})
})
}

render() {
return this.props.children({});
return this.props.children(this.state.context);
}
}

Expand All @@ -52,6 +82,6 @@ export class WithLifecycle<C> extends React.Component<
*/
export function withLifecycle<C>(
options: WithLifecycleOptions<C>
): ChainableComponent<{}> {
): ChainableComponent<C> {
return fromRenderProp<WithLifecycleProps<C>>(WithLifecycle, options);
}
6 changes: 6 additions & 0 deletions src/lib/withProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Provider, createElement } from 'react';
import { ChainableComponent, fromRender } from '../ChainableComponent';

export function withProvider<A>(provider: Provider<A>): (value: A) => ChainableComponent<A> {
return a => fromRender<A>(f => createElement(provider, {value: a}, f(a)))
}
55 changes: 55 additions & 0 deletions src/lib/withReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from 'react';
import { ChainableComponent, fromRenderProp, RenderPropsProps } from '../ChainableComponent';

/**
* The state used by the WithState render prop.
*/
export type WithReducerState<S> = {
value: S,
};

/**
* Stores the state value and provides a function that updates the state.
*/
export type WithReducerContext<S, A> = {
state: S,
dispatch: (a: A) => void,
};

/**
* The options to pass
*/
export type WithReducerOptions<S, A> = {
initialState: S,
reducer: (state: S, action: A) => S
};

export type WithReducerProps<S, A> = RenderPropsProps<WithReducerOptions<S, A>, WithReducerContext<S, A>>;

export class WithReducer<S, A> extends React.Component<WithReducerProps<S, A>, WithReducerState<S>> {
state: WithReducerState<S> = {
value: this.props.initialState,
};

constructor(props: WithReducerProps<S, A>) {
super(props);
this.dispatch = this.dispatch.bind(this);
}

dispatch(a: A) {
this.setState(state => ({
value: this.props.reducer(state.value, a)
}))
}

render() {
return this.props.children({
state: this.state.value,
dispatch: this.dispatch,
});
}
}

export function withReducer<S, A>(reducer: (state: S, action: A) => S, initialState: S): ChainableComponent<WithReducerContext<S, A>> {
return fromRenderProp<WithReducerProps<S, A>>(WithReducer, {reducer, initialState});
}
Loading