Skip to content

Commit

Permalink
Merge pull request #8 from sc0ttj/validator
Browse files Browse the repository at this point in the history
added state validation:
  - requires @scottjarvis/validator
  - pass in schema as 2nd param when creating a new component
  - setState will throw an Error if state to set is not valid
  - updated README etc
  • Loading branch information
sc0ttj committed Jul 26, 2020
2 parents 2df13f2 + 15d68a6 commit 95477b5
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 61 deletions.
98 changes: 67 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,6 @@ Middleware functions can be re-used across lots of components and, unlike "actio
Here's how to use "middleware" functions to customise your components `setState()` behaviour:

1. Define some "middleware" functions - these will be called at the end of `setState()`
- Note that "props" will contain the latest state (that was just set)
2. Add your "middleware" to a component as an array of functions:

```js
Expand Down Expand Up @@ -309,28 +308,16 @@ var htmlView = props => `
// or return the state itself (pure headless component)
var dataOnlyView = props => props

// or a custom JS object view
var jsonView = props => {
return { count: props.count, items: props.items }
}

// Choose a view to render
App.view = htmlView


// render the component
App.render()


// ..other rendering options...

// print it to the terminal
console.log(App.render())

// Or render the component as an HTTP response,
// using something like express.js
res.send(App.render())

```

If rendering a component in NodeJS that has a `.view()` and `.style()`, or if calling `.toString()` directly, the output will be a string like this one:
Expand Down Expand Up @@ -406,10 +393,6 @@ var state = {
}
var App = new Component(state)

// Define some other component
var state2 = { foo: "bar" }
var Foo = new Component(state2)

// Define chainable "actions", to update the state more easily
App.actions({
plus: props => App.setState({ count: App.state.count + props }),
Expand All @@ -419,37 +402,27 @@ App.actions({
addItems: props => App.setState({ items: [...App.state.items, ...props] })
})

// Define some other component
var Foo = new Component({ foo: "bar" })

// Listen for the actions above:
// - Log the first time the "minus" action is called
// - Log every time the "plus" and "addItems" actions are called
Foo
.once("minus", props => console.log("Foo: action 'minus'", props.count))
.on("plus", props => console.log("Foo: action 'plus'", props.count))
.on("addItems", props => console.log("Foo: action 'addItems'", props.items))


// ...now we're ready to run the program..

// Log initial state
console.log(App.render())

// If you defined some "actions", you can use them
// to update specific parts of your state
App.plus(105)
App.minus(5)

// stop listening to the "plus" action
Foo.off("plus")

// A components "actions" can be chained
App.minus(1)
.minus(1)
.plus(3)
.addItems([{ name: "two" }, { name: "three" }])


// Log final app state
console.log(App.render())
```

Also see [examples/usage-emitter.js](examples/usage-emitter.js)
Expand Down Expand Up @@ -566,7 +539,7 @@ The `tweenProps` object returned to callbacks provides the tweening values of th

Also see [examples/usage-tweenState.js](examples/usage-tweenState.js)

## Using JSON-LD (linked data)
### Using JSON-LD (linked data)

Adding linked data to your components is easy - just define it as part of your view:

Expand All @@ -581,9 +554,72 @@ App.view = props => `
- use the `props` passed in to define/update whatever you need
- you JSON-LD will be updated along with your view whenever your component re-renders

### State validation

You can validate your component state against a schema, before you set it or render anything.

This is a similar concept to `propTypes` in React, but simpler and dumber.

First, you must install a tiny (~300 bytes) additional dependency:

```js
npm i @scottjarvis/validator
```

Then enable it:

```js
var { Component } = require("@scottjarvis/component")
Component.validator = require("@scottjarvis/validator")
```

Define a schema object. For each property included, the value should be:

- a `typeof` type name, as a string
- or a validator function, that returns true or false

Then create your component, passing in the schema as the second parameter.

```js
var state = {
count: 0,
age: 20
items: [ "one", "two" ],
foo: {
bar: "whatever"
}
}

var schema = {
count: "number",
age: age => typeof age === "number" && age > 17
items: "array",
foo: {
bar: "string"
}
}

var App = new Component(state, schema)
```

If you try to set an invalid state, your component will `throw` an error:

```js
App.setState ({ count: "foo" }) // this will throw an Error!
```

See [`@scottjarvis/validator`](https://github.com/sc0ttj/validator) for more usage info.

## Changelog

**1.1.12**
- new feature: state validation
- simply pass a schema as the 2nd param when creating a new component
- then call `setState()` as usual - Component will throw an error if the state isn't valid
- requires `@scottjarvis/validator` to be installed (~330 bytes minified & gzipped)
- see [`@scottjarvis/validator`](https://github.com/sc0ttj/validator) for more usage info
- updated README

**1.1.11**
- updated `rollup` deps and rebuilt in `dist/`
- nothing else
Expand Down
2 changes: 1 addition & 1 deletion dist/component.min.js

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

Binary file modified dist/component.min.js.gz
Binary file not shown.
Binary file modified dist/emitter.min.js.gz
Binary file not shown.
Binary file modified dist/index.min.js.gz
Binary file not shown.
Binary file modified dist/tweenState.min.js.gz
Binary file not shown.
68 changes: 42 additions & 26 deletions examples/usage-in-node.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var { Component } = require("../dist/index.min.js")
Component.validator = require("@scottjarvis/validator")

// ------------------------------------------------
// USAGE: Defining components
Expand All @@ -10,7 +11,13 @@ var state = {
incrementBy: 5,
id: "foo-id",
items: [{ name: "Item one" }, { name: "Item two" }],
btnColor: "green"
btnColor: "green",
foo: {
bar: 0,
bob: {
ppp: "foobar"
}
}
}

// Define some generic, re-usable, stateless "sub-components"
Expand All @@ -19,17 +26,24 @@ var Heading = text => `<h1>${text}</h1>`
var List = items =>
`<ul>${items.map(item => `<li>${item.name}</li>`).join("")}</ul>`

var Button = (label, fn) =>
`<button class="btn" onclick=${fn}>${label}</button>`

// Define a stateful main component
var App = new Component(state)

// OPTIONAL: call only 'setState()' instead of 'App.setState()'
var setState = App.setState
// OPTIONAL: Define the schema against which to validate state updates
var schema = {
count: "number",
incrementBy: "number",
id: "string",
items: "array",
btnColor: "string",
foo: {
bar: "number",
bob: {
ppp: "string"
}
}
}

// OPTIONAL: Define some events
clickBtn = props => setState({ count: App.state.count + props })
// Define a stateful main component: pass in
// a schema as an optional, second param
var App = new Component(state, schema)

// IMPORTANT: Define a view - a function which receives the
// state as `props` and returns the view as a string or JSON
Expand All @@ -39,7 +53,6 @@ var htmlView = props => `
<div id=${props.id}>
${Heading("Total so far = " + props.count)}
${List(props.items)}
${Button("Click here", `clickBtn(${props.incrementBy})`)}
</div>`

// ...or a JS object view
Expand All @@ -57,6 +70,9 @@ App.view = dataOnlyView
// OPTIONAL: Define chainable "actions", to update the state more easily
// ------------------------------------------------

// OPTIONAL: call only 'setState()' instead of 'App.setState()'
var setState = App.setState

// These "actions" are like regular methods, except
// they're always chainable and tagged by name in
// your components state history
Expand All @@ -83,18 +99,18 @@ App.actions({

// Define some "middleware" functions - these are called right after setState().
// Note that "props" will contain the latest state (that was just set)
var countLog = props => console.log("middleware -> count logger", props.count)
var itemsLog = props => console.log("middleware -> items logger", props.items)
var countLog = props => console.log("middleware -> count logger: ", props.count)
var itemsLog = props => console.log("middleware -> items logger: ", props.items)

// Add your "middleware" to a component as an array of functions
App.middleware = [countLog, itemsLog]

// ...now, every time setState() is called, each middleware function
// ...now every time setState() is called, each middleware function
// in the array will run too.

//
// ------------------------------------------------
// USAGE: Using the component
// USAGE: Using the component (setting state)
// ------------------------------------------------

//
Expand All @@ -105,9 +121,6 @@ App.middleware = [countLog, itemsLog]
// Using setState() to trigger a full re-render
setState({ items: [{ name: "First" }] })

// ...Or directly call any methods you defined
clickBtn(1)

// If you defined some "actions", you can use them
// to update specific parts of your state
App.plus(105)
Expand Down Expand Up @@ -139,18 +152,21 @@ App.setState(App.log[0].state)
App.setState(snapshot)

//
// ------------------------------------------------
// OPTIONAL: State validation:
// ------------------------------------------------
//
//
// Now return the component...
// Uncomment below to cause an error, as "count" should be a number!
// setState({ count: "one" })

//
// ------------------------------------------------
// Now return or render the component:
// ------------------------------------------------
//
// Returns the current view (and any App styles you
// defined) as a string, or a JS object
//
// (for demo purposes we console log it, so you can see
// the component rendered to the terminal)
console.log(App.render())

// Or render the component as an HTTP response,
// using something like express.js:
//
// res.send(App.render())
Loading

0 comments on commit 95477b5

Please sign in to comment.