Skip to content

Instantly share code, notes, and snippets.

@selfup
Created July 7, 2017 02:49
Show Gist options
  • Select an option

  • Save selfup/b484075a25b6608a42f5d05a38c136cd to your computer and use it in GitHub Desktop.

Select an option

Save selfup/b484075a25b6608a42f5d05a38c136cd to your computer and use it in GitHub Desktop.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..42f7332
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [email protected]. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/docs/README.md b/docs/README.md
index cc36fea..9d4a9d3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -15,7 +15,7 @@ We assume you have some knowledge of HTML and JavaScript. If you are completely
- [View and State](/docs/core.md#view-and-state)
- [Actions](/docs/core.md#actions)
- [Events](/docs/core.md#events)
- - [Plugins](/docs/core.md#plugins)
+ - [Mixins](/docs/core.md#mixins)
- [Keys](/docs/keys.md)
- [Custom Tags](/docs/custom-tags.md)
- [Lifecycle Events](/docs/lifecycle-events.md)
diff --git a/docs/api.md b/docs/api.md
index 0cede47..40d16b8 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -10,7 +10,7 @@
* [action](#action)
* [update](#update)
* [render](#render)
- * [props.plugins](#plugins)
+ * [props.mixins](#mixins)
* [props.root](#root)
* [emit](#emit)
@@ -22,7 +22,7 @@ Type: ([tag](#h-tag), [data](#h-data), [children](#h-children)): [vnode]
* <a name="h-tag"></a>tag: string | ([props](#h-data), [children](#h-children)): [vnode]
* <a name="h-data"></a>data: {}
-* <a name="h-children"></a>children: string | [vnode]\[\]
+* <a name="h-children"></a>children: string | Array\<[vnode]\>
## app
@@ -33,7 +33,7 @@ Type: ([props](#app-props))
* [view](#view)
* [actions](#actions)
* [events](#events)
- * [plugins](#plugins)
+ * [mixins](#mixins)
* [root](#root)
### state
@@ -54,13 +54,13 @@ Type: ([state](#state), [actions](#actions), [data](#actions-data), [emit](#emit
### events
#### loaded
-Type: ([state](#state), [actions](#actions), _, [emit](#emit)) | [events](#loaded)\[\]
+Type: ([state](#state), [actions](#actions), _, [emit](#emit)) | Array\<[events](#loaded)\>
Fired after the view is mounted on the DOM.
#### action
-Type: ([state](#state), [actions](#actions), [data](#action-data), [emit](#emit)): [data](#action-data) | [action](#action)\[\]
+Type: ([state](#state), [actions](#actions), [data](#action-data), [emit](#emit)): [data](#action-data) | Array\<[action](#action)\>
* <a name="action-data"></a>data
* name: string
@@ -70,7 +70,7 @@ Fired before an action is triggered.
#### update
-Type: ([state](#state), [actions](#actions), [data](#update-data), [emit](#emit)): [data](#update-data) | [update](#update)\[\]
+Type: ([state](#state), [actions](#actions), [data](#update-data), [emit](#emit)): [data](#update-data) | Array\<[update](#update)\>
* <a name="update-data"></a>data: the updated fragment of the state.
@@ -78,19 +78,20 @@ Fired before the state is updated.
#### render
-Type: ([state](#state), [actions](#actions), [view](#view), [emit](#emit)): [view](#view) | [render](#render)\[\]
+Type: ([state](#state), [actions](#actions), [view](#view), [emit](#emit)): [view](#view) | Array\<[render](#render)\>
Fired before the view is rendered.
-### plugins
+### mixins
-Type: [Plugin](#plugins-plugin)\[\]
+Type: Array\<[Mixin](#mixins-mixin)\>
-#### <a name="plugins-plugin"></a>Plugin
+#### <a name="mixins-mixin"></a>Mixin
-Type: ([props](#app-props)): [props](#plugin-props)
+Type: ([props](#app-props)): [props](#mixin-props)
-* <a name="plugin-props"></a>props
+* <a name="mixin-props"></a>props
+ * [mixins](#mixins)
* [state](#state)
* [actions](#actions)
* [events](#events)
diff --git a/docs/core.md b/docs/core.md
index 4e7d434..b33e03a 100644
--- a/docs/core.md
+++ b/docs/core.md
@@ -8,7 +8,7 @@
* [Namespaces](#namespaces)
* [Events](#events)
* [Custom Events](#custom-events)
- * [Plugins](#plugins)
+ * [Mixins](#mixins)
## Virtual Nodes
@@ -64,7 +64,7 @@ data: {
}
```
-Attributes also include [lifecycle events](/docs/lifecycle-events.md) and meta data such as [keys](#/docs/keys.md).
+Attributes also include [lifecycle events](/docs/lifecycle-events.md) and meta data such as [keys](/docs/keys.md).
## Applications
@@ -292,9 +292,9 @@ app({
})
```
-### Plugins
+### Mixins
-Use [plugins](/docs/api.md#events) to extend your application state, actions and events in a modular fashion.
+Use [mixins](/docs/api.md#mixins) to extend your application state, actions and events in a modular fashion.
```jsx
const Logger = () => ({
@@ -313,6 +313,32 @@ app({
actions: {
addOne: state => state + 1
},
- plugins: [Logger]
+ mixins: [Logger]
})
```
+
+Mixins can also compose with other mixins:
+
+```js
+const Counter = () => ({
+ mixins: [Logger],
+ state: {
+ count: 0
+ },
+ actions: {
+ up: state => ({ count: state.count + 1 }),
+ down: state => ({ count: state.count + 1 })
+ }
+})
+
+app({
+ mixins: [Counter],
+ view: state =>
+ <div class="counter">
+ <button onclick={actions.up}>+</button>
+ <span>{state.count}</span>
+ <button onclick={actions.down}>-</button>
+ </div>
+})
+```
+
diff --git a/docs/index.md b/docs/index.md
index 292a328..244b82a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -67,7 +67,7 @@ Below is an alphabetical list of some of the terms and concepts used throughout
##### P
[state.router.params](/docs/routing.md#params)<br>
-[plugins](/docs/core.md#plugins)<br>
+[mixins](/docs/core.md#mixins)<br>
[popstate](https://developer.mozilla.org/en-US/docs/Web/Events/popstate)<br>
[promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<br>
[pragma](https://babeljs.io/docs/plugins/transform-react-jsx/#optionspragma)<br>
diff --git a/docs/lifecycle-events.md b/docs/lifecycle-events.md
index fb18f7a..691c084 100644
--- a/docs/lifecycle-events.md
+++ b/docs/lifecycle-events.md
@@ -25,7 +25,9 @@ Fired before the element is removed from the DOM.
Note that when using this event you are responsible for removing the element yourself.
```js
-element.parent.removeChild(element)
+if (element.parentNode) {
+ element.parentNode.removeChild(element);
+}
```
## Example
diff --git a/docs/routing.md b/docs/routing.md
index b53718d..d2ad0ac 100644
--- a/docs/routing.md
+++ b/docs/routing.md
@@ -12,31 +12,33 @@
## Usage
-To add routing to your application, use the Router plugin.
+To add routing to your application, use the Router mixin.
```jsx
import { Router } from "hyperapp"
```
-The router treats the view as an object of key/value pairs where the key is a route, e.g. <samp>*</samp>, <samp>/home</samp> etc., and the value is the corresponding [view](/docs/api.md#view) function.
+The router treats the view as an array of route/view pairs.
```jsx
app({
- view: {
- "*": state => <h1>404</h1>,
- "/": state => <h1>Hi.</h1>
- },
- plugins: [Router]
+ view: [
+ ["/", state => <h1>Hi.</h1>]
+ ["*", state => <h1>404</h1>],
+ ],
+ mixins: [Router]
})
```
-When the page loads or the browser fires a [popstate](https://developer.mozilla.org/en-US/docs/Web/Events/popstate) event, the view whose key/route matches [location.pathname](https://developer.mozilla.org/en-US/docs/Web/API/Location) will be rendered. If there is no match, <samp>*</samp> is used as a fallback.
+When the page loads or the browser fires a [popstate](https://developer.mozilla.org/en-US/docs/Web/Events/popstate) event, the first route that matches [location.pathname](https://developer.mozilla.org/en-US/docs/Web/API/Location) will be rendered.
+
+Routes are matched in the order in which they are declared. To use the wildcard <samp>*</samp> correctly, it must be declared last.
|route | location.pathname |
|-------------------------|-----------------------------------|
-| <samp>*</samp> | Match if no other route matches.
| <samp>/</samp> | <samp>/</samp>
| <samp>/:foo</samp> | Match <samp>[A-Za-z0-9]+</samp>. See [params](#params).
+| <samp>*</samp> | Match anything.
To navigate to a different route use [actions.router.go](#go).
@@ -51,7 +53,7 @@ The matched route params.
|route |location.pathname |state.router.params |
|----------------------|---------------------|---------------------|
-|<samp>/:foo</samp> |<samp>/hyper</samp> | { foo: "hyper" } |
+|<samp>/:foo</samp> |/hyper | { foo: "hyper" } |
#### match
@@ -70,20 +72,10 @@ Update [location.pathname](https://developer.mozilla.org/en-US/docs/Web/API/Loca
### events
#### route
-Type: ([state](/docs/api.md#state), [actions](/docs/api.md#actions), [data](#events-data), [emit](/docs/api.md#emit)) | [route](#route)\[\]
+Type: ([state](/docs/api.md#state), [actions](/docs/api.md#actions), [data](#events-data), [emit](/docs/api.md#emit)) | Array\<[route](#route)\>
* <a name="events-data"></a>data
* [params](#params)
* [match](#match)
Fired when a route is matched.
-
-
-
-
-
-
-
-
-
-
diff --git a/package.json b/package.json
index 8312c76..53926d0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "hyperapp",
"description": "1kb JavaScript library for building frontend applications.",
- "version": "0.9.2",
+ "version": "0.9.3",
"main": "dist/hyperapp.js",
"jsnext:main": "src/index.js",
"module": "src/index.js",
diff --git a/src/app.js b/src/app.js
index 68a8fd5..e1dbdfc 100644
--- a/src/app.js
+++ b/src/app.js
@@ -6,17 +6,21 @@ export default function(app) {
var node
var element
- for (var i = -1, plugins = app.plugins || []; i < plugins.length; i++) {
- var plugin = plugins[i] ? plugins[i](app) : app
+ for (var i = -1, mixins = app.mixins || []; i < mixins.length; i++) {
+ var mixin = mixins[i] ? mixins[i](app) : app
- if (plugin.state != null) {
- state = merge(state, plugin.state)
+ if (mixin.mixins != null && mixin !== app) {
+ mixins = mixins.concat(mixin.mixins)
}
- init(actions, plugin.actions)
+ if (mixin.state != null) {
+ state = merge(state, mixin.state)
+ }
+
+ init(actions, mixin.actions)
- Object.keys(plugin.events || []).map(function(key) {
- events[key] = (events[key] || []).concat(plugin.events[key])
+ Object.keys(mixin.events || []).map(function(key) {
+ events[key] = (events[key] || []).concat(mixin.events[key])
})
}
diff --git a/src/router.js b/src/router.js
index dfcaef8..16a28c1 100644
--- a/src/router.js
+++ b/src/router.js
@@ -1,4 +1,4 @@
-export default function(app) {
+export default function(app, view) {
return {
state: {
router: match(location.pathname)
@@ -20,48 +20,50 @@ export default function(app) {
loaded: function(state, actions) {
match()
addEventListener("popstate", match)
+
function match() {
actions.router.match(location.pathname)
}
},
- render: function(state, actions, view) {
- return view[state.router.match]
+ render: function() {
+ return view
}
}
}
function match(data) {
- var match
- var params = {}
-
- for (var route in app.view) {
+ for (var match, params = {}, i = 0, len = app.view.length; i < len; i++) {
+ var route = app.view[i][0]
var keys = []
- if (!match && route !== "*") {
+ if (!match) {
data.replace(
RegExp(
- "^" +
- route
- .replace(/\//g, "\\/")
- .replace(/:([\w]+)/g, function(_, key) {
- keys.push(key)
- return "([-\\w]+)"
- }) +
- "/?$",
+ route === "*"
+ ? "." + route
+ : "^" +
+ route
+ .replace(/\//g, "\\/")
+ .replace(/:([\w]+)/g, function(_, key) {
+ keys.push(key)
+ return "([-\\.\\w]+)"
+ }) +
+ "/?$",
"g"
),
function() {
- for (var i = 1; i < arguments.length - 2; ) {
- params[keys.shift()] = arguments[i++]
+ for (var j = 1; j < arguments.length - 2; ) {
+ params[keys.shift()] = arguments[j++]
}
match = route
+ view = app.view[i][1]
}
)
}
}
return {
- match: match || "*",
+ match: match,
params: params
}
}
diff --git a/test/plugins.js b/test/mixins.test.js
similarity index 79%
rename from test/plugins.js
rename to test/mixins.test.js
index f6a3860..190d26a 100644
--- a/test/plugins.js
+++ b/test/mixins.test.js
@@ -4,7 +4,7 @@ import { expectHTMLToBe } from "./util"
beforeEach(() => (document.body.innerHTML = ""))
test("extend the state", () => {
- const plugin = app => ({
+ const mixin = app => ({
state: {
bar: app.state.foo
}
@@ -23,7 +23,7 @@ test("extend the state", () => {
})
}
},
- plugins: [plugin]
+ mixins: [mixin]
})
})
@@ -47,12 +47,12 @@ test("extend events", () => {
events: {
loaded: _ => expect(++count).toBe(1)
},
- plugins: [A, B]
+ mixins: [A, B]
})
})
test("extend actions", () => {
- const plugin = app => ({
+ const mixin = app => ({
actions: {
foo: {
bar: {
@@ -84,12 +84,12 @@ test("extend actions", () => {
`
}
},
- plugins: [plugin]
+ mixins: [mixin]
})
})
test("don't overwrite actions in the same namespace", () => {
- const plugin = app => ({
+ const mixin = app => ({
actions: {
foo: {
bar: {
@@ -122,6 +122,32 @@ test("don't overwrite actions in the same namespace", () => {
(state, actions) => actions.foo.bar.qux("foo.bar.qux")
]
},
- plugins: [plugin]
+ mixins: [mixin]
+ })
+})
+
+test('mixin inside of a mixin', () => {
+ const A = () => ({
+ state: {
+ foo: 1
+ }
+ })
+
+ const B = () => ({
+ mixins: [A],
+ state: {
+ bar: 2
+ }
+ })
+
+ app({
+ mixins: [B],
+ view: () => "",
+ events: {
+ loaded: (state) => {
+ expect(state.bar).toBe(2)
+ expect(state.foo).toBe(1)
+ }
+ }
})
})
diff --git a/test/router.test.js b/test/router.test.js
index 8ae2449..c5a6e55 100644
--- a/test/router.test.js
+++ b/test/router.test.js
@@ -13,10 +13,8 @@ beforeEach(() => {
test("/", () => {
app({
- view: {
- "/": state => h("div", {}, "foo")
- },
- plugins: [Router]
+ view: [["/", state => h("div", {}, "foo")]],
+ mixins: [Router]
})
expectHTMLToBe`
@@ -27,10 +25,8 @@ test("/", () => {
test("*", () => {
app({
- view: {
- "*": state => h("div", {}, "foo")
- },
- plugins: [Router],
+ view: [["*", state => h("div", {}, "foo")]],
+ mixins: [Router],
events: {
loaded: (state, actions) => {
actions.router.go("/bar")
@@ -59,10 +55,10 @@ test("routes", () => {
window.location.pathname = "/foo/bar/baz"
app({
- view: {
- "/foo/bar/baz": state => h("div", {}, "foo", "bar", "baz")
- },
- plugins: [Router]
+ view: [
+ ["/foo/bar/baz", state => h("div", {}, "foo", "bar", "baz")]
+ ],
+ mixins: [Router]
})
expectHTMLToBe`
@@ -76,17 +72,20 @@ test("route params", () => {
window.location.pathname = "/be_ep/bOp/b00p"
app({
- view: {
- "/:foo/:bar/:baz": state =>
- h(
- "ul",
- {},
- Object.keys(state.router.params).map(key =>
- h("li", {}, `${key}:${state.router.params[key]}`)
+ view: [
+ [
+ "/:foo/:bar/:baz",
+ state =>
+ h(
+ "ul",
+ {},
+ Object.keys(state.router.params).map(key =>
+ h("li", {}, `${key}:${state.router.params[key]}`)
+ )
)
- )
- },
- plugins: [Router]
+ ]
+ ],
+ mixins: [Router]
})
expectHTMLToBe`
@@ -102,17 +101,20 @@ test("route params separated by a dash", () => {
window.location.pathname = "/beep-bop-boop"
app({
- view: {
- "/:foo-:bar-:baz": state =>
- h(
- "ul",
- {},
- Object.keys(state.router.params).map(key =>
- h("li", {}, `${key}:${state.router.params[key]}`)
+ view: [
+ [
+ "/:foo-:bar-:baz",
+ state =>
+ h(
+ "ul",
+ {},
+ Object.keys(state.router.params).map(key =>
+ h("li", {}, `${key}:${state.router.params[key]}`)
+ )
)
- )
- },
- plugins: [Router]
+ ]
+ ],
+ mixins: [Router]
})
expectHTMLToBe`
@@ -124,14 +126,41 @@ test("route params separated by a dash", () => {
`
})
+test("route params including a dot", () => {
+ window.location.pathname = "/beep/bop.bop/boop"
+
+ app({
+ view: [
+ [
+ "/:foo/:bar/:baz",
+ state =>
+ h(
+ "ul",
+ {},
+ Object.keys(state.router.params).map(key =>
+ h("li", {}, `${key}:${state.router.params[key]}`)
+ )
+ )
+ ]
+ ],
+ mixins: [Router]
+ })
+
+ expectHTMLToBe`
+ <ul>
+ <li>foo:beep</li>
+ <li>bar:bop.bop</li>
+ <li>baz:boop</li>
+ </ul>
+ `
+})
+
test("routes with dashes into a single param key", () => {
window.location.pathname = "/beep-bop-boop"
app({
- view: {
- "/:foo": state => h("div", {}, state.router.params.foo)
- },
- plugins: [Router]
+ view: [["/:foo", state => h("div", {}, state.router.params.foo)]],
+ mixins: [Router]
})
expectHTMLToBe`
@@ -143,11 +172,11 @@ test("routes with dashes into a single param key", () => {
test("popstate", () => {
app({
- view: {
- "/": state => "",
- "/foo": state => h("div", {}, "foo")
- },
- plugins: [Router]
+ view: [
+ ["/", state => ""],
+ ["/foo", state => h("div", {}, "foo")]
+ ],
+ mixins: [Router]
})
window.location.pathname = "/foo"
@@ -168,13 +197,13 @@ test("go", () => {
expect(url).toMatch(/^\/(foo|bar|baz)$/)
app({
- view: {
- "/": state => "",
- "/foo": state => h("div", {}, "foo"),
- "/bar": state => h("div", {}, "bar"),
- "/baz": state => h("div", {}, "baz")
- },
- plugins: [Router],
+ view: [
+ ["/", state => ""],
+ ["/foo", state => h("div", {}, "foo")],
+ ["/bar", state => h("div", {}, "bar")],
+ ["/baz", state => h("div", {}, "baz")]
+ ],
+ mixins: [Router],
events: {
loaded: (state, actions) => {
actions.router.go("/foo")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment