Skip to content

Commit

Permalink
Enhanced syntactical possibilities
Browse files Browse the repository at this point in the history
  • Loading branch information
mcjazzyfunky committed Dec 21, 2019
1 parent 01481bf commit 3a24f22
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 58 deletions.
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,41 @@ npm run storybook
```jsx
import { h, render } from 'preact'
import { statefulComponent } from 'preactive'
import { useProps, useValue } from 'preactive/hooks'
import { useValue } from 'preactive/hooks'

const counterDefaults = {
initialValue: 0,
label: 'Counter'
}
const Counter = statefulComponent('Counter', (c, props) => {
const
[count, setCount] = useValue(c, props.initialValue || 0),
onIncrement = () => setCount(it => it + 1),
onInput = ev => setCount(ev.currentTarget.valueAsNumber)

return () =>
<div>
<label>{props.label || 'Counter'}: </label>
<input type="number" value={count.value} onInput={onInput} />
<button onClick={onIncrement}>{count.value}</button>
</div>
})

render(<Counter/>, document.getElementById('app'))
```

const Counter = statefulComponent('Counter', c => {
### Alternative syntax

```jsx
import { h, render } from 'preact'
import { statefulComponent } from 'preactive'
import { useValue } from 'preactive/hooks'

const Counter = statefulComponent({
displayName: 'Counter',

defaultProps: {
initialValue: 0,
label: 'Counter'
}
}, (c, props) => {
const
props = useProps(c, counterDefaults),
[count, setCount] = useValue(c, props.initialValue),
onIncrement = () => setCount(it => it + 1),
onInput = ev => setCount(ev.currentTarget.valueAsNumber)
Expand All @@ -64,8 +89,7 @@ methods directly they will only be used internally by some basic
hook and utility functions):

```typescript
type Ctrl<P extends Props = {}> = {
getProps(): P,
type Ctrl = {
isMounted(): boolean,
update(): void,
getContextValue<T>(Context<T>): T,
Expand Down Expand Up @@ -99,7 +123,6 @@ type Context<T> = Preact.Context<T>
### *Package 'preactive/hooks'*
- `useProps(c, defaultProps)`
- `useValue(c, initialValue)`
- `useState(c, initialStateObject)`
- `useContext(c, context)`
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"compression-webpack-plugin": "^3.0.0",
"eslint": "^6.7.1",
"eslint-plugin-react": "^7.17.0",
"js-spec": "^0.1.73",
"js-spec": "^0.1.78",
"lit-html": "^1.1.2",
"terser-webpack-plugin": "^2.2.1",
"uglifyjs-webpack-plugin": "^2.2.0",
Expand Down
169 changes: 161 additions & 8 deletions src/main/core.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,141 @@
import { Component } from 'preact'
import { memo, Component } from 'preact'
import * as Spec from 'js-spec/validators'

// Brrrr, this is horrible as hell - please fix asap!!!!
const
isMinimized = Component.name !== 'Component',
keyContextId = isMinimized ? '__c' : '_id',
keyContextDefaultValue = isMinimized ? '__' : '_defaultValue'

// --- constants -----------------------------------------------------

const
REGEX_DISPLAY_NAME = /^[A-Z][a-zA-Z0-9]*$/

// --- statelessComponent --------------------------------------------

export function statelessComponent(displayName, render) {
const ret = render.bind(null)
export function statelessComponent(arg1, arg2) {
const config = typeof arg1 === 'string'
? { displayName: arg1, init: arg2 }
: typeof arg2 === 'function'
? { ...arg1, init: arg2 }
: arg1

if (process.env.NODE_ENV === 'development') {
let errorMsg

const
type1 = typeof arg1,
type2 = typeof arg2

if (arg1 === null || (type1 !== 'object' && type1 !== 'string')) {
errorMsg = 'First argument must be a string or an object'
} else if (type1 === 'object' && arg2 !== undefined && type2 !== 'function') {
errorMsg = 'Unexpected second argument'
} else if (type1 === 'string' && type2 !== 'function') {
errorMsg = 'Expected function as second argument'
} else {
const error = validateStatelessComponentConfig(config)

if (error) {
errorMsg = error.message
}
}

if (errorMsg) {
const displayName = type1 === 'string'
? arg1
: arg1 && typeof arg1.displayName === 'string'
? arg1.displayName
: ''

throw new TypeError(
'[statelessComponent] Error: '
+ (displayName ? `${displayName} ` : '')
+ errorMessage)
}
}

let ret = config.defaultProps
? props => config.render(Object.assign({}, defaultProps, props)) // TODO - optimize
: config.render.bind(null)

ret.displayName = displayName

if (config.memoize === true) {
// TODO - `memo` is only available in "preact/compat"
// ret = memo(ret)
}

return ret
}

// --- statefulComponent ---------------------------------------------

export function statefulComponent(displayName, init) {
export function statefulComponent(arg1, arg2) {
const config = typeof arg1 === 'string'
? { displayName: arg1, init: arg2 }
: typeof arg2 === 'function'
? { ...arg1, init: arg2 }
: arg1

if (process.env.NODE_ENV === 'development') {
let errorMsg

const
type1 = typeof arg1,
type2 = typeof arg2

if (arg1 === null || (type1 !== 'object' && type1 !== 'string')) {
errorMsg = 'First argument must be a string or an object'
} else if (type1 === 'object' && arg2 !== undefined && type2 !== 'function') {
errorMsg = 'Unexpected second argument'
} else if (type1 === 'string' && type2 !== 'function') {
errorMsg = 'Expected function as second argument'
} else {
const error = validateStatefulComponentConfig(config)

if (error) {
errorMsg = error.message
}
}

if (errorMsg) {
const displayName = type1 === 'string'
? arg1
: arg1 && typeof arg1.displayName === 'string'
? arg1.displayName
: ''

throw new TypeError(
'[statefulComponent] Error '
+ (displayName ? `when defining component "${displayName}" ` : '')
+ '=> ' + errorMsg)
}
}

const
hasDefaultProps =
config.defaultProps && Object.keys(config.defaultProps) > 0,

needsPropObject = config.init.length > 1

const CustomComponent = function (props) {
let mounted = false
let
mounted = false,
oldProps = props

const
propsObject =
!needsPropObject ? null : Object.assign({}, config.defaultProps, props),

afterMountNotifier = createNotifier(),
beforeUpdateNotifier = createNotifier(),
afterUpdateNotifier = createNotifier(),
beforeUnmountNotifier = createNotifier(),
runOnceBeforeUpdateTasks = [],

ctrl = {
getProps: () => this.props,
isMounted: () => mounted,
update: () => this.forceUpdate(),

Expand All @@ -46,7 +153,7 @@ export function statefulComponent(displayName, init) {
runOnceBeforeUpdate: task => runOnceBeforeUpdateTasks.push(task)
},

render = init(ctrl)
render = config.init(ctrl, propsObject)

this.props = props

Expand All @@ -58,7 +165,29 @@ export function statefulComponent(displayName, init) {
this.componentDidUpdate = afterUpdateNotifier.notify
this.componentWillUnmount = beforeUnmountNotifier.notify

if (config.memoize === true) {
this.shouldComponentUpdate = () => false
} else if (typeof config.memoize === 'function') {
// This will follow in a later version
}

this.render = () => {
if (needsPropObject) {
if (this.props !== oldProps) {
oldProps = this.props

for (const key in propsObject) {
delete propsObject[key]
}

if (hasDefaultProps) {
Object.assign(propsObject, config.defaultProps)
}

Object.assign(propsObject, this.props)
}
}

const taskCount = runOnceBeforeUpdateTasks.length

for (let i = 0; i < taskCount; ++i) {
Expand All @@ -77,13 +206,37 @@ export function statefulComponent(displayName, init) {
}

CustomComponent.prototype = Object.create(Component.prototype)
CustomComponent.displayName = displayName
CustomComponent.displayName = config.displayName

return CustomComponent
}

// --- locals --------------------------------------------------------

const validateStatelessComponentConfig =
Spec.exact({
displayName: Spec.match(REGEX_DISPLAY_NAME),
memoize: Spec.optional(Spec.boolean),
validate: Spec.optional(Spec.func),

defaultProps: Spec.optional(Spec.object),
render: Spec.func
})

const validateStatefulComponentConfig =
Spec.exact({
displayName: Spec.match(REGEX_DISPLAY_NAME),
memoize: Spec.optional(Spec.boolean),
validate: Spec.optional(Spec.func),

defaultProps: Spec.optional(Spec.object),
init: Spec.func
})

function hasOnwProp(obj, propName) {
return Object.prototype.hasOwnProperty.call(obj, propName)
}

function createNotifier() {
const subscribers = []

Expand Down
23 changes: 0 additions & 23 deletions src/main/hooks.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,3 @@
// --- useProps ------------------------------------------------------

export function useProps(c, defaultProps = null) {
let oldProps = c.getProps()
const props = Object.assign({}, defaultProps, oldProps)

c.beforeUpdate(() => {
const newProps = c.getProps()

if (newProps !== oldProps) {
oldProps = newProps

for (const key in props) {
delete props[key]
}

Object.assign(props, defaultProps, newProps)
}
})

return props
}

// --- useState ------------------------------------------------------

export function useState(c, initialState) {
Expand Down
Loading

0 comments on commit 3a24f22

Please sign in to comment.