diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..64c9ef1 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +hyperapp.dev \ No newline at end of file diff --git a/FiraCode-Bold.9341457d.woff b/FiraCode-Bold.9341457d.woff new file mode 100644 index 0000000..dd6d534 Binary files /dev/null and b/FiraCode-Bold.9341457d.woff differ diff --git a/FiraCode-Bold.d2e4e930.woff2 b/FiraCode-Bold.d2e4e930.woff2 new file mode 100644 index 0000000..d504c0b Binary files /dev/null and b/FiraCode-Bold.d2e4e930.woff2 differ diff --git a/FiraCode-Light.0049a308.woff2 b/FiraCode-Light.0049a308.woff2 new file mode 100644 index 0000000..1553342 Binary files /dev/null and b/FiraCode-Light.0049a308.woff2 differ diff --git a/FiraCode-Light.7462e69b.woff b/FiraCode-Light.7462e69b.woff new file mode 100644 index 0000000..f778b02 Binary files /dev/null and b/FiraCode-Light.7462e69b.woff differ diff --git a/FiraCode-Medium.61e32950.woff2 b/FiraCode-Medium.61e32950.woff2 new file mode 100644 index 0000000..8e5da1e Binary files /dev/null and b/FiraCode-Medium.61e32950.woff2 differ diff --git a/FiraCode-Medium.fa345726.woff b/FiraCode-Medium.fa345726.woff new file mode 100644 index 0000000..e632ba3 Binary files /dev/null and b/FiraCode-Medium.fa345726.woff differ diff --git a/FiraCode-Regular.5c4d2b60.woff b/FiraCode-Regular.5c4d2b60.woff new file mode 100644 index 0000000..e73f098 Binary files /dev/null and b/FiraCode-Regular.5c4d2b60.woff differ diff --git a/FiraCode-Regular.de18f42f.woff2 b/FiraCode-Regular.de18f42f.woff2 new file mode 100644 index 0000000..f6583be Binary files /dev/null and b/FiraCode-Regular.de18f42f.woff2 differ diff --git a/FiraCode-VF.2a9bbcf8.woff b/FiraCode-VF.2a9bbcf8.woff new file mode 100644 index 0000000..bc5fb4e Binary files /dev/null and b/FiraCode-VF.2a9bbcf8.woff differ diff --git a/FiraCode-VF.fa7346fc.woff2 b/FiraCode-VF.fa7346fc.woff2 new file mode 100644 index 0000000..1ea6c92 Binary files /dev/null and b/FiraCode-VF.fa7346fc.woff2 differ diff --git a/IBMPlexSans-Bold.4dfaebd3.ttf b/IBMPlexSans-Bold.4dfaebd3.ttf new file mode 100644 index 0000000..94717b8 Binary files /dev/null and b/IBMPlexSans-Bold.4dfaebd3.ttf differ diff --git a/IBMPlexSans-Light.ab432367.ttf b/IBMPlexSans-Light.ab432367.ttf new file mode 100644 index 0000000..5c25d87 Binary files /dev/null and b/IBMPlexSans-Light.ab432367.ttf differ diff --git a/IBMPlexSans-Medium.f3ba3949.ttf b/IBMPlexSans-Medium.f3ba3949.ttf new file mode 100644 index 0000000..4c74ec6 Binary files /dev/null and b/IBMPlexSans-Medium.f3ba3949.ttf differ diff --git a/IBMPlexSans-Regular.97a23001.ttf b/IBMPlexSans-Regular.97a23001.ttf new file mode 100644 index 0000000..702c637 Binary files /dev/null and b/IBMPlexSans-Regular.97a23001.ttf differ diff --git a/IBMPlexSans-SemiBold.027d36f8.ttf b/IBMPlexSans-SemiBold.027d36f8.ttf new file mode 100644 index 0000000..a0cde8e Binary files /dev/null and b/IBMPlexSans-SemiBold.027d36f8.ttf differ diff --git a/IBMPlexSans-Thin.eb4bbefb.ttf b/IBMPlexSans-Thin.eb4bbefb.ttf new file mode 100644 index 0000000..1ff8fb0 Binary files /dev/null and b/IBMPlexSans-Thin.eb4bbefb.ttf differ diff --git a/README.a5fa9974.html b/README.a5fa9974.html new file mode 100644 index 0000000..54d902c --- /dev/null +++ b/README.a5fa9974.html @@ -0,0 +1,34 @@ +

Hyperapp npm

+

The tiny framework for building web interfaces.

+

To learn more, go to https://hyperapp.dev for documentation, guides, and examples.

Quickstart

Install Hyperapp with npm or Yarn:

npm i hyperapp

Then with a module bundler like Parcel or Webpack import it in your application and get right down to business.

import { h, app } from "hyperapp"

Don't want to set up a build step? Import Hyperapp in a <script> tag as a module. Don't worry; modules are supported in all evergreen, self-updating desktop, and mobile browsers.

<script type="module">
+  import { h, app } from "https://unpkg.com/hyperapp"
+</script>

Here's the first example to get you started: a counter that can go up or down. You can try it online here.

<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <script type="module">
+      import { h, app } from "https://unpkg.com/hyperapp"
+
+      app({
+        init: 0,
+        view: state =>
+          h("main", {}, [
+            h("h1", {}, state),
+            h("button", { onClick: state => state - 1 }, "-"),
+            h("button", { onClick: state => state + 1 }, "+")
+          ]),
+        node: document.getElementById("app")
+      })
+    </script>
+  </head>
+  <body>
+    <main id="app"></main>
+  </body>
+</html>

The app starts off with init as the initial state. Our code doesn't explicitly maintain any state. Instead, we define actions to transform it and a view to visualize it. The view returns a plain object representation of the DOM known as a virtual DOM, and Hyperapp updates the real DOM to match it whenever the state changes.

Now it's your turn! Experiment with the code a bit. Spend some time thinking about how the view reacts to changes in the state. Can you add a button that resets the counter back to zero? Or how about multiple counters?

Help, I'm stuck!

We love to talk JavaScript and Hyperapp. If you've hit a stumbling block, hop on the Hyperapp Slack or drop by Spectrum to get support, and if you don't receive an answer, or if you remain stuck, please file an issue, and we'll try to help you out.

Is anything wrong, unclear, missing? Help us improve this page.

Stay in the loop

License

MIT

\ No newline at end of file diff --git a/__/node_modules/hyperapp/LICENSE.html b/__/node_modules/hyperapp/LICENSE.html new file mode 100644 index 0000000..9e75819 --- /dev/null +++ b/__/node_modules/hyperapp/LICENSE.html @@ -0,0 +1 @@ +

Copyright © Jorge Bucaran <https://jorgebucaran.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

\ No newline at end of file diff --git a/_redirects b/_redirects new file mode 100644 index 0000000..235cf79 --- /dev/null +++ b/_redirects @@ -0,0 +1,4 @@ +# Netlify redirects file + +# Redirect 404s to index.html (404 should be handled by client) +/* /index.html 200 diff --git a/api.4a87837f.html b/api.4a87837f.html new file mode 100644 index 0000000..11aec1d --- /dev/null +++ b/api.4a87837f.html @@ -0,0 +1,34 @@ +

api

+

The tiny framework for building web interfaces.

+

To learn more, go to https://hyperapp.dev for documentation, guides, and examples.

Quickstart

Install Hyperapp with npm or Yarn:

npm i hyperapp

Then with a module bundler like Parcel or Webpack import it in your application and get right down to business.

import { h, app } from "hyperapp"

Don't want to set up a build step? Import Hyperapp in a <script> tag as a module. Don't worry; modules are supported in all evergreen, self-updating desktop, and mobile browsers.

<script type="module">
+  import { h, app } from "https://unpkg.com/hyperapp"
+</script>

Here's the first example to get you started: a counter that can go up or down. You can try it online here.

<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <script type="module">
+      import { h, app } from "https://unpkg.com/hyperapp"
+
+      app({
+        init: 0,
+        view: state =>
+          h("main", {}, [
+            h("h1", {}, state),
+            h("button", { onClick: state => state - 1 }, "-"),
+            h("button", { onClick: state => state + 1 }, "+")
+          ]),
+        node: document.getElementById("app")
+      })
+    </script>
+  </head>
+  <body>
+    <main id="app"></main>
+  </body>
+</html>

The app starts off with init as the initial state. Our code doesn't explicitly maintain any state. Instead, we define actions to transform it and a view to visualize it. The view returns a plain object representation of the DOM known as a virtual DOM, and Hyperapp updates the real DOM to match it whenever the state changes.

Now it's your turn! Experiment with the code a bit. Spend some time thinking about how the view reacts to changes in the state. Can you add a button that resets the counter back to zero? Or how about multiple counters?

Help, I'm stuck!

We love to talk JavaScript and Hyperapp. If you've hit a stumbling block, hop on the Hyperapp Slack or drop by Spectrum to get support, and if you don't receive an answer, or if you remain stuck, please file an issue, and we'll try to help you out.

Is anything wrong, unclear, missing? Help us improve this page.

Stay in the loop

License

\ No newline at end of file diff --git a/card.b7cfd6ff.png b/card.b7cfd6ff.png new file mode 100644 index 0000000..cecf36c Binary files /dev/null and b/card.b7cfd6ff.png differ diff --git a/close.d10ab7e3.svg b/close.d10ab7e3.svg new file mode 100644 index 0000000..d8f567a --- /dev/null +++ b/close.d10ab7e3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/do-more-with-less.310b45d4.svg b/do-more-with-less.310b45d4.svg new file mode 100644 index 0000000..c92629f --- /dev/null +++ b/do-more-with-less.310b45d4.svg @@ -0,0 +1,13 @@ + + + + DoMoreWithLess + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/ecosystem.0b358926.html b/ecosystem.0b358926.html new file mode 100644 index 0000000..871c30c --- /dev/null +++ b/ecosystem.0b358926.html @@ -0,0 +1 @@ +

ecosystem

Lorem ipsum dolor sit amet, consectetur adipiscing elit. In lobortis dignissim tellus, eget vestibulum leo feugiat vel. Fusce ac odio at nulla feugiat tincidunt. Cras eu metus varius, placerat ex nec, commodo felis. Morbi ac tempus ligula, eget finibus mi. Maecenas porttitor est a lacus suscipit luctus. Suspendisse ornare mattis purus eu mollis. Maecenas pellentesque sem quam, eu imperdiet ante lobortis in. Interdum et malesuada fames ac ante ipsum primis in faucibus. Curabitur purus lectus, sollicitudin in tempus vel, dignissim vitae nibh. In eu elit non elit dapibus dignissim eu congue diam. Nullam ut tellus et erat egestas consectetur eu ut lorem.

\ No newline at end of file diff --git a/examples.md b/examples.md new file mode 100644 index 0000000..9324aea --- /dev/null +++ b/examples.md @@ -0,0 +1,212 @@ +# Examples + +## [Counter](https://codesandbox.io/s/hyperapp-playground-fwjlo) + +```js +import { h, app } from "https://unpkg.com/hyperapp" + +app({ + init: 0, + view: state => + h("div", {}, [ + h("h1", {}, state), + h("button", { onClick: state => state - 1 }, "-"), + h("button", { onClick: state => state + 1 }, "+") + ]), + node: document.getElementById("app") +}) +``` + +## [Calculator](https://codesandbox.io/s/hyperapp-calculator-v8y5h) + +```js +import { h, app } from "hyperapp" + +const computer = { + "+": (a, b) => a + b, + "-": (a, b) => a - b, + "×": (a, b) => a * b, + "÷": (a, b) => a / b +} + +const initialState = { + fn: "", + carry: 0, + value: 0, + hasCarry: false +} + +const Clear = () => initialState + +const NewDigit = (state, number) => ({ + ...state, + hasCarry: false, + value: (state.hasCarry ? 0 : state.value) * 10 + number +}) + +const NewFunction = (state, fn) => ({ + ...state, + fn, + hasCarry: true, + carry: state.value, + value: + state.hasCarry || !state.fn + ? state.value + : computer[state.fn](state.carry, state.value) +}) + +const Equal = state => ({ + ...state, + hasCarry: true, + carry: state.hasCarry ? state.carry : state.value, + value: state.fn + ? computer[state.fn]( + state.hasCarry ? state.value : state.carry, + state.hasCarry ? state.carry : state.value + ) + : state.value +}) + +const Calculator = state => + h("main", {}, [ + Display(state.value), + Keypad([ + Functions({ keys: Object.keys(computer) }), + Digits({ keys: [7, 8, 9, 4, 5, 6, 1, 2, 3, 0] }), + AC, + EQ + ]) + ]) + +const Display = value => h("div", { class: "display" }, value) + +const Keypad = children => h("div", { class: "keys" }, children) + +const Functions = props => + props.keys.map(fn => + h("button", { class: "function", onClick: [NewFunction, fn] }, fn) + ) + +const Digits = props => + props.keys.map(digit => + h( + "button", + { class: { zero: digit === 0 }, onClick: [NewDigit, digit] }, + digit + ) + ) + +const AC = h("button", { onClick: Clear }, "AC") +const EQ = h("button", { onClick: Equal, class: "equal" }, "=") + +app({ + init: initialState, + view: Calculator, + node: document.getElementById("app") +}) +``` + +## [Simple Clock](https://codesandbox.io/s/hyperapp-simple-clock-uhk59) + +```js +import { h, app } from "hyperapp" +import { interval } from "@hyperapp/time" + +const timeToUnits = t => [t.getHours(), t.getMinutes(), t.getSeconds()] + +const formatTime = (hours, minutes, seconds, use24) => + (use24 ? hours : hours > 12 ? hours - 12 : hours) + + ":" + + `${minutes}`.padStart(2, "0") + + ":" + + `${seconds}`.padStart(2, "0") + + (use24 ? "" : ` ${hours > 12 ? "PM" : "AM"}`) + +const posixToHumanTime = (time, use24) => + formatTime(...timeToUnits(new Date(time)), use24) + +const Tick = (state, time) => ({ + ...state, + time +}) + +const ToggleFormat = state => ({ + ...state, + use24: !state.use24 +}) + +const getInitialState = time => ({ + time, + use24: false +}) + +app({ + init: getInitialState(Date.now()), + view: state => + h("div", {}, [ + h("h1", {}, posixToHumanTime(state.time, state.use24)), + h("fieldset", {}, [ + h("legend", {}, "Settings"), + h("label", {}, [ + h("input", { + type: "checkbox", + checked: state.use24, + onInput: ToggleFormat + }), + "Use 24 Hour Clock" + ]) + ]) + ]), + subscriptions: state => interval(Tick, { delay: 1000 }), + node: document.getElementById("app") +}) +``` + +## [Todo App](https://codesandbox.io/s/hyperapp-todo-app-m3ctx) + +```js +import { h, app } from "hyperapp" +import { preventDefault, targetValue } from "@hyperapp/events" + +const getInitialState = items => ({ items, value: "" }) + +const newItem = value => ({ + value, + lastValue: "", + isEditing: false, + id: Math.random().toString(36) +}) + +const NewValue = (state, value) => ({ ...state, value }) + +const Add = state => + state.value.length === 0 + ? state + : { + ...state, + value: "", + items: state.items.concat(newItem(state.value)) + } + +const TodoList = items => + h("ol", {}, items.map(item => h("li", {}, item.value))) + +app({ + init: getInitialState([newItem("Take out the trash")]), + view: state => + h("div", {}, [ + h("h1", {}, "What needs done?"), + TodoList(state.items), + h("form", { onSubmit: preventDefault(Add) }, [ + h("label", {}, [ + h("input", { + value: state.value, + onInput: [NewValue, targetValue] + }) + ]), + h("button", {}, `New #${state.items.length + 1}`) + ]) + ]), + node: document.getElementById("app") +}) +``` diff --git a/faster-than-react.31c816ca.svg b/faster-than-react.31c816ca.svg new file mode 100644 index 0000000..dc58941 --- /dev/null +++ b/faster-than-react.31c816ca.svg @@ -0,0 +1,22 @@ + + + + FasterThanReact + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/favicon.648f6921.png b/favicon.648f6921.png new file mode 100644 index 0000000..d23ba9f Binary files /dev/null and b/favicon.648f6921.png differ diff --git a/global.89bdf38a.css b/global.89bdf38a.css new file mode 100644 index 0000000..9c95d8c --- /dev/null +++ b/global.89bdf38a.css @@ -0,0 +1 @@ +*,:after,:before{box-sizing:border-box}:after,:before{text-decoration:inherit;vertical-align:inherit}html{cursor:default;line-height:1.5;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;word-break:break-word}body{margin:0}h1{font-size:2em;margin:.67em 0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{margin:0}hr{height:0;overflow:visible}main{display:block}nav ol,nav ul{list-style:none;padding:0}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}audio,canvas,iframe,img,svg,video{vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}iframe,img{border-style:none}svg:not([fill]){fill:currentColor}svg:not(:root){overflow:hidden}table{border-collapse:collapse}button,input,select{margin:0}button{overflow:visible;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}fieldset{border:1px solid #a0a0a0;padding:.35em .75em .625em}input{overflow:visible}legend{color:inherit;display:table;max-width:100%;white-space:normal}progress{display:inline-block;vertical-align:baseline}select{text-transform:none}textarea{margin:0;overflow:auto;resize:vertical}[type=checkbox],[type=radio]{padding:0}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}details,dialog{display:block}dialog{background-color:#fff;border:solid;color:#000;height:-moz-fit-content;height:-webkit-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-moz-fit-content;width:-webkit-fit-content;width:fit-content}dialog:not([open]){display:none}summary{display:list-item}canvas{display:inline-block}template{display:none}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}[hidden]{display:none}[aria-busy=true]{cursor:progress}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}html{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}code,kbd,pre,samp{font-family:Menlo,Consolas,Roboto Mono,"Ubuntu Monospace",Noto Mono,Oxygen Mono,Liberation Mono,monospace,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}button,input,select,textarea{background-color:transparent;border:1px solid WindowFrame;color:inherit;font:inherit;letter-spacing:inherit;padding:.25em .375em}select{-moz-appearance:none;-webkit-appearance:none;background:no-repeat 100%/1em;border-radius:0;padding-right:1em}select:not([multiple]):not([size]){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='4'%3E%3Cpath d='M4 0h6L7 4'/%3E%3C/svg%3E")}::-ms-expand{display:none}:-ms-input-placeholder{color:rgba(0,0,0,.54)}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:100;font-display:swap;src:url(/IBMPlexSans-Thin.eb4bbefb.ttf) format("truetype")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:300;font-display:swap;src:url(/IBMPlexSans-Light.ab432367.ttf) format("truetype")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:400;font-display:swap;src:url(/IBMPlexSans-Regular.97a23001.ttf) format("truetype")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:500;font-display:swap;src:url(/IBMPlexSans-Medium.f3ba3949.ttf) format("truetype")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:600;font-display:swap;src:url(/IBMPlexSans-SemiBold.027d36f8.ttf) format("truetype")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:700;font-display:swap;src:url(/IBMPlexSans-Bold.4dfaebd3.ttf) format("truetype")}@font-face{font-family:Fira Code;src:url(/FiraCode-Light.0049a308.woff2) format("woff2"),url(/FiraCode-Light.7462e69b.woff) format("woff");font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:Fira Code;src:url(/FiraCode-Regular.de18f42f.woff2) format("woff2"),url(/FiraCode-Regular.5c4d2b60.woff) format("woff");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:Fira Code;src:url(/FiraCode-Medium.61e32950.woff2) format("woff2"),url(/FiraCode-Medium.fa345726.woff) format("woff");font-weight:500;font-style:normal}@font-face{font-family:Fira Code;src:url(/FiraCode-Bold.d2e4e930.woff2) format("woff2"),url(/FiraCode-Bold.9341457d.woff) format("woff");font-weight:700;font-style:normal}@font-face{font-family:Fira Code VF;src:url(/FiraCode-VF.fa7346fc.woff2) format("woff2-variations"),url(/FiraCode-VF.2a9bbcf8.woff) format("woff-variations");font-weight:300 700;font-style:normal}:root{--primary-blue:#1661ee;--dark-blue:#091226;--body-text-grey:#9da0a8;--error-red:#ff4545;--disabled-grey:#989898;--disabled-grey-2:#f3f4f7;--active-input-text:#dadfe1;--line-separator:#eef0f3}html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;color:var(--body-text-grey);background-color:#fff;scroll-behavior:smooth}body{position:relative;overflow-x:hidden}@media (min-width:640px){body{font-size:1.25rem}}::selection{background:var(--primary-blue);color:#fff}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:IBM Plex Sans,sans-serif;color:var(--dark-blue);font-weight:300}.h1,h1{font-size:2.625rem;line-height:1.25}.h1,.h2,h1,h2{font-weight:300;width:95%}.h2,h2{font-size:1.75rem;line-height:1.425}.h3,h3{font-size:1.5rem;line-height:1.5;font-weight:300}.h4,.h5,.h6,h4,h5,h6{font-size:1.25rem;line-height:1.25}.h5,.h6,h5,h6{margin:1em 0}@media (min-width:640px){.h1,h1{font-size:3.375rem;line-height:1.2}.h2,h2{font-size:2.625rem;line-height:1.25}.h3,h3{font-size:1.75rem;font-weight:300;line-height:1.425}.h4,h4{font-size:1.5rem;line-height:1.5}}label{display:block}button,input,select,textarea{border:none;padding:.5rem 0}iframe,img,input,select,textarea{height:auto;max-width:100%}code,kbd,samp{display:inline-block;color:var(--dark-blue);font-family:Fira Code,monospace;font-size:1rem}pre code{display:block;padding:1rem 0;overflow:auto}em{font-weight:500}a,em{color:var(--dark-blue)}a{text-decoration:none}a:hover{color:var(--primary-blue)}a:active,a:hover{text-decoration:underline}a:active{color:var(--body-text-grey)}.arrow-link{display:inline-block;text-decoration:none;position:relative;padding-right:1.5rem;font-size:1.25rem}.arrow-link:after{content:"";display:block;position:absolute;top:.6rem;right:.2rem;background-image:url('data:image/svg+xml;utf8,');background-size:contain;background-repeat:no-repeat;width:.8rem;height:.8rem}.arrow-link:hover:after{background-image:url('data:image/svg+xml;utf8,')}.arrow-link:active:after{background-image:url('data:image/svg+xml;utf8,')}.back-link{display:inline-block;text-decoration:none;position:relative;padding-right:2rem;font-size:1.25rem}.back-link:after{content:"";display:block;position:absolute;top:.33rem;right:.2rem;background-image:url('data:image/svg+xml;utf8,');background-size:contain;background-repeat:no-repeat;width:1.2rem;height:1.2rem}.back-link:hover:after{background-image:url('data:image/svg+xml;utf8,')}.back-link:active:after{background-image:url('data:image/svg+xml;utf8,')}blockquote{color:var(--dark-blue);font-size:1.5rem;padding:1rem 3rem;border-left:1px solid var(--line-separator);margin-left:0}hr{border:none;border-top:1px solid var(--line-separator);margin:2rem 0}caption{font-weight:500}caption,th{color:var(--dark-blue)}th{font-weight:400;text-align:left}td,th{padding:.5rem}strong{font-weight:500}b,strong{color:var(--dark-blue)}b{font-weight:400}abbr,cite,del,dfn,i,ins,q,s,time,u,var{color:var(--dark-blue)}mark{background:var(--primary-blue);color:#fff}small{font-size:.875rem}.error{color:var(--error-red)}fieldset{border:1px solid var(--dark-blue)}.button,button,label,legend{color:var(--dark-blue)}.button,button{padding:.5rem 1rem;border:1px solid;cursor:pointer;min-height:3rem;min-width:3rem}.button+.button,button+button{margin-left:1rem}.button:hover,button:hover{color:var(--primary-blue)}.button:active,button:active{color:#fff;background-color:var(--primary-blue);border-color:var(--primary-blue);outline:none}.button:disabled,button:disabled{color:var(--disabled-grey);background-color:var(--disabled-grey-2);border-color:var(--disabled-grey-2)}.primary-button{color:#fff;background-color:var(--primary-blue);border-color:var(--primary-blue)}.primary-button:hover{color:#fff;background-color:var(--dark-blue);border-color:var(--dark-blue)}.primary-button:active{color:var(--dark-blue);background-color:#fff}.square-button{font-size:0;color:#fff;background-color:var(--primary-blue);border-color:var(--primary-blue);display:flex;align-items:center;justify-content:center;padding:0;position:relative}.square-button:after,.square-button:before{content:"";display:block;background-color:currentColor;position:absolute}.square-button:before{top:.75rem;bottom:.75rem;width:1px}.square-button:after{left:.75rem;right:.75rem;height:1px}.square-button:hover{background-color:#000;border-color:#000;color:#fff}.square-button:hover:before{top:0;bottom:0}.square-button:hover:after{left:0;right:0}.square-button:active{background-color:#fff;border-color:#000;color:#000}.square-button:active:before{top:0;bottom:0}.square-button:active:after{left:0;right:0}input{font-size:inherit}.nice-input,input{color:var(--dark-blue)}.nice-input{display:grid;grid-template-columns:1fr auto;grid-template-rows:auto;max-width:24rem;margin:1rem 0}.nice-input input{grid-row:1;grid-column:1;color:inherit}.nice-input button{grid-row:1;grid-column:2}.nice-input small{grid-row:2;grid-column:1/2}.nice-input.error{color:var(--error-red)}.nice-input.error button{background-color:var(--error-red);border-color:var(--error-red)}body{--menu-width:18rem}.app{margin:0 auto;position:relative;min-height:100vh}.noBodyScroll{overflow:hidden;max-height:100vh}.main-content{padding:1rem 1.25rem;max-width:1024px;position:relative}.secondary-menu{position:fixed;top:1.25rem;right:1.25rem;height:0;display:flex;flex-direction:column;align-items:flex-end;display:none;z-index:5}.secondary-menu a{color:inherit;text-decoration:none;position:relative}.secondary-menu a:hover{color:#546067}.secondary-menu a:hover:before{content:"";display:block;position:fixed;top:0;bottom:0;left:var(--menu-width);right:0;background-color:hsla(0,0%,100%,.8);width:100%;height:100%;min-height:50vh;pointer-events:none;z-index:-1}.secondary-menu a.active{color:var(--dark-blue)}h1 a{text-decoration:none}@media (min-width:992px){.main-content{margin-left:var(--menu-width);margin-bottom:6rem;padding:1.25rem}.secondary-menu{display:flex}}@media (min-width:1320px){body{--menu-width:24rem}}code,pre{padding:0!important;background-color:#fff!important}code{display:inline-block!important;color:var(--dark-blue)!important;font-family:Fira Code,monospace!important;font-size:1rem!important} \ No newline at end of file diff --git a/guides.31d0dce1.html b/guides.31d0dce1.html new file mode 100644 index 0000000..3730c99 --- /dev/null +++ b/guides.31d0dce1.html @@ -0,0 +1 @@ +

guides

lorem lorem

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In id turpis et nibh commodo viverra et eu nisl. Pellentesque imperdiet dapibus aliquam. Cras vulputate viverra neque nec ultricies. Mauris consectetur vulputate eleifend. Interdum et malesuada fames ac ante ipsum primis in faucibus. Maecenas a auctor augue. Donec iaculis urna eget nisi maximus, vel aliquam magna volutpat. Mauris mattis ac augue non pharetra. Aliquam erat volutpat. Vestibulum ultricies, turpis ac pretium fringilla, quam ante tincidunt nisi, quis venenatis turpis odio sed libero. Nullam pulvinar tempor elit, gravida eleifend odio viverra non.

\ No newline at end of file diff --git a/hyperapp-logo-v1.eed9079f.svg b/hyperapp-logo-v1.eed9079f.svg new file mode 100644 index 0000000..9331f4e --- /dev/null +++ b/hyperapp-logo-v1.eed9079f.svg @@ -0,0 +1,9 @@ + + + + hyperapp logo 2 + Created with Sketch. + + + + \ No newline at end of file diff --git a/hyperapp-logo-v2.10d2f4a5.svg b/hyperapp-logo-v2.10d2f4a5.svg new file mode 100644 index 0000000..6f300ba --- /dev/null +++ b/hyperapp-logo-v2.10d2f4a5.svg @@ -0,0 +1,9 @@ + + + + logo + Created with Sketch. + + \ No newline at end of file diff --git a/hypercharged.f1b19631.svg b/hypercharged.f1b19631.svg new file mode 100644 index 0000000..32bb9a7 --- /dev/null +++ b/hypercharged.f1b19631.svg @@ -0,0 +1,16 @@ + + + + Hypercharged + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/icon-192x192.c99be655.png b/icon-192x192.c99be655.png new file mode 100644 index 0000000..669b11c Binary files /dev/null and b/icon-192x192.c99be655.png differ diff --git a/icon-512x512.ebbe6b77.png b/icon-512x512.ebbe6b77.png new file mode 100644 index 0000000..2d8df0a Binary files /dev/null and b/icon-512x512.ebbe6b77.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..cf1e03a --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +Hyperapp • The tiny framework for building web interfaces
\ No newline at end of file diff --git a/logo-big.e296631b.svg b/logo-big.e296631b.svg new file mode 100644 index 0000000..358e34d --- /dev/null +++ b/logo-big.e296631b.svg @@ -0,0 +1,4 @@ + + + + diff --git a/manifest.webmanifest b/manifest.webmanifest new file mode 100644 index 0000000..97b1d44 --- /dev/null +++ b/manifest.webmanifest @@ -0,0 +1 @@ +{"short_name":"Hyperapp","name":"Hyperapp","description":"Hyperapp with a modern configuration and best-practices in mind.","display":"fullscreen","scope":"/","start_url":"/","theme_color":"white","background_color":"white","orientation":"portrait","dir":"ltr","lang":"en","icons":[{"src":"icon-192x192.c99be655.png","sizes":"192x192","type":"image/png"},{"src":"icon-512x512.ebbe6b77.png","sizes":"512x512","type":"image/png"}]} \ No newline at end of file diff --git a/menu.1f287146.svg b/menu.1f287146.svg new file mode 100644 index 0000000..38ffe8e --- /dev/null +++ b/menu.1f287146.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/pages-data.json b/pages-data.json new file mode 100644 index 0000000..e656538 --- /dev/null +++ b/pages-data.json @@ -0,0 +1 @@ +{"api":"# api\n\n\n> The tiny framework for building web interfaces.\n\n- **Do more with less**—We have minimized the concepts you need to learn to be productive. Views, actions, effects, and subscriptions are all pretty easy to get to grips with and work together seamlessly.\n- **Write what, not how**—With a declarative syntax that's easy to read and natural to write, Hyperapp is your tool of choice to develop purely functional, feature-rich, browser-based applications.\n- **Hypercharged**—Hyperapp is a modern VDOM engine, state management solution, and application design pattern all-in-one. Once you learn to use it, there'll be no end to what you can do.\n\nTo learn more, go to for documentation, guides, and examples.\n\n## Quickstart\n\nInstall Hyperapp with npm or Yarn:\n\n```console\nnpm i hyperapp\n```\n\nThen with a module bundler like [Parcel](https://parceljs.org) or [Webpack](https://webpack.js.org) import it in your application and get right down to business.\n\n```js\nimport { h, app } from \"hyperapp\"\n```\n\nDon't want to set up a build step? Import Hyperapp in a `\n```\n\nHere's the first example to get you started: a counter that can go up or down. You can try it online [here](https://codesandbox.io/s/hyperapp-playground-fwjlo).\n\n```html\n\n\n \n \n \n \n
\n \n\n```\n\nThe app starts off with `init` as the initial state. Our code doesn't explicitly maintain any state. Instead, we define actions to transform it and a view to visualize it. The view returns a plain object representation of the DOM known as a virtual DOM, and Hyperapp updates the real DOM to match it whenever the state changes.\n\nNow it's your turn! Experiment with the code a bit. Spend some time thinking about how the view reacts to changes in the state. Can you add a button that resets the counter back to zero? Or how about multiple counters?\n\n## Help, I'm stuck!\n\nWe love to talk JavaScript and Hyperapp. If you've hit a stumbling block, hop on the [Hyperapp Slack](https://hyperappjs.herokuapp.com) or drop by [Spectrum](https://spectrum.chat/hyperapp) to get support, and if you don't receive an answer, or if you remain stuck, please file an issue, and we'll try to help you out.\n\nIs anything wrong, unclear, missing? Help us [improve this page](https://github.com/jorgebucaran/hyperapp/fork).\n\n## Stay in the loop\n\n- [Twitter](https://twitter.com/hyperappjs)\n- [Awesome](https://github.com/jorgebucaran/awesome-hyperapp)\n- [/r/hyperapp](https://www.reddit.com/r/hyperapp)\n\n## License\n","ecosystem":"# ecosystem\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. In lobortis dignissim tellus, eget vestibulum leo feugiat vel. Fusce ac odio at nulla feugiat tincidunt. Cras eu metus varius, placerat ex nec, commodo felis. Morbi ac tempus ligula, eget finibus mi. Maecenas porttitor est a lacus suscipit luctus. Suspendisse ornare mattis purus eu mollis. Maecenas pellentesque sem quam, eu imperdiet ante lobortis in. Interdum et malesuada fames ac ante ipsum primis in faucibus. Curabitur purus lectus, sollicitudin in tempus vel, dignissim vitae nibh. In eu elit non elit dapibus dignissim eu congue diam. Nullam ut tellus et erat egestas consectetur eu ut lorem.\n","guides":"# guides\nlorem lorem\n\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In id turpis et nibh commodo viverra et eu nisl. Pellentesque imperdiet dapibus aliquam. Cras vulputate viverra neque nec ultricies. Mauris consectetur vulputate eleifend. Interdum et malesuada fames ac ante ipsum primis in faucibus. Maecenas a auctor augue. Donec iaculis urna eget nisi maximus, vel aliquam magna volutpat. Mauris mattis ac augue non pharetra. Aliquam erat volutpat. Vestibulum ultricies, turpis ac pretium fringilla, quam ante tincidunt nisi, quis venenatis turpis odio sed libero. Nullam pulvinar tempor elit, gravida eleifend odio viverra non.\n","quickstart":"## quickstart\n\n1․ Install Hyperapp with npm or Yarn:\n\n---\n```console\nnpm i hyperapp\n```\n---\n\n\n\n\n2․ Then with a module bundler like [Parcel](https://parceljs.org) or [Webpack](https://webpack.js.org) import it in your application and get right down to business.\n\n---\n```js\nimport { h, app } from \"hyperapp\"\n```\n---\n\n\n\n\n3․ Don't want to set up a build step? Import Hyperapp in a `\n```\n---\n\n\n\n\nHere's the first example to get you started: a counter that can go up or down. You can try it online [here](https://codesandbox.io/s/hyperapp-playground-fwjlo).\n\n---","sponsor":"# sponsor\nlorem lorem lorem\n\nIn urna ex, finibus sit amet laoreet id, pharetra placerat lorem. Suspendisse laoreet pulvinar nunc, sed tristique ex venenatis tristique. Quisque non vulputate enim, vitae facilisis sapien. Nunc sagittis vel mi et tristique. In ornare leo et lectus ornare, vel pretium odio vulputate. Nam rhoncus quam vel neque rhoncus rutrum. Quisque posuere, purus sit amet ornare blandit, massa ligula sagittis magna, ut interdum purus neque et nisl. Integer eros sapien, faucibus at est vel, rhoncus gravida arcu. In volutpat sapien neque, vel malesuada sapien aliquam at.\n","tutorial":"# tutorial\n\n===================================\n\nWelcome! If you're new to Hyperapp, you've found the perfect place to start learning.\n\nThe Set-up\n-----------------------------------\n\nTogether we'll build a simple newsreader-like application. As we do, we'll work\nour way through the five core concepts: view, state, actions, effects and subscriptions.\n\nTo move things along, let's imagine we've already made a static version of the\napp we want to build, with this HTML:\n\n\n```html\n
\n
\n Filter:\n ocean\n \n
\n
\n
    \n
  • \n

    The Ocean is Sinking

    \n

    Kat Stropher

    \n
  • \n
  • \n

    Ocean life is brutal

    \n

    Surphy McBrah

    \n
  • \n
  • \n

    \n Family friendly fun at the\n ocean exhibit\n

    \n

    Guy Prosales

    \n
  • \n
\n
\n
\n

Ocean life is brutal

\n

\n Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\n eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim\n ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut\n aliquip ex ea commodo consequat.\n

\n

Surphy McBrah

\n
\n
\n Auto update:\n \n
\n
\n```\n\n...and some CSS [here](https://zaceno.github.com/hatut/style.css).\n\nIt looks like this:\n\n\nWe'll start by making Hyperapp render the HTML for us. Then we will\nadd dynamic behavior to all the widgets, including text input and \ndynamically fetching stories.\n\nFirst, let's begin with the traditional \"Hello World!\"\n\nHello World\n------------------------------\n\nCreate this html file:\n\n```html\n\n\n \n \n \n \n \n
\n \n\n```\n\n> The section structure outlined in the comments is not important. It's\n> just a suggestion for how to organize the code we'll be\n> adding throughout the tutorial.\n\nOpen it in a browser, and you'll be greeted with an optimistic **Hello _World!_**.\n\nView\n------------------------------------\n\nLet's step through what just happened.\n\n### Virtual Nodes\n\nHyperapp exports the `app` and `h` functions.\n`h` is for creating _virtual nodes_, which is to say: plain javascript objects\nwhich _represent_ DOM nodes.\n\nThe result of \n\n```js\nh(\"h1\", {}, [\n \"Hello \", \n h(\"i\", {}, \"World!\")\n])\n```\n\nis a virtual node, representing\n\n```html\n

\n Hello \n World!\n

\n```\n\n### Rendering to the DOM\n\n`app` is the function that runs our app. It is called with a single argument - an object\nwhich can take several properties. For now we're just concerned with `view` and `node. `\n\nHyperapp calls the `view` function which tells it the DOM structure we want, in the form\nof virtual nodes. Hyperapp proceeds to create it for us, replacing the node specified in `node`.\n\nTo render the HTML we want, change the `view` to:\n\n```js\nview: () => h(\"div\", {id: \"app\", class: \"container\"}, [\n h(\"div\", {class: \"filter\"}, [\n \" Filter: \",\n h(\"span\", {class: \"filter-word\"}, \"ocean\"),\n h(\"button\", {}, \"\\u270E\")\n ]),\n h(\"div\", {class: \"stories\"}, [\n h(\"ul\", {}, [\n h(\"li\", {class: \"unread\"}, [\n h(\"p\", {class: \"title\"}, [\n \"The \",\n h(\"em\", {}, \"Ocean\"),\n \" is Sinking!\"\n ]),\n h(\"p\", {class: \"author\"}, \"Kat Stropher\")\n ]),\n h(\"li\", {class: \"reading\"}, [\n h(\"p\", {class: \"title\"}, [\n h(\"em\", {}, \"Ocean\"),\n \" life is brutal\"\n ]),\n h(\"p\", {class: \"author\"}, \"Surphy McBrah\"),\n ]),\n h(\"li\", {}, [\n h(\"p\", {class: \"title\"}, [\n \"Family friendly fun at the \",\n h(\"em\", {}, \"ocean\"),\n \" exhibit\"\n ]),\n h(\"p\", {class: \"author\"}, \"Guy Prosales\")\n ])\n ])\n ]),\n h(\"div\", {class: \"story\"}, [\n h(\"h1\", {}, \"Ocean life is brutal\"),\n h(\"p\", {}, `\n Lorem ipsum dolor sit amet, consectetur adipiscing\n elit, sed do eiusmod tempor incididunt ut labore et\n dolore magna aliqua. Ut enim ad minim veniam, quis\n nostrud exercitation ullamco laboris nisi ut aliquip\n ex ea commodo consequat.\n `),\n h(\"p\", {class: \"signature\"}, \"Surphy McBrah\")\n ]),\n h(\"div\", {class: \"autoupdate\"}, [\n \"Auto update: \",\n h(\"input\", {type: \"checkbox\"})\n ])\n]),\n```\n\nTry it out to confirm that the result matches the screenshot above.\n\n> In many frameworks it is common to write your views/templates\n> using syntax that looks like HTML. This is possible with Hyperapp as well.\n> [JSX](https://babeljs.io/docs/en/babel-plugin-transform-react-jsx) \n> can compile a HTML-like syntax into `h` calls at build-time. If you'd rather\n> not use a build system, [htm](https://github.com/developit/htm) does the same at run-time.\n>\n> In this tutorial we'll stick with `h` to keep it simple and close to the metal.\n\n### Composing the view with reusable functions\n\nThe great thing about using plain functions to build up our virtual DOM\nis that we can break out repetitive or complicated parts into their own functions.\n\nAdd this function (in the \"VIEWS\" section):\n\n```js\nconst emphasize = (word, string) =>\n string.split(\" \").map(x => {\n if (x.toLowerCase() === word.toLowerCase()) {\n return h(\"em\", {}, x + \" \")\n } else {\n return x + \" \"\n }\n }) \n```\n\nIt lets you change this: \n\n```js\n ...\n h(\"p\", {class: \"title\"}, [\n \"The \",\n h(\"em\", {}, \"Ocean\"),\n \" is Sinking!\"\n ]),\n ...\n```\n\ninto this:\n\n```js\n ...\n h(\"p\", {class: \"title\"}, emphasize(\"ocean\",\n \"The Ocean is Sinking\"\n ))\n ...\n```\n\nStory thumbnails are repeated several times, so encapsulate\nthem in their own function:\n\n```js\nconst StoryThumbnail = props => h(\n \"li\",\n {class: {\n unread: props.unread,\n reading: props.reading,\n }},\n [\n h(\"p\", {class: \"title\"}, emphasize(props.filter, props.title)),\n h(\"p\", {class: \"author\"}, props.author)\n ]\n)\n```\n\n> The last example demonstrates a helpful feature of the `class` property. When\n> you set it to an object rather than a string, each key with a truthy value\n> will become a class in the class list.\n\nContinue by creating functions for each section of the view:\n\n```js\n\nconst StoryList = props => h(\"div\", {class: \"stories\"}, [\n h(\"ul\", {}, Object.keys(props.stories).map(id =>\n StoryThumbnail({\n id,\n title: props.stories[id].title,\n author: props.stories[id].author,\n unread: !props.stories[id].seen,\n reading: props.reading === id,\n filter: props.filter,\n })\n ))\n])\n\nconst Filter = props => h(\"div\", {class: \"filter\"}, [\n \"Filter:\",\n h(\"span\", {class: \"filter-word\"}, props.filter),\n h(\"button\", {}, \"\\u270E\")\n])\n\nconst StoryDetail = props => h(\"div\", {class: \"story\"}, [\n props && h(\"h1\", {}, props.title),\n props && h(\"p\", {}, `\n Lorem ipsum dolor sit amet, consectetur adipiscing\n elit, sed do eiusmod tempor incididunt ut labore et\n dolore magna aliqua. Ut enim ad minim veniam, qui\n nostrud exercitation ullamco laboris nisi ut aliquip\n ex ea commodo consequat.\n `),\n props && h(\"p\", {class: \"signature\"}, props.author)\n])\n\nconst AutoUpdate = props => h(\"div\", {class: \"autoupdate\"}, [\n \"Auto update: \",\n h(\"input\", {type: \"checkbox\"})\n])\n\nconst Container = content => h(\"div\", {class: \"container\"}, content)\n\n```\n\n\nWith those the view can be written as:\n\n```js\nview: () => Container([\n Filter({\n filter: \"ocean\"\n }),\n StoryList({\n stories: {\n \"112\": {\n title: \"The Ocean is Sinking\",\n author: \"Kat Stropher\",\n seen: false,\n },\n \"113\": {\n title: \"Ocean life is brutal\",\n author: \"Surphy McBrah\",\n seen: true,\n },\n \"114\": {\n title: \"Family friendly fun at the ocean exhibit\",\n author: \"Guy Prosales\",\n seen: true,\n }\n },\n reading: \"113\",\n filter: \"ocean\"\n }),\n StoryDetail({\n title: \"Ocean life is brutal\",\n author: \"Surphy McBrah\",\n }),\n AutoUpdate(),\n])\n```\n\nWhat you see on the page should be exactly the same as before, because we haven't\nchanged what `view` returns. Using basic functional composition, we were able to make\nthe code a bit more manageable, and that's the only difference.\n\nState\n-------------------------------\n\nWith all that view logic broken out in separate functions, `view` is starting to look like\nplain _data_. The next step is to fully separate data from the view.\n\nAdd an `init` property to your app, with this pure data:\n\n```js\n init: {\n filter: \"ocean\",\n reading: \"113\",\n stories: {\n \"112\": {\n title: \"The Ocean is Sinking\",\n author: \"Kat Stropher\",\n seen: false,\n },\n \"113\": {\n title: \"Ocean life is brutal\",\n author: \"Surphy McBrah\",\n seen: true,\n },\n \"114\": {\n title: \"Family friendly fun at the ocean exhibit\",\n author: \"Guy Prosales\",\n seen: true,\n }\n }\n },\n```\n\nThe value of `init` becomes the app's _state_. Hyperapp calls `view` with the state\nas an argument, so it can be reduced to:\n\n```js\n view: state => Container([\n Filter(state),\n StoryList(state),\n StoryDetail(state.reading && state.stories[state.reading]),\n AutoUpdate(state),\n ]),\n```\n\nVisually, everything is _still_ the same. If you'd like to see a working example of the code so far, have a look [here](https://codesandbox.io/s/hyperapp-tutorial-step-1-gq662)\n\nActions\n---------------------\n\nNow that we know all about rendering views, it's finally time for some _action_!\n\n### Reacting to events in the DOM\n\nThe first bit of dynamic behavior we will add is so that when you click\nthe pencil-button, a text input with the filter word appears.\n\nAdd an `onClick` property to the button in the filter view:\n\n```js\nconst Filter = props => h(\"div\", {class: \"filter\"}, [\n \"Filter:\",\n h(\"span\", {class: \"filter-word\"}, props.filter),\n h(\"button\", { onClick: StartEditingFilter }, \"\\u270E\") // <---\n])\n```\n\nThis makes Hyperapp bind a click-event handler on the button element, so\nthat when the button is clicked, an action named `StartEditingFilter` is\n_dispatched_. Create the action in the \"ACTIONS\" section:\n\n```js\nconst StartEditingFilter = state => ({...state, editingFilter: true})\n```\n\nActions are just functions describing transformations of the state.\nThis action keeps everything in the state the same except for `editingFilter`\nwhich it sets to `true`.\n\nWhen Hyperapp dispatches an action, it replaces the old state with the new\none calculated using the action. Then the DOM is modified to match what the\nview returns for this new state.\n\nWhen `editingFilter` is true, we want to have a text input instead of a \nspan with the filter word. We can express this in the `Filter` view using a\nternary operator (`a ? b : c`).\n\n```js\nconst Filter = props => h(\"div\", {class: \"filter\"}, [\n \"Filter:\",\n \n props.editingFilter // <---\n ? h(\"input\", {type: \"text\", value: props.filter}) // <---\n : h(\"span\", {class: \"filter-word\"}, props.filter),\n \n h(\"button\", { onClick: StartEditingFilter }, \"\\u270E\")\n])\n```\n\nNow, when you click the pencil button the text input appears. But we still need to add\na way to go back. We need an action to `StopEditingFilter`, and a button to dispatch it.\n\nAdd the action:\n\n```js\nconst StopEditingFilter = state => ({...state, editingFilter: false})\n```\n\nand update the `Filter` view again:\n\n```js\nconst Filter = props => h(\"div\", {class: \"filter\"}, [\n \"Filter:\",\n \n props.editingFilter\n ? h(\"input\", {type: \"text\", value: props.filter})\n : h(\"span\", {class: \"filter-word\"}, props.filter),\n\n props.editingFilter // <---\n ? h(\"button\", {onClick: StopEditingFilter}, \"\\u2713\") \n : h(\"button\", {onClick: StartEditingFilter}, \"\\u270E\"), // <---\n])\n```\n\nWhen you click the pencil button, it is replaced with a check-mark button that can take you back to the first state.\n\n\n\n\n### Capturing event-data in actions\n\nThe next step is to use the input for editing the filter word. Whatever we\ntype in the box should be emphasized in the story-list.\n\nUpdate the `Filter` view yet again:\n\n```js\nconst Filter = props => h(\"div\", {class: \"filter\"}, [\n \"Filter:\",\n \n props.editingFilter\n ? h(\"input\", {\n type: \"text\",\n value: props.filter,\n onInput: SetFilter, // <----\n })\n : h(\"span\", {class: \"filter-word\"}, props.filter),\n\n props.editingFilter\n ? h(\"button\", {onClick: StopEditingFilter}, \"\\u2713\") \n : h(\"button\", {onClick: StartEditingFilter}, \"\\u270E\"), \n])\n```\n\nThis will dispatch the `SetFilter` action everytime someone types in the input. Implement the action like this:\n\n```js\nconst SetFilter = (state, event) => ({...state, filter: event.target.value})\n```\n\nThe second argument to an action is known as the _payload_. Actions\ndispatched in response to an events on DOM elements receive the [event object](https://developer.mozilla.org/en-US/docs/Web/API/Event) for a payload. `event.target` refers to the input element in the DOM, and\n`event.target.value` refers to the current value entered into it.\n\nNow see what happens when you erase \"ocean\" and type \"friendly\" instead:\n\n\n\n\n### Actions with custom payloads\n\nNext up: selecting stories by clicking them in the list.\n\nThe following action sets the `reading` property in the state to a story-id, which amounts to \"selecting\" the story:\n\n```js\nconst SelectStory = (state, id) => ({...state, reading: id})\n```\n\nIt has a payload, but it's not an event object. It's a custom value telling us which\nstory was clicked. How are actions dispatched with custom payloads? – Like this:\n\n```js\n\nconst StoryThumbnail = props => h(\n \"li\",\n { \n onClick: [SelectStory, props.id], // <----\n class: {\n unread: props.unread,\n reading: props.reading,\n }\n },\n [\n h(\"p\", {class: \"title\"}, emphasize(props.filter, props.title)),\n h(\"p\", {class: \"author\"}, props.author)\n ]\n)\n```\n\nInstead of just specifying the action, we give a length-2 array with the action first and the custom payload second.\n\nSelecting stories works now, but the feature is not quite done. When a story is selected,\nwe need to set its `seen` property to `true`, so we can highlight which stories the user has yet to read. Update the `SelectStory` action:\n\n```js\nconst SelectStory = (state, id) => ({\n ...state, // keep all state the same, except for the following:\n reading: id,\n stories: {\n ...state.stories, //keep stories the same, except for:\n [id]: {\n ...state.stories[id], //keep this story the same, except for:\n seen: true,\n }\n }\n})\n```\n\nNow, when you select a blue-edged story it turns yellow because it is selected, and when you select something else,\nthe edge turns gray to indicate you've read the story.\n\n\n\n\n### Payload filters\n\nThere's one little thing we should fix about `SetFilter`. See how it's dependent on the complex `event` object? \nIt would be easier to test and reuse if it were simply:\n\n```js\nconst SetFilter = (state, word) => ({...state, filter: word})\n```\n\nBut we don't know the word beforehand, so how can we set it as a custom payload? Change the `Filter` view \nagain (last time - I promise!):\n\n```js\nconst Filter = props => h(\"div\", {class: \"filter\"}, [\n \"Filter:\",\n \n props.editingFilter\n ? h(\"input\", {\n type: \"text\",\n value: props.filter,\n onInput: [SetFilter, event => event.target.value], // <----\n })\n : h(\"span\", {class: \"filter-word\"}, props.filter),\n\n props.editingFilter\n ? h(\"button\", {onClick: StopEditingFilter}, \"\\u2713\") \n : h(\"button\", {onClick: StartEditingFilter}, \"\\u270E\"), \n])\n```\n\nWhen we give a _function_ as the custom payload, Hyperapp considers it a _payload filter_ and passes the default\npayload through it, providing the returned value as payload to the action.\n\n> Payload filters are also useful when you need a payload that is a combination of custom data and event data\n\nIf you'd like to see a working example of the code so far, have a look [here](https://codesandbox.io/s/hyperapp-tutorial-step-2-5yv34)\n\nEffects\n----------------------------\n\nUntil now, the list of stories has been defined in the state and doesn't change. What we really want is\nfor stories matching the filter to be dynamically loaded. When we click the check-mark button\n(indicating we are done editing the filter), we want to query an API and display the stories it responds with.\n\n### Actions can return effects\n\nAdd this import (to the \"IMPORTS\" section):\n\n```js\nimport {Http} from \"https:/unpkg.com/hyperapp-fx@next?module\"\n```\n\nUse the imported `Http` in the `StopEditingFilter` action like this:\n\n```js \nconst StopEditingFilter = state => [\n {\n ...state,\n editingFilter: false,\n },\n Http({ // <---\n url: `https://zaceno.github.io/hatut/data/${state.filter.toLowerCase()}.json`, // <---\n response: \"json\", // <---\n action: GotStories, // <---\n }) \n]\n```\n\nThe call to `Http(...)` does _not_ immediately execute the API request. `Http` is an _effect creator_. It returns\nan _effect_ bound to the options we provided. \n\nWhen Hyperapp sees an action return an array, it takes the first element of the array to be the new state, and the rest to\nbe _effects_. Effects are executed by Hyperapp as part of processing the action's return value.\n\n> Hyperapp provides effect creators for many common situations. If you've got an unusual case or are working\n> with less common APIs you may need to implement your own effects. Don't worry - it's easy! See the \n> [API reference]() for more information.\n\n### Effects can dispatch actions\n\nOne of the options we passed to `Http` was `action: GotStories`. The way this effect works is that when the response comes\nback from the api, an action named `GotStories` (yet to be implemented) will be dispatched, with the response body as the payload.\n\nThe response body is in json, but the payload will be a javascript object, thanks to the parsing hint `response: \"json\"`. It will look like this (although the details depend on your filter of course):\n\n```js\n{\n \"112\": {\n title: \"The Ocean is Sinking\",\n author: \"Kat Stropher\",\n },\n \"113\": {\n title: \"Ocean life is brutal\",\n author: \"Surphy McBrah\",\n },\n \"114\": {\n title: \"Family friendly fun at the ocean exhibit\",\n author: \"Guy Prosales\",\n }\n}\n```\n\nThe job of `GotStories` is to load this data into the state, in place of the stories we already have there. As it\ndoes, it should take care to remember which story was selected, and which stories we have seen, if they were already\nin the previous state. This will be our most complex action yet, and it could look like this:\n\n```js\nconst GotStories = (state, response) => {\n const stories = {}\n Object.keys(response).forEach(id => {\n stories[id] = {...response[id], seen: false}\n if (state.stories[id] && state.stories[id].seen) {\n stories[id].seen = true\n }\n })\n const reading = stories[state.reading] ? state.reading :  null\n return {\n ...state,\n stories,\n reading,\n }\n}\n```\n\nTry it out! Enter \"life\" in the filter input. When you click the check-mark button some new\nstories are loaded – all with blue edges except for \"Ocean life is brutal\" because it is\nstill selected.\n\n\n\n\n\n\n### Running effects on initialization\n\nThe next obvious step is to load the _initial_ stories from the API as well. Change init to this:\n\n\n```js\n init: [\n {\n editingFilter: false,\n autoUpdate: false,\n filter: \"ocean\",\n reading: null,\n stories: {}, // <---\n },\n Http({ // <---\n url: `https://zaceno.github.io/hatut/data/ocean.json`, // <---\n response: 'json', // <---\n action: GotStories, // <---\n })\n ],\n```\n\nHyperapp treats the init-value the same way as it treats return values from actions. By adding the `Http` effect\nin `init`, the app will fire the API request immediately, so we don't need the stories in the state from the start.\n\n\n\n\n### Tracking state for asynchronous effects\n\nIf we could display a spinner while we wait for stories to load, it would make for a smoother user experience. To\ndo that, we will need a new state property to tell us if we're waiting for a repsonse - and\nconsequently wether or not to render the spinner.\n\nCreate this action: \n\n```js\nconst FetchStories = state => [\n {...state, fetching: true},\n Http({\n url: `https://zaceno.github.io/hatut/data/${state.filter.toLowerCase()}.json`,\n response: 'json',\n action: GotStories,\n })\n]\n```\n\nInstead of dispatching this action, we will use it to simplify `StopEditingFilter`:\n\n```js \nconst StopEditingFilter = state => FetchStories({...state, editingFilter: false})\n```\n\n... and `init` as well:\n\n```js\n init: FetchStories({\n editingFilter: false,\n autoUpdate: false,\n filter: \"ocean\",\n reading: null,\n stories: {},\n }),\n```\n\nNow, when `StopEditingFilter` is dispatched, _and_ at initialization, the API call goes out and the\n`fetching` prop is set to `true`. Also, notice how we refactored out the repetitive use of `Http`.\n\nWe also need to set `fetching: false` in `GotStories`:\n\n```js\nconst GotStories = (state, response) => {\n const stories = {}\n Object.keys(response).forEach(id => {\n stories[id] = {...response[id], seen: false}\n if (state.stories[id] && state.stories[id].seen) {\n stories[id].seen = true\n }\n })\n const reading = stories[state.reading] ? state.reading :  null\n return {\n ...state,\n stories,\n reading,\n fetching: false, // <---\n }\n}\n```\n\nWith this, we know that when `fetching` is `true` we are waiting for a response, and should display\nthe spinner in the `StoryList` view:\n\n```js\nconst StoryList = props => h(\"div\", {class: \"stories\"}, [\n\n props.fetching && h(\"div\", {class: \"loadscreen\"}, [ // <---\n h(\"div\", {class: \"spinner\"}) // <---\n ]), // <---\n \n h(\"ul\", {}, Object.keys(props.stories).map(id => \n StoryThumbnail({\n id, \n title: props.stories[id].title,\n author: props.stories[id].author,\n unread: !props.stories[id].seen,\n reading: props.reading === id,\n filter: props.filter\n })\n ))\n])\n```\n\nWhen the app loads, and when you change the filter, you should see the spinner appear until the stories are loaded.\n\n\n\n> If you aren't seeing the spinner, it might just be happening too fast. Try choking your network speed. In the Chrome\n> browser you can set your network speed to \"slow 3g\" under the network tab in the developer tools.\n\nIf you'd like to see a working example of the code so far, have a look [here](https://codesandbox.io/s/hyperapp-tutorial-step-3-2mmug)\n\nSubscriptions\n-------------------------------------------------------------------\n\nThe last feature we'll add is to make our app periodically check for new stories matching the filter. There won't actually\nbe any because it's not a real service, but you'll know it's happening when you see the spinner pop up every five\nseconds. \n\nHowever, we want to make it opt-in. That's what the auto update checkbox at the bottom is for. We need a\nproperty in the state to track wether the box is checked or not. \n\nChange the `AutoUpdate` view:\n\n```js\nconst AutoUpdate = props => h(\"div\", {class: \"autoupdate\"}, [\n \"Auto update: \",\n h(\"input\", {\n type: \"checkbox\",\n checked: props.autoUpdate, // <---\n onInput: ToggleAutoUpdate, // <---\n })\n])\n```\n\nand implement the `ToggleAutoUpdate` action:\n\n```js\nconst ToggleAutoUpdate = state => ({...state, autoUpdate: !state.autoUpdate})\n```\n\nNow we've got `autoUpdate` in the state tracking the checkbox. All we need now, is to set up `FetchStories`\nto be dispatched every five seconds when `autoUpdate` is `true`.\n\nImport the `interval` _subscription creator_:\n\n```js\nimport {interval} from \"https://unpkg.com/@hyperapp/time?module\"\n```\n\nAdd a `subscriptions` property to your app, with a conditional declaration of `interval` like this:\n\n```js\n subscriptions: state => [\n state.autoUpdate && interval(FetchStories, {delay: 5000})\n ]\n```\n\nHyperapp will call `subscriptions` every time the state changes. If it notices a\nnew subscription, it will be started, or if one has been removed it will be stopped.\n\nThe options we passed to the `interval` subscription state that `FetchStories` should be dispatched every five seconds. It\nwill start when we check the auto update box, and stop when it is unchecked.\n\n\n\n> As with effects, Hyperapp offers subscriptions for the most common cases, but you\n> may need to implement your own. Refer to the [API reference](). Again, \n> it is no big deal - just not in scope for this tutorial.\n\nIf you'd like to see a working example of the final code, have a look [here](https://codesandbox.io/s/hyperapp-tutorial-step-4-8u9q8)\n\nConclusion\n------------------\n\nCongratulations on completing this Hyperapp tutorial!\n\nAlong the way you've familiarized yourself with\nthe core concepts: _view_, _state_, _actions_, _effects_ & _subscriptions_. And that's really all you need to\nbuild any web application.\n\n\n"} \ No newline at end of file diff --git a/quickstart.0e6b9c83.html b/quickstart.0e6b9c83.html new file mode 100644 index 0000000..30dc80e --- /dev/null +++ b/quickstart.0e6b9c83.html @@ -0,0 +1,3 @@ +

quickstart

1․ Install Hyperapp with npm or Yarn:


npm i hyperapp

2․ Then with a module bundler like Parcel or Webpack import it in your application and get right down to business.


import { h, app } from "hyperapp"

3․ Don't want to set up a build step? Import Hyperapp in a <script> tag as a module. Don't worry; modules are supported in all evergreen, self-updating desktop, and mobile browsers.


<script type="module">
+  import { h, app } from "https://unpkg.com/hyperapp"
+</script>

Here's the first example to get you started: a counter that can go up or down. You can try it online here.


\ No newline at end of file diff --git a/ref.md b/ref.md new file mode 100644 index 0000000..8ff4deb --- /dev/null +++ b/ref.md @@ -0,0 +1,16 @@ +# API + +> Technical reference for Hyperapp core APIs and packages. + +Under construction. See [Examples](examples.md). + +## Index + +- [Core]() + - [`h()`](#h-) + - [`app()`](#app-) + - [`Lazy()`](#Lazy-) +- [@hyperapp/time](#) +- [@hyperapp/http](#) +- [@hyperapp/events](#) +- [@hyperapp/random](#) diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..d0e5f1b --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ +# www.robotstxt.org/ + +# Allow crawling of all content +User-agent: * +Disallow: diff --git a/so-small-cant-even.9a03a356.svg b/so-small-cant-even.9a03a356.svg new file mode 100644 index 0000000..93d64d2 --- /dev/null +++ b/so-small-cant-even.9a03a356.svg @@ -0,0 +1,9 @@ + + + + SoSmallCantEven + Created with Sketch. + + + + \ No newline at end of file diff --git a/sponsor.ef925587.html b/sponsor.ef925587.html new file mode 100644 index 0000000..6f7b180 --- /dev/null +++ b/sponsor.ef925587.html @@ -0,0 +1 @@ +

sponsor

lorem lorem lorem

In urna ex, finibus sit amet laoreet id, pharetra placerat lorem. Suspendisse laoreet pulvinar nunc, sed tristique ex venenatis tristique. Quisque non vulputate enim, vitae facilisis sapien. Nunc sagittis vel mi et tristique. In ornare leo et lectus ornare, vel pretium odio vulputate. Nam rhoncus quam vel neque rhoncus rutrum. Quisque posuere, purus sit amet ornare blandit, massa ligula sagittis magna, ut interdum purus neque et nisl. Integer eros sapien, faucibus at est vel, rhoncus gravida arcu. In volutpat sapien neque, vel malesuada sapien aliquam at.

\ No newline at end of file diff --git a/src.07c797be.js b/src.07c797be.js new file mode 100644 index 0000000..4d4ad38 --- /dev/null +++ b/src.07c797be.js @@ -0,0 +1,80 @@ +parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c0)for(var r,t=0;tA)for(;k<=L;)t.insertBefore(y(x[k]=w(x[k++]),l,u),(s=z[C])&&s.node);else if(k>L)for(;C<=A;)t.removeChild(z[C++].node);else{b=C;for(var N={},E={};b<=A;b++)null!=(c=z[b].key)&&(N[c]=z[b]);for(;k<=L;)c=h(s=z[C]),d=h(x[k]=w(x[k],s)),E[c]||null!=d&&d===h(z[C+1])?(null==c&&t.removeChild(s.node),C++):null==d||o.type===e?(null==c&&(m(t,s&&s.node,s,x[k],l,u),k++),C++):(c===d?(m(t,s.node,s,x[k],l,u),E[d]=!0,C++):null!=(f=N[d])?(m(t,t.insertBefore(f.node,s&&s.node),f,x[k],l,u),E[d]=!0):m(t,s&&s.node,null,x[k],l,u),k++);for(;C<=A;)null==h(s=z[C++])&&t.removeChild(s.node);for(var b in N)null==E[b]&&t.removeChild(N[b].node)}}return i.node=t},g=function(e,n){for(var r in e)if(e[r]!==n[r])return!0;for(var r in n)if(e[r]!==n[r])return!0},z=function(e){return"object"==typeof e?e:C(e)},w=function(e,r){return e.type===n?((!r||!r.lazy||g(r.lazy,e.lazy))&&((r=z(e.lazy.view(e.lazy))).lazy=e.lazy),r):e},x=function(e,n,r,t,o,i){return{name:e,props:n,children:r,node:t,type:i,key:o}},C=function(e,n){return x(e,t,o,n,void 0,r)},k=function(n){return n.nodeType===r?C(n.nodeValue,n):x(n.nodeName.toLowerCase(),t,i.call(n.childNodes,k),n,void 0,e)},A=function(e){return{lazy:e,type:n}};exports.Lazy=A;var L=function(e,n){for(var r,o=[],i=[],u=arguments.length;u-- >2;)o.push(arguments[u]);for(;o.length>0;)if(l(r=o.pop()))for(u=r.length;u-- >0;)o.push(r[u]);else!1===r||!0===r||null==r||i.push(z(r));return n=n||t,"function"==typeof e?e(n,i):x(e,n,i,void 0,n.key)};exports.h=L;var b=function(e){var n={},r=!1,t=e.view,o=e.node,i=o&&k(o),f=e.subscriptions,a=[],c=function(e){v(this.actions[e.type],e)},d=function(e){return n!==e&&(n=e,f&&(a=p(a,s([f(n)]),v)),t&&!r&&u(y,r=!0)),n},v=(e.middleware||function(e){return e})(function(e,r){return"function"==typeof e?v(e(n,r)):l(e)?"function"==typeof e[0]||l(e[0])?v(e[0],"function"==typeof e[1]?e[1](r):e[1]):(s(e.slice(1)).map(function(e){e&&e[0](v,e[1])},d(e[0])),n):d(e)}),y=function(){r=!1,o=m(o.parentNode,o,i,i=z(t(n)),c)};v(e.init)};exports.app=b; +},{}],"LC7c":[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.request=void 0;var t=function(t){return function(e){return[t,e]}},e=t(function(t,e){var n=e.url,r=e.action,o=e.options||{},u=e.expect||"text";return fetch(n,o).then(function(t){if(!t.ok)throw t;return t}).then(function(t){return t[u]()}).then(function(e){t(r,e)}).catch(function(e){t(r,e)})});exports.request=e; +},{}],"AkZ9":[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.PopState=exports.WindowScrolled=void 0;var e=function(e){return function(t){return[e,t]}},t=e(function(e,t){var n=function(n){e([t.action,{ev:n,scrollY:window.scrollY}])};return addEventListener("scroll",n),function(){removeEventListener("scroll",n)}});exports.WindowScrolled=t;var n=e(function(e,t){var n=function(){e([t.action,window.location.pathname+window.location.search])};return addEventListener("popstate",n),function(){removeEventListener("popstate",n)}});exports.PopState=n; +},{}],"FOZT":[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getPathInfo=void 0;var e=function(e){var t=new URL(e,"http://localhost"),r=t.search,a=t.pathname,s=t.searchParams;return{path:"/"!==a?a.replace(/\/$/,""):a,query:r,queryParams:Object.fromEntries(s.entries())}};exports.getPathInfo=e; +},{}],"O6VD":[function(require,module,exports) { +var global = arguments[3]; +var e=arguments[3],a="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},n=function(e){var a=/\blang(?:uage)?-([\w-]+)\b/i,n=0,t={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof r?new r(e.type,t.util.encode(e.content),e.alias):Array.isArray(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(k instanceof r)){if(h&&b!=a.length-1){if(d.lastIndex=v,!(_=d.exec(e)))break;for(var w=_.index+(f&&_[1]?_[1].length:0),A=_.index+_[0].length,x=b,$=v,S=a.length;x"+n.content+""},!e.document)return e.addEventListener&&(t.disableWorkerMessageHandler||e.addEventListener("message",function(a){var n=JSON.parse(a.data),r=n.language,i=n.code,s=n.immediateClose;e.postMessage(t.highlight(i,t.languages[r],r)),s&&e.close()},!1)),t;var i=t.util.currentScript();if(i&&(t.filename=i.src,i.hasAttribute("data-manual")&&(t.manual=!0)),!t.manual){var s=function(){t.manual||t.highlightAll()},l=document.readyState;"loading"===l||"interactive"===l&&i&&i.defer?document.addEventListener("DOMContentLoaded",s):window.requestAnimationFrame?window.requestAnimationFrame(s):window.setTimeout(s,16)}return t}(a);"undefined"!=typeof module&&module.exports&&(module.exports=n),void 0!==e&&(e.Prism=n),n.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:(?!)*\]\s*)?>/i,greedy:!0},cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},n.languages.markup.tag.inside["attr-value"].inside.entity=n.languages.markup.entity,n.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(n.languages.markup.tag,"addInlined",{value:function(e,a){var t={};t["language-"+a]={pattern:/(^$)/i,lookbehind:!0,inside:n.languages[a]},t.cdata=/^$/i;var r={"included-cdata":{pattern://i,inside:t}};r["language-"+a]={pattern:/[\s\S]+/,inside:n.languages[a]};var i={};i[e]={pattern:RegExp("(<__[\\s\\S]*?>)(?:\\s*|[\\s\\S])*?(?=<\\/__>)".replace(/__/g,e),"i"),lookbehind:!0,greedy:!0,inside:r},n.languages.insertBefore("markup","cdata",i)}}),n.languages.xml=n.languages.extend("markup",{}),n.languages.html=n.languages.markup,n.languages.mathml=n.languages.markup,n.languages.svg=n.languages.markup,function(e){var a=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+[\s\S]*?(?:;|(?=\s*\{))/,inside:{rule:/@[\w-]+/}},url:{pattern:RegExp("url\\((?:"+a.source+"|[^\n\r()]*)\\)","i"),inside:{function:/^url/i,punctuation:/^\(|\)$/}},selector:RegExp("[^{}\\s](?:[^{};\"']|"+a.source+")*?(?=\\s*\\{)"),string:{pattern:a,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),e.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:n.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:e.languages.css}},alias:"language-css"}},n.tag))}(n),n.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},n.languages.javascript=n.languages.extend("clike",{"class-name":[n.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.])\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/--|\+\+|\*\*=?|=>|&&|\|\||[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?[.?]?|[~:]/}),n.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,n.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=\s*(?:$|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:n.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:n.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:n.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:n.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),n.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:n.languages.javascript}},string:/[\s\S]+/}}}),n.languages.markup&&n.languages.markup.tag.addInlined("script","javascript"),n.languages.js=n.languages.javascript; +},{}],"pWz5":[function(require,module,exports) { + +},{}],"wKh0":[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Focus=exports.HighLight=exports.UpdateHistory=void 0;var t=e(require("./lib/prism"));function e(t){return t&&t.__esModule?t:{default:t}}require("./lib/prism.css");var r=function(t,e){var r=e.to;history.pushState(null,"",r)},o=function(t){var e=t.to;return[r,{to:e}]};exports.UpdateHistory=o;var u=function(){setTimeout(function(){t.default.highlightAllUnder(document.body)},50)},i=function(){return[u]};exports.HighLight=i;var n=function(t,e){var r=e.selector;setTimeout(function(){var t=document.querySelector(r);t&&t.focus()},50)},s=function(t){var e=t.selector;return[n,{selector:e}]};exports.Focus=s; +},{"./lib/prism":"O6VD","./lib/prism.css":"pWz5"}],"U8fQ":[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Navigate=exports.SetSearchData=exports.ParseUrl=exports.WindowScroll=exports.CloseMenu=exports.OpenMenu=void 0;var e=require("./utils"),r=require("./effects");function t(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r&&(n=n.filter(function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable})),t.push.apply(t,n)}return t}function n(e){for(var r=1;r32})};exports.WindowScroll=u;var s=function(r,t){return n({},r,{location:(0,e.getPathInfo)(t)})};exports.ParseUrl=s;var a=function(e,r){return n({},e,{searchData:r})};exports.SetSearchData=a;var p=function(e,t){return[i(s(e,t)),[(0,r.UpdateHistory)({to:t}),(0,r.HighLight)()]]};exports.Navigate=p; +},{"./utils":"FOZT","./effects":"wKh0"}],"UTSh":[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;var e=require("hyperapp"),r=require("../../actions");function t(){return(t=Object.assign||function(e){for(var r=1;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(u[t]=e[t])}return u}function o(e,r){if(null==e)return{};var t,n,o={},u=Object.keys(e);for(n=0;n=0||(o[t]=e[t]);return o}var u=function(o,u){var a=o.to,i=n(o,["to"]);return(0,e.h)("a",t({href:a,onClick:[[r.Navigate,a],function(e){return e.preventDefault()}]},i),u)},a=u;exports.default=a; +},{"hyperapp":"xJOT","../../actions":"U8fQ"}],"V3Jl":[function(require,module,exports) { +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;var e=require("hyperapp"),r=t(require("../Link"));function t(e){return e&&e.__esModule?e:{default:e}}function n(){return(n=Object.assign||function(e){for(var r=1;rcode[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.site-header{display:flex;justify-content:space-between;padding:1rem;position:sticky;top:0;left:0;right:0;z-index:10}.logo{position:relative;z-index:15;display:flex;align-items:center}.logo .v2.mobile{height:2rem}.logo .v1,.logo .v2.desktop{display:none}.logo:hover .v2{display:none!important}.logo:hover .v1{display:block}.menu-toggler{position:relative;z-index:15;font-size:0;padding:0;border:none}.menu-toggler img{width:2.5rem}.menu-toggler:focus{background-color:transparent}.menu{position:fixed;top:0;left:0;background-color:#fff;width:100%;z-index:10;min-height:100vh;padding:5rem 1rem 1rem;display:none}.menu.opened{display:block}.menu .search-field,.menu a{display:block;font-size:3.375rem;font-weight:300;line-height:1.2;color:var(--text-grey);text-decoration:none;font-family:IBM Plex Sans,sans-serif}.menu a:hover{color:#546067}.menu a.active{color:var(--dark-blue)}.search-field{max-width:100%;width:100%;padding:0;color:#3d434e!important}.search-field:focus{outline:none}.search-field:valid{color:var(--dark-blue)}@media (min-width:640px){.site-header{padding:1rem 1.25rem}.menu{padding:6rem 1.25rem 2rem}}@media (min-width:992px){.site-header{position:fixed;padding:1.25rem;flex-direction:column;justify-content:flex-start;max-width:var(--menu-width)}.menu-toggler{display:none}.menu{position:relative;display:block;width:var(--menu-width);background:none;padding:0;min-height:auto;margin:.5rem 0}.logo .v2.mobile{display:none;height:2.5rem}.logo .v2.desktop{display:block;height:2.5rem}}footer{padding:1rem}footer nav{display:flex;flex-direction:column;align-items:flex-start}footer p{line-height:1}@media (min-width:992px){footer{padding:1.25rem;margin:1rem 0 1rem var(--menu-width)}}@media (min-width:992px) and (min-height:768px){footer{position:fixed;bottom:0;margin:1rem 0}}.live-example{margin:3rem 0}.live-example-tabs button{margin:0;padding:1rem .5rem;width:50%;border-color:transparent;border-bottom-color:var(--line-separator)}.live-example-tabs button.current{border-color:var(--line-separator);border-bottom-color:transparent}.live-example-tabs button:active{color:var(--primary-blue);background-color:transparent}.live-example-tabs button:focus{outline:none}.live-example-tabs .code{margin-right:-1px}.join-us{margin:3rem 0}.home-header{display:flex;align-items:center;margin:0 0 1rem}.home-header marquee{max-width:calc(100% - 6rem);margin:0 .5rem}.home-title{margin:0;max-width:32rem}.small-card h2{color:var(--primary-blue);font-size:2.625rem;margin-bottom:0}.small-card h5{margin-top:.5rem}.small-card img{display:none}.home-secondary-title{max-width:16rem;margin-top:6rem}.home-right-text img{margin-top:3rem}.home-grid{margin-bottom:6rem}.join-us-text{margin:3rem 0}@media (max-width:767px){.live-example>:not(.shown){display:none}}@media (min-width:768px){.live-example-tabs{display:none}.home-grid{display:grid;grid-template-columns:repeat(3,1fr);grid-column-gap:1.5rem;grid-row-gap:4rem;margin-top:8.25rem}.small-card{display:flex;flex-direction:column;align-items:flex-start;min-height:17rem}.small-card img{display:block}.small-card h2{font-size:3rem;margin-top:auto;margin-bottom:0}.small-card h5{margin-top:1.5rem}.home-right-text{grid-column:2/4}.home-right-text h2{margin-bottom:1rem}.home-right-text img{margin-top:7rem;margin-bottom:1rem}.live-example{display:grid;grid-template-columns:minmax(auto,calc(100% - 18rem)) minmax(18rem,auto);grid-template-rows:auto 1fr;grid-column-gap:4rem;grid-row-gap:1rem}.live-example pre{grid-row:1/3;margin:0}.live-example .counter-link{margin:1rem 0}}@media (min-width:992px){.home-header{margin:3rem 0 2rem}.home-header marquee{max-width:calc(100% - 16rem)}}.counter h1{margin-top:0}.four-oh-four-page{position:relative;overflow:hidden;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;min-height:50vh}.four-oh-four-page .code-background{position:fixed;top:0;bottom:0;left:0;right:0;opacity:.5;z-index:-1;pointer-events:none;animation-duration:5s;animation-name:background-translation;animation-timing-function:linear;animation-iteration-count:infinite}@keyframes background-translation{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@media (min-width:992px){.four-oh-four-page{min-height:100vh;margin:-1.25rem 0 -7.25rem -1.25rem}.four-oh-four-page .code-background{position:absolute}}.results{list-style:none;margin:0;padding:0}@media (min-width:992px){.search-results-page{margin-top:7rem;display:grid;grid-template-columns:33% auto;grid-template-rows:auto 1fr}.search-results-page h1{margin:0}.search-results-page>*{grid-column:1}.search-results-page .results{grid-column:2;grid-row:1/3}} \ No newline at end of file diff --git a/time-to-interactive.df8b26b1.svg b/time-to-interactive.df8b26b1.svg new file mode 100644 index 0000000..bd228fa --- /dev/null +++ b/time-to-interactive.df8b26b1.svg @@ -0,0 +1,30 @@ + + + + TimeToInteractive + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tutorial.9811de31.html b/tutorial.9811de31.html new file mode 100644 index 0000000..db60c52 --- /dev/null +++ b/tutorial.9811de31.html @@ -0,0 +1,501 @@ +

tutorial

===================================

Welcome! If you're new to Hyperapp, you've found the perfect place to start learning.

The Set-up

Together we'll build a simple newsreader-like application. As we do, we'll work +our way through the five core concepts: view, state, actions, effects and subscriptions.

To move things along, let's imagine we've already made a static version of the +app we want to build, with this HTML:

<div id="app" class="container">
+  <div class="filter">
+    Filter:
+    <span class="filter-word">ocean</span>
+    <button>&#9998;</button>
+  </div>
+  <div class="stories">
+    <ul>
+      <li class="unread">
+        <p class="title">The <em>Ocean </em>is Sinking</p>
+        <p class="author">Kat Stropher</p>
+      </li>
+      <li class="reading">
+        <p class="title"><em>Ocean </em>life is brutal</p>
+        <p class="author">Surphy McBrah</p>
+      </li>
+      <li>
+        <p class="title">
+          Family friendly fun at the
+          <em>ocean </em>exhibit
+        </p>
+        <p class="author">Guy Prosales</p>
+      </li>
+    </ul>
+  </div>
+  <div class="story">
+    <h1>Ocean life is brutal</h1>
+    <p>
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+      eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
+      ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+      aliquip ex ea commodo consequat.
+    </p>
+    <p class="signature">Surphy McBrah</p>
+  </div>
+  <div class="autoupdate">
+    Auto update:
+    <input type="checkbox" />
+  </div>
+</div>

...and some CSS here.

It looks like this:

We'll start by making Hyperapp render the HTML for us. Then we will +add dynamic behavior to all the widgets, including text input and dynamically fetching stories.

First, let's begin with the traditional "Hello World!"

Hello World

Create this html file:

<!doctype html>
+<html>
+  <head>
+    <link rel="stylesheet" href="https://zaceno.github.com/hatut/style.css">
+    <script type="module">
+
+// -- IMPORTS --
+
+import {h, app} from "https://unpkg.com/hyperapp?module"
+
+
+
+// -- ACTIONS --
+
+
+
+// -- VIEWS ---
+
+
+
+// -- RUN --
+
+app({
+  node: document.getElementById("app"),
+  view: () => h("h1", {}, [
+      "Hello ", 
+      h("i", {}, "World!")
+  ])
+})
+    </script>
+  </head> 
+  <body>
+    <div id="app"></div>
+  </body>
+</html>
+

The section structure outlined in the comments is not important. It's +just a suggestion for how to organize the code we'll be +adding throughout the tutorial.

+

Open it in a browser, and you'll be greeted with an optimistic Hello World!.

View

Let's step through what just happened.

Virtual Nodes

Hyperapp exports the app and h functions. +h is for creating virtual nodes, which is to say: plain javascript objects +which represent DOM nodes.

The result of

h("h1", {}, [
+  "Hello ", 
+  h("i", {}, "World!")
+])

is a virtual node, representing

<h1>
+  Hello 
+  <i>World!</i>
+</h1>

Rendering to the DOM

app is the function that runs our app. It is called with a single argument - an object +which can take several properties. For now we're just concerned with view and node.

Hyperapp calls the view function which tells it the DOM structure we want, in the form +of virtual nodes. Hyperapp proceeds to create it for us, replacing the node specified in node.

To render the HTML we want, change the view to:

view: () => h("div", {id: "app", class: "container"}, [
+  h("div", {class: "filter"}, [
+    " Filter: ",
+    h("span", {class: "filter-word"}, "ocean"),
+    h("button", {}, "\u270E")
+  ]),
+  h("div", {class: "stories"}, [
+    h("ul", {}, [
+      h("li", {class: "unread"}, [
+        h("p", {class: "title"}, [
+          "The ",
+          h("em", {}, "Ocean"),
+          " is Sinking!"
+        ]),
+        h("p", {class: "author"}, "Kat Stropher")
+      ]),
+      h("li", {class: "reading"}, [
+        h("p", {class: "title"}, [
+          h("em", {}, "Ocean"),
+          " life is brutal"
+        ]),
+        h("p", {class: "author"}, "Surphy McBrah"),
+      ]),
+      h("li", {}, [
+        h("p", {class: "title"}, [
+          "Family friendly fun at the ",
+          h("em", {}, "ocean"),
+          " exhibit"
+        ]),
+        h("p", {class: "author"}, "Guy Prosales")
+      ])
+    ])
+  ]),
+  h("div", {class: "story"}, [
+    h("h1", {}, "Ocean life is brutal"),
+    h("p", {}, `
+      Lorem ipsum dolor sit amet, consectetur adipiscing
+      elit, sed do eiusmod tempor incididunt ut labore et
+      dolore magna aliqua. Ut enim ad minim veniam, quis
+      nostrud exercitation ullamco laboris nisi ut aliquip
+      ex ea commodo consequat.
+    `),
+    h("p", {class: "signature"}, "Surphy McBrah")
+  ]),
+  h("div", {class: "autoupdate"}, [
+    "Auto update: ",
+    h("input", {type: "checkbox"})
+  ])
+]),

Try it out to confirm that the result matches the screenshot above.

+

In many frameworks it is common to write your views/templates +using syntax that looks like HTML. This is possible with Hyperapp as well. +JSX can compile a HTML-like syntax into h calls at build-time. If you'd rather +not use a build system, htm does the same at run-time.

+

In this tutorial we'll stick with h to keep it simple and close to the metal.

+

Composing the view with reusable functions

The great thing about using plain functions to build up our virtual DOM +is that we can break out repetitive or complicated parts into their own functions.

Add this function (in the "VIEWS" section):

const emphasize = (word, string) =>
+  string.split(" ").map(x => {
+    if (x.toLowerCase() === word.toLowerCase()) {
+      return h("em", {}, x + " ")
+    } else {
+      return x + " "
+    }
+  }) 

It lets you change this:

  ...
+  h("p", {class: "title"}, [
+    "The ",
+    h("em", {}, "Ocean"),
+    " is Sinking!"
+  ]),
+  ...

into this:

  ...
+  h("p", {class: "title"}, emphasize("ocean",
+    "The Ocean is Sinking"
+  ))
+  ...

Story thumbnails are repeated several times, so encapsulate +them in their own function:

const StoryThumbnail = props => h(
+  "li",
+  {class: {
+      unread: props.unread,
+      reading: props.reading,
+  }},
+  [
+    h("p", {class: "title"}, emphasize(props.filter, props.title)),
+    h("p", {class: "author"}, props.author)
+  ]
+)
+

The last example demonstrates a helpful feature of the class property. When +you set it to an object rather than a string, each key with a truthy value +will become a class in the class list.

+

Continue by creating functions for each section of the view:


+const StoryList = props => h("div", {class: "stories"}, [
+  h("ul", {}, Object.keys(props.stories).map(id =>
+    StoryThumbnail({
+      id,
+      title: props.stories[id].title,
+      author: props.stories[id].author,
+      unread: !props.stories[id].seen,
+      reading: props.reading === id,
+      filter: props.filter,
+    })
+  ))
+])
+
+const Filter = props => h("div", {class: "filter"}, [
+  "Filter:",
+  h("span", {class: "filter-word"}, props.filter),
+  h("button", {}, "\u270E")
+])
+
+const StoryDetail = props => h("div", {class: "story"}, [
+  props && h("h1", {}, props.title),
+  props && h("p", {}, `
+    Lorem ipsum dolor sit amet, consectetur adipiscing
+    elit, sed do eiusmod tempor incididunt ut labore et
+    dolore magna aliqua. Ut enim ad minim veniam, qui
+    nostrud exercitation ullamco laboris nisi ut aliquip
+    ex ea commodo consequat.
+  `),
+   props && h("p", {class: "signature"}, props.author)
+])
+
+const AutoUpdate = props => h("div", {class: "autoupdate"}, [
+  "Auto update: ",
+  h("input", {type: "checkbox"})
+])
+
+const Container = content => h("div", {class: "container"}, content)
+

With those the view can be written as:

view: () => Container([
+  Filter({
+    filter: "ocean"
+  }),
+  StoryList({
+    stories: {
+      "112": {
+        title: "The Ocean is Sinking",
+        author: "Kat Stropher",
+        seen: false,
+      },
+      "113": {
+        title: "Ocean life is brutal",
+        author: "Surphy McBrah",
+        seen: true,
+      },
+      "114": {
+        title: "Family friendly fun at the ocean exhibit",
+        author: "Guy Prosales",
+        seen: true,
+      }
+    },
+    reading: "113",
+    filter: "ocean"
+  }),
+  StoryDetail({
+    title: "Ocean life is brutal",
+    author: "Surphy McBrah",
+  }),
+  AutoUpdate(),
+])

What you see on the page should be exactly the same as before, because we haven't +changed what view returns. Using basic functional composition, we were able to make +the code a bit more manageable, and that's the only difference.

State

With all that view logic broken out in separate functions, view is starting to look like +plain data. The next step is to fully separate data from the view.

Add an init property to your app, with this pure data:

  init: {
+    filter: "ocean",
+    reading: "113",
+    stories: {
+      "112": {
+        title: "The Ocean is Sinking",
+        author: "Kat Stropher",
+        seen: false,
+      },
+      "113": {
+        title: "Ocean life is brutal",
+        author: "Surphy McBrah",
+        seen: true,
+      },
+      "114": {
+        title: "Family friendly fun at the ocean exhibit",
+        author: "Guy Prosales",
+        seen: true,
+      }
+    }
+  },

The value of init becomes the app's state. Hyperapp calls view with the state +as an argument, so it can be reduced to:

  view: state => Container([
+    Filter(state),
+    StoryList(state),
+    StoryDetail(state.reading && state.stories[state.reading]),
+    AutoUpdate(state),
+  ]),

Visually, everything is still the same. If you'd like to see a working example of the code so far, have a look here

Actions

Now that we know all about rendering views, it's finally time for some action!

Reacting to events in the DOM

The first bit of dynamic behavior we will add is so that when you click +the pencil-button, a text input with the filter word appears.

Add an onClick property to the button in the filter view:

const Filter = props => h("div", {class: "filter"}, [
+  "Filter:",
+  h("span", {class: "filter-word"}, props.filter),
+  h("button", { onClick: StartEditingFilter }, "\u270E") // <---
+])

This makes Hyperapp bind a click-event handler on the button element, so +that when the button is clicked, an action named StartEditingFilter is +dispatched. Create the action in the "ACTIONS" section:

const StartEditingFilter = state => ({...state, editingFilter: true})

Actions are just functions describing transformations of the state. +This action keeps everything in the state the same except for editingFilter +which it sets to true.

When Hyperapp dispatches an action, it replaces the old state with the new +one calculated using the action. Then the DOM is modified to match what the +view returns for this new state.

When editingFilter is true, we want to have a text input instead of a span with the filter word. We can express this in the Filter view using a +ternary operator (a ? b : c).

const Filter = props => h("div", {class: "filter"}, [
+  "Filter:",
+
+  props.editingFilter                               // <---
+  ? h("input", {type: "text", value: props.filter}) // <---
+  : h("span", {class: "filter-word"}, props.filter),
+
+  h("button", { onClick: StartEditingFilter }, "\u270E")
+])

Now, when you click the pencil button the text input appears. But we still need to add +a way to go back. We need an action to StopEditingFilter, and a button to dispatch it.

Add the action:

const StopEditingFilter = state => ({...state, editingFilter: false})

and update the Filter view again:

const Filter = props => h("div", {class: "filter"}, [
+  "Filter:",
+
+  props.editingFilter
+  ? h("input", {type: "text", value: props.filter})
+  : h("span", {class: "filter-word"}, props.filter),
+
+  props.editingFilter                                      // <---
+  ? h("button", {onClick: StopEditingFilter}, "\u2713") 
+  : h("button", {onClick: StartEditingFilter}, "\u270E"),  // <---
+])

When you click the pencil button, it is replaced with a check-mark button that can take you back to the first state.

Capturing event-data in actions

The next step is to use the input for editing the filter word. Whatever we +type in the box should be emphasized in the story-list.

Update the Filter view yet again:

const Filter = props => h("div", {class: "filter"}, [
+  "Filter:",
+
+  props.editingFilter
+  ? h("input", {
+    type: "text",
+    value: props.filter,
+    onInput: SetFilter,   // <----
+  })
+  : h("span", {class: "filter-word"}, props.filter),
+
+  props.editingFilter
+  ? h("button", {onClick: StopEditingFilter}, "\u2713") 
+  : h("button", {onClick: StartEditingFilter}, "\u270E"), 
+])

This will dispatch the SetFilter action everytime someone types in the input. Implement the action like this:

const SetFilter = (state, event) => ({...state, filter: event.target.value})

The second argument to an action is known as the payload. Actions +dispatched in response to an events on DOM elements receive the event object for a payload. event.target refers to the input element in the DOM, and +event.target.value refers to the current value entered into it.

Now see what happens when you erase "ocean" and type "friendly" instead:

Actions with custom payloads

Next up: selecting stories by clicking them in the list.

The following action sets the reading property in the state to a story-id, which amounts to "selecting" the story:

const SelectStory = (state, id) => ({...state, reading: id})

It has a payload, but it's not an event object. It's a custom value telling us which +story was clicked. How are actions dispatched with custom payloads? – Like this:


+const StoryThumbnail = props => h(
+  "li",
+  { 
+    onClick: [SelectStory, props.id], // <----
+    class: {
+      unread: props.unread,
+      reading: props.reading,
+    }
+  },
+  [
+    h("p", {class: "title"}, emphasize(props.filter, props.title)),
+    h("p", {class: "author"}, props.author)
+  ]
+)

Instead of just specifying the action, we give a length-2 array with the action first and the custom payload second.

Selecting stories works now, but the feature is not quite done. When a story is selected, +we need to set its seen property to true, so we can highlight which stories the user has yet to read. Update the SelectStory action:

const SelectStory = (state, id) => ({
+  ...state, // keep all state the same, except for the following:
+  reading: id,
+  stories: {
+    ...state.stories, //keep stories the same, except for:
+    [id]: {
+      ...state.stories[id],  //keep this story the same, except for:
+      seen: true,
+    }
+  }
+})

Now, when you select a blue-edged story it turns yellow because it is selected, and when you select something else, +the edge turns gray to indicate you've read the story.

Payload filters

There's one little thing we should fix about SetFilter. See how it's dependent on the complex event object? It would be easier to test and reuse if it were simply:

const SetFilter = (state, word) => ({...state, filter: word})

But we don't know the word beforehand, so how can we set it as a custom payload? Change the Filter view again (last time - I promise!):

const Filter = props => h("div", {class: "filter"}, [
+  "Filter:",
+
+  props.editingFilter
+  ? h("input", {
+    type: "text",
+    value: props.filter,
+    onInput: [SetFilter, event => event.target.value],   // <----
+  })
+  : h("span", {class: "filter-word"}, props.filter),
+
+  props.editingFilter
+  ? h("button", {onClick: StopEditingFilter}, "\u2713") 
+  : h("button", {onClick: StartEditingFilter}, "\u270E"), 
+])

When we give a function as the custom payload, Hyperapp considers it a payload filter and passes the default +payload through it, providing the returned value as payload to the action.

+

Payload filters are also useful when you need a payload that is a combination of custom data and event data

+

If you'd like to see a working example of the code so far, have a look here

Effects

Until now, the list of stories has been defined in the state and doesn't change. What we really want is +for stories matching the filter to be dynamically loaded. When we click the check-mark button +(indicating we are done editing the filter), we want to query an API and display the stories it responds with.

Actions can return effects

Add this import (to the "IMPORTS" section):

import {Http} from "https:/unpkg.com/hyperapp-fx@next?module"

Use the imported Http in the StopEditingFilter action like this:

const StopEditingFilter = state => [
+  {
+    ...state,
+    editingFilter: false,
+  },
+  Http({                                                                            // <---
+    url: `https://zaceno.github.io/hatut/data/${state.filter.toLowerCase()}.json`,  // <---
+    response: "json",                                                               // <---
+    action: GotStories,                                                             // <---
+  })    
+]

The call to Http(...) does not immediately execute the API request. Http is an effect creator. It returns +an effect bound to the options we provided.

When Hyperapp sees an action return an array, it takes the first element of the array to be the new state, and the rest to +be effects. Effects are executed by Hyperapp as part of processing the action's return value.

+

Hyperapp provides effect creators for many common situations. If you've got an unusual case or are working +with less common APIs you may need to implement your own effects. Don't worry - it's easy! See the API reference for more information.

+

Effects can dispatch actions

One of the options we passed to Http was action: GotStories. The way this effect works is that when the response comes +back from the api, an action named GotStories (yet to be implemented) will be dispatched, with the response body as the payload.

The response body is in json, but the payload will be a javascript object, thanks to the parsing hint response: "json". It will look like this (although the details depend on your filter of course):

{
+  "112": {
+    title: "The Ocean is Sinking",
+    author: "Kat Stropher",
+  },
+  "113": {
+    title: "Ocean life is brutal",
+    author: "Surphy McBrah",
+  },
+  "114": {
+    title: "Family friendly fun at the ocean exhibit",
+    author: "Guy Prosales",
+  }
+}

The job of GotStories is to load this data into the state, in place of the stories we already have there. As it +does, it should take care to remember which story was selected, and which stories we have seen, if they were already +in the previous state. This will be our most complex action yet, and it could look like this:

const GotStories = (state, response) => {
+  const stories = {}
+  Object.keys(response).forEach(id => {
+    stories[id] = {...response[id], seen: false}
+    if (state.stories[id] && state.stories[id].seen) {
+      stories[id].seen = true
+    }
+  })
+  const reading = stories[state.reading] ? state.reading :  null
+  return {
+    ...state,
+    stories,
+    reading,
+  }
+}

Try it out! Enter "life" in the filter input. When you click the check-mark button some new +stories are loaded – all with blue edges except for "Ocean life is brutal" because it is +still selected.

Running effects on initialization

The next obvious step is to load the initial stories from the API as well. Change init to this:

  init: [
+    {
+      editingFilter: false,
+      autoUpdate: false,
+      filter: "ocean",
+      reading: null,
+      stories: {},                                            // <---
+    },
+    Http({                                                    // <---
+      url: `https://zaceno.github.io/hatut/data/ocean.json`,  // <---
+      response: 'json',                                       // <---
+      action: GotStories,                                     // <---
+    })
+  ],

Hyperapp treats the init-value the same way as it treats return values from actions. By adding the Http effect +in init, the app will fire the API request immediately, so we don't need the stories in the state from the start.

Tracking state for asynchronous effects

If we could display a spinner while we wait for stories to load, it would make for a smoother user experience. To +do that, we will need a new state property to tell us if we're waiting for a repsonse - and +consequently wether or not to render the spinner.

Create this action:

const FetchStories = state => [
+  {...state, fetching: true},
+  Http({
+    url: `https://zaceno.github.io/hatut/data/${state.filter.toLowerCase()}.json`,
+    response: 'json',
+    action: GotStories,
+  })
+]

Instead of dispatching this action, we will use it to simplify StopEditingFilter:

const StopEditingFilter = state => FetchStories({...state, editingFilter: false})

... and init as well:

  init: FetchStories({
+    editingFilter: false,
+    autoUpdate: false,
+    filter: "ocean",
+    reading: null,
+    stories: {},
+  }),

Now, when StopEditingFilter is dispatched, and at initialization, the API call goes out and the +fetching prop is set to true. Also, notice how we refactored out the repetitive use of Http.

We also need to set fetching: false in GotStories:

const GotStories = (state, response) => {
+  const stories = {}
+  Object.keys(response).forEach(id => {
+    stories[id] = {...response[id], seen: false}
+    if (state.stories[id] && state.stories[id].seen) {
+      stories[id].seen = true
+    }
+  })
+  const reading = stories[state.reading] ? state.reading :  null
+  return {
+    ...state,
+    stories,
+    reading,
+    fetching: false,          // <---
+  }
+}

With this, we know that when fetching is true we are waiting for a response, and should display +the spinner in the StoryList view:

const StoryList = props => h("div", {class: "stories"}, [
+
+  props.fetching && h("div", {class: "loadscreen"}, [  // <---
+    h("div", {class: "spinner"})                       // <---
+  ]),                                                  // <---
+
+  h("ul", {}, Object.keys(props.stories).map(id => 
+    StoryThumbnail({
+      id,    
+      title: props.stories[id].title,
+      author: props.stories[id].author,
+      unread: !props.stories[id].seen,
+      reading: props.reading === id,
+      filter: props.filter
+    })
+  ))
+])

When the app loads, and when you change the filter, you should see the spinner appear until the stories are loaded.

+

If you aren't seeing the spinner, it might just be happening too fast. Try choking your network speed. In the Chrome +browser you can set your network speed to "slow 3g" under the network tab in the developer tools.

+

If you'd like to see a working example of the code so far, have a look here

Subscriptions

The last feature we'll add is to make our app periodically check for new stories matching the filter. There won't actually +be any because it's not a real service, but you'll know it's happening when you see the spinner pop up every five +seconds.

However, we want to make it opt-in. That's what the auto update checkbox at the bottom is for. We need a +property in the state to track wether the box is checked or not.

Change the AutoUpdate view:

const AutoUpdate = props => h("div", {class: "autoupdate"}, [
+  "Auto update: ",
+  h("input", {
+    type: "checkbox",
+    checked: props.autoUpdate, // <---
+    onInput: ToggleAutoUpdate, // <---
+  })
+])

and implement the ToggleAutoUpdate action:

const ToggleAutoUpdate = state => ({...state, autoUpdate: !state.autoUpdate})

Now we've got autoUpdate in the state tracking the checkbox. All we need now, is to set up FetchStories +to be dispatched every five seconds when autoUpdate is true.

Import the interval subscription creator:

import {interval} from "https://unpkg.com/@hyperapp/time?module"

Add a subscriptions property to your app, with a conditional declaration of interval like this:

  subscriptions: state => [
+    state.autoUpdate && interval(FetchStories, {delay: 5000})
+  ]

Hyperapp will call subscriptions every time the state changes. If it notices a +new subscription, it will be started, or if one has been removed it will be stopped.

The options we passed to the interval subscription state that FetchStories should be dispatched every five seconds. It +will start when we check the auto update box, and stop when it is unchecked.

+

As with effects, Hyperapp offers subscriptions for the most common cases, but you +may need to implement your own. Refer to the API reference. Again, it is no big deal - just not in scope for this tutorial.

+

If you'd like to see a working example of the final code, have a look here

Conclusion

Congratulations on completing this Hyperapp tutorial!

Along the way you've familiarized yourself with +the core concepts: view, state, actions, effects & subscriptions. And that's really all you need to +build any web application.

\ No newline at end of file diff --git a/tutorial.md b/tutorial.md new file mode 100644 index 0000000..aefa0ab --- /dev/null +++ b/tutorial.md @@ -0,0 +1,915 @@ +Tutorial +=================================== + +Welcome! If you're new to Hyperapp, you've found the perfect place to start learning. + +The Set-up +----------------------------------- + +Together we'll build a simple newsreader-like application. As we do, we'll work +our way through the five core concepts: view, state, actions, effects and subscriptions. + +To move things along, let's imagine we've already made a static version of the +app we want to build, with this HTML: + + +```html +
+
+ Filter: + ocean + +
+
+
    +
  • +

    The Ocean is Sinking

    +

    Kat Stropher

    +
  • +
  • +

    Ocean life is brutal

    +

    Surphy McBrah

    +
  • +
  • +

    + Family friendly fun at the + ocean exhibit +

    +

    Guy Prosales

    +
  • +
+
+
+

Ocean life is brutal

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. +

+

Surphy McBrah

+
+
+ Auto update: + +
+
+``` + +...and some CSS [here](https://zaceno.github.com/hatut/style.css). + +It looks like this: + +![Initial static mockup](https://user-images.githubusercontent.com/6243887/73389558-15d97580-42dd-11ea-90fa-f79a2c351fe8.png) + +We'll start by making Hyperapp render the HTML for us. Then we will +add dynamic behavior to all the widgets, including text input and +dynamically fetching stories. + +First, let's begin with the traditional "Hello World!" + +Hello World +------------------------------ + +Create this html file: + +```html + + + + + + + +
+ + +``` + +> The section structure outlined in the comments is not important. It's +> just a suggestion for how to organize the code we'll be +> adding throughout the tutorial. + +Open it in a browser, and you'll be greeted with an optimistic **Hello _World!_**. + +View +------------------------------------ + +Let's step through what just happened. + +### Virtual Nodes + +Hyperapp exports the `app` and `h` functions. +`h` is for creating _virtual nodes_, which is to say: plain javascript objects +which _represent_ DOM nodes. + +The result of + +```js +h("h1", {}, [ + "Hello ", + h("i", {}, "World!") +]) +``` + +is a virtual node, representing + +```html +

+ Hello + World! +

+``` + +### Rendering to the DOM + +`app` is the function that runs our app. It is called with a single argument - an object +which can take several properties. For now we're just concerned with `view` and `node. ` + +Hyperapp calls the `view` function which tells it the DOM structure we want, in the form +of virtual nodes. Hyperapp proceeds to create it for us, replacing the node specified in `node`. + +To render the HTML we want, change the `view` to: + +```js +view: () => h("div", {id: "app", class: "container"}, [ + h("div", {class: "filter"}, [ + " Filter: ", + h("span", {class: "filter-word"}, "ocean"), + h("button", {}, "\u270E") + ]), + h("div", {class: "stories"}, [ + h("ul", {}, [ + h("li", {class: "unread"}, [ + h("p", {class: "title"}, [ + "The ", + h("em", {}, "Ocean"), + " is Sinking!" + ]), + h("p", {class: "author"}, "Kat Stropher") + ]), + h("li", {class: "reading"}, [ + h("p", {class: "title"}, [ + h("em", {}, "Ocean"), + " life is brutal" + ]), + h("p", {class: "author"}, "Surphy McBrah"), + ]), + h("li", {}, [ + h("p", {class: "title"}, [ + "Family friendly fun at the ", + h("em", {}, "ocean"), + " exhibit" + ]), + h("p", {class: "author"}, "Guy Prosales") + ]) + ]) + ]), + h("div", {class: "story"}, [ + h("h1", {}, "Ocean life is brutal"), + h("p", {}, ` + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. + `), + h("p", {class: "signature"}, "Surphy McBrah") + ]), + h("div", {class: "autoupdate"}, [ + "Auto update: ", + h("input", {type: "checkbox"}) + ]) +]), +``` + +Try it out to confirm that the result matches the screenshot above. + +> In many frameworks it is common to write your views/templates +> using syntax that looks like HTML. This is possible with Hyperapp as well. +> [JSX](https://babeljs.io/docs/en/babel-plugin-transform-react-jsx) +> can compile a HTML-like syntax into `h` calls at build-time. If you'd rather +> not use a build system, [htm](https://github.com/developit/htm) does the same at run-time. +> +> In this tutorial we'll stick with `h` to keep it simple and close to the metal. + +### Composing the view with reusable functions + +The great thing about using plain functions to build up our virtual DOM +is that we can break out repetitive or complicated parts into their own functions. + +Add this function (in the "VIEWS" section): + +```js +const emphasize = (word, string) => + string.split(" ").map(x => { + if (x.toLowerCase() === word.toLowerCase()) { + return h("em", {}, x + " ") + } else { + return x + " " + } + }) +``` + +It lets you change this: + +```js + ... + h("p", {class: "title"}, [ + "The ", + h("em", {}, "Ocean"), + " is Sinking!" + ]), + ... +``` + +into this: + +```js + ... + h("p", {class: "title"}, emphasize("ocean", + "The Ocean is Sinking" + )) + ... +``` + +Story thumbnails are repeated several times, so encapsulate +them in their own function: + +```js +const StoryThumbnail = props => h( + "li", + {class: { + unread: props.unread, + reading: props.reading, + }}, + [ + h("p", {class: "title"}, emphasize(props.filter, props.title)), + h("p", {class: "author"}, props.author) + ] +) +``` + +> The last example demonstrates a helpful feature of the `class` property. When +> you set it to an object rather than a string, each key with a truthy value +> will become a class in the class list. + +Continue by creating functions for each section of the view: + +```js + +const StoryList = props => h("div", {class: "stories"}, [ + h("ul", {}, Object.keys(props.stories).map(id => + StoryThumbnail({ + id, + title: props.stories[id].title, + author: props.stories[id].author, + unread: !props.stories[id].seen, + reading: props.reading === id, + filter: props.filter, + }) + )) +]) + +const Filter = props => h("div", {class: "filter"}, [ + "Filter:", + h("span", {class: "filter-word"}, props.filter), + h("button", {}, "\u270E") +]) + +const StoryDetail = props => h("div", {class: "story"}, [ + props && h("h1", {}, props.title), + props && h("p", {}, ` + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, qui + nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. + `), + props && h("p", {class: "signature"}, props.author) +]) + +const AutoUpdate = props => h("div", {class: "autoupdate"}, [ + "Auto update: ", + h("input", {type: "checkbox"}) +]) + +const Container = content => h("div", {class: "container"}, content) + +``` + + +With those the view can be written as: + +```js +view: () => Container([ + Filter({ + filter: "ocean" + }), + StoryList({ + stories: { + "112": { + title: "The Ocean is Sinking", + author: "Kat Stropher", + seen: false, + }, + "113": { + title: "Ocean life is brutal", + author: "Surphy McBrah", + seen: true, + }, + "114": { + title: "Family friendly fun at the ocean exhibit", + author: "Guy Prosales", + seen: true, + } + }, + reading: "113", + filter: "ocean" + }), + StoryDetail({ + title: "Ocean life is brutal", + author: "Surphy McBrah", + }), + AutoUpdate(), +]) +``` + +What you see on the page should be exactly the same as before, because we haven't +changed what `view` returns. Using basic functional composition, we were able to make +the code a bit more manageable, and that's the only difference. + +State +------------------------------- + +With all that view logic broken out in separate functions, `view` is starting to look like +plain _data_. The next step is to fully separate data from the view. + +Add an `init` property to your app, with this pure data: + +```js + init: { + filter: "ocean", + reading: "113", + stories: { + "112": { + title: "The Ocean is Sinking", + author: "Kat Stropher", + seen: false, + }, + "113": { + title: "Ocean life is brutal", + author: "Surphy McBrah", + seen: true, + }, + "114": { + title: "Family friendly fun at the ocean exhibit", + author: "Guy Prosales", + seen: true, + } + } + }, +``` + +The value of `init` becomes the app's _state_. Hyperapp calls `view` with the state +as an argument, so it can be reduced to: + +```js + view: state => Container([ + Filter(state), + StoryList(state), + StoryDetail(state.reading && state.stories[state.reading]), + AutoUpdate(state), + ]), +``` + +Visually, everything is _still_ the same. If you'd like to see a working example of the code so far, have a look [here](https://codesandbox.io/s/hyperapp-tutorial-step-1-gq662). + +Actions +--------------------- + +Now that we know all about rendering views, it's finally time for some _action_! + +### Reacting to events in the DOM + +The first bit of dynamic behavior we will add is so that when you click +the pencil-button, a text input with the filter word appears. + +Add an `onClick` property to the button in the filter view: + +```js +const Filter = props => h("div", {class: "filter"}, [ + "Filter:", + h("span", {class: "filter-word"}, props.filter), + h("button", { onClick: StartEditingFilter }, "\u270E") // <--- +]) +``` + +This makes Hyperapp bind a click-event handler on the button element, so +that when the button is clicked, an action named `StartEditingFilter` is +_dispatched_. Create the action in the "ACTIONS" section: + +```js +const StartEditingFilter = state => ({...state, editingFilter: true}) +``` + +Actions are just functions describing transformations of the state. +This action keeps everything in the state the same except for `editingFilter` +which it sets to `true`. + +When Hyperapp dispatches an action, it replaces the old state with the new +one calculated using the action. Then the DOM is modified to match what the +view returns for this new state. + +When `editingFilter` is true, we want to have a text input instead of a +span with the filter word. We can express this in the `Filter` view using a +ternary operator (`a ? b : c`). + +```js +const Filter = props => h("div", {class: "filter"}, [ + "Filter:", + + props.editingFilter // <--- + ? h("input", {type: "text", value: props.filter}) // <--- + : h("span", {class: "filter-word"}, props.filter), + + h("button", { onClick: StartEditingFilter }, "\u270E") +]) +``` + +Now, when you click the pencil button the text input appears. But we still need to add +a way to go back. We need an action to `StopEditingFilter`, and a button to dispatch it. + +Add the action: + +```js +const StopEditingFilter = state => ({...state, editingFilter: false}) +``` + +and update the `Filter` view again: + +```js +const Filter = props => h("div", {class: "filter"}, [ + "Filter:", + + props.editingFilter + ? h("input", {type: "text", value: props.filter}) + : h("span", {class: "filter-word"}, props.filter), + + props.editingFilter // <--- + ? h("button", {onClick: StopEditingFilter}, "\u2713") + : h("button", {onClick: StartEditingFilter}, "\u270E"), // <--- +]) +``` + +When you click the pencil button, it is replaced with a check-mark button that can take you back to the first state. + +![Filter in edit mode](https://user-images.githubusercontent.com/6243887/73389562-1a059300-42dd-11ea-80ea-631c999d5f62.png) + + +### Capturing event-data in actions + +The next step is to use the input for editing the filter word. Whatever we +type in the box should be emphasized in the story-list. + +Update the `Filter` view yet again: + +```js +const Filter = props => h("div", {class: "filter"}, [ + "Filter:", + + props.editingFilter + ? h("input", { + type: "text", + value: props.filter, + onInput: SetFilter, // <---- + }) + : h("span", {class: "filter-word"}, props.filter), + + props.editingFilter + ? h("button", {onClick: StopEditingFilter}, "\u2713") + : h("button", {onClick: StartEditingFilter}, "\u270E"), +]) +``` + +This will dispatch the `SetFilter` action everytime someone types in the input. Implement the action like this: + +```js +const SetFilter = (state, event) => ({...state, filter: event.target.value}) +``` + +The second argument to an action is known as the _payload_. Actions +dispatched in response to an events on DOM elements receive the [event object](https://developer.mozilla.org/en-US/docs/Web/API/Event) for a payload. `event.target` refers to the input element in the DOM, and +`event.target.value` refers to the current value entered into it. + +Now see what happens when you erase "ocean" and type "friendly" instead: + +![filtering other words](https://user-images.githubusercontent.com/6243887/73389567-1d991a00-42dd-11ea-9bf1-b1fc6b85b635.png) + + +### Actions with custom payloads + +Next up: selecting stories by clicking them in the list. + +The following action sets the `reading` property in the state to a story-id, which amounts to "selecting" the story: + +```js +const SelectStory = (state, id) => ({...state, reading: id}) +``` + +It has a payload, but it's not an event object. It's a custom value telling us which +story was clicked. How are actions dispatched with custom payloads? – Like this: + +```js + +const StoryThumbnail = props => h( + "li", + { + onClick: [SelectStory, props.id], // <---- + class: { + unread: props.unread, + reading: props.reading, + } + }, + [ + h("p", {class: "title"}, emphasize(props.filter, props.title)), + h("p", {class: "author"}, props.author) + ] +) +``` + +Instead of just specifying the action, we give a length-2 array with the action first and the custom payload second. + +Selecting stories works now, but the feature is not quite done. When a story is selected, +we need to set its `seen` property to `true`, so we can highlight which stories the user has yet to read. Update the `SelectStory` action: + +```js +const SelectStory = (state, id) => ({ + ...state, // keep all state the same, except for the following: + reading: id, + stories: { + ...state.stories, //keep stories the same, except for: + [id]: { + ...state.stories[id], //keep this story the same, except for: + seen: true, + } + } +}) +``` + +Now, when you select a blue-edged story it turns yellow because it is selected, and when you select something else, +the edge turns gray to indicate you've read the story. + +![all stories read](https://user-images.githubusercontent.com/6243887/73389573-20940a80-42dd-11ea-9b26-ebdad474b169.png) + + +### Payload filters + +There's one little thing we should fix about `SetFilter`. See how it's dependent on the complex `event` object? +It would be easier to test and reuse if it were simply: + +```js +const SetFilter = (state, word) => ({...state, filter: word}) +``` + +But we don't know the word beforehand, so how can we set it as a custom payload? Change the `Filter` view +again (last time - I promise!): + +```js +const Filter = props => h("div", {class: "filter"}, [ + "Filter:", + + props.editingFilter + ? h("input", { + type: "text", + value: props.filter, + onInput: [SetFilter, event => event.target.value], // <---- + }) + : h("span", {class: "filter-word"}, props.filter), + + props.editingFilter + ? h("button", {onClick: StopEditingFilter}, "\u2713") + : h("button", {onClick: StartEditingFilter}, "\u270E"), +]) +``` + +When we give a _function_ as the custom payload, Hyperapp considers it a _payload filter_ and passes the default +payload through it, providing the returned value as payload to the action. + +> Payload filters are also useful when you need a payload that is a combination of custom data and event data + +If you'd like to see a working example of the code so far, have a look [here](https://codesandbox.io/s/hyperapp-tutorial-step-2-5yv34). + +Effects +---------------------------- + +Until now, the list of stories has been defined in the state and doesn't change. What we really want is +for stories matching the filter to be dynamically loaded. When we click the check-mark button +(indicating we are done editing the filter), we want to query an API and display the stories it responds with. + +### Actions can return effects + +Add this import (to the "IMPORTS" section): + +```js +import {Http} from "https://unpkg.com/hyperapp-fx@next?module" +``` + +Use the imported `Http` in the `StopEditingFilter` action like this: + +```js +const StopEditingFilter = state => [ + { + ...state, + editingFilter: false, + }, + Http({ // <--- + url: `https://zaceno.github.io/hatut/data/${state.filter.toLowerCase()}.json`, // <--- + response: "json", // <--- + action: GotStories, // <--- + }) +] +``` + +The call to `Http(...)` does _not_ immediately execute the API request. `Http` is an _effect creator_. It returns +an _effect_ bound to the options we provided. + +When Hyperapp sees an action return an array, it takes the first element of the array to be the new state, and the rest to +be _effects_. Effects are executed by Hyperapp as part of processing the action's return value. + +> Hyperapp provides effect creators for many common situations. If you've got an unusual case or are working +> with less common APIs you may need to implement your own effects. Don't worry - it's easy! See the +> [API reference](./ref.md) for more information. + +### Effects can dispatch actions + +One of the options we passed to `Http` was `action: GotStories`. The way this effect works is that when the response comes +back from the api, an action named `GotStories` (yet to be implemented) will be dispatched, with the response body as the payload. + +The response body is in json, but the payload will be a javascript object, thanks to the parsing hint `response: "json"`. It will look like this (although the details depend on your filter of course): + +```js +{ + "112": { + title: "The Ocean is Sinking", + author: "Kat Stropher", + }, + "113": { + title: "Ocean life is brutal", + author: "Surphy McBrah", + }, + "114": { + title: "Family friendly fun at the ocean exhibit", + author: "Guy Prosales", + } +} +``` + +The job of `GotStories` is to load this data into the state, in place of the stories we already have there. As it +does, it should take care to remember which story was selected, and which stories we have seen, if they were already +in the previous state. This will be our most complex action yet, and it could look like this: + +```js +const GotStories = (state, response) => { + const stories = {} + Object.keys(response).forEach(id => { + stories[id] = {...response[id], seen: false} + if (state.stories[id] && state.stories[id].seen) { + stories[id].seen = true + } + }) + const reading = stories[state.reading] ? state.reading :  null + return { + ...state, + stories, + reading, + } +} +``` + +Try it out! Enter "life" in the filter input. When you click the check-mark button some new +stories are loaded – all with blue edges except for "Ocean life is brutal" because it is +still selected. + +![loaded other stories](https://user-images.githubusercontent.com/6243887/73389577-24279180-42dd-11ea-8b4a-b231f1c811c8.png) + + + + +### Running effects on initialization + +The next obvious step is to load the _initial_ stories from the API as well. Change init to this: + + +```js + init: [ + { + editingFilter: false, + autoUpdate: false, + filter: "ocean", + reading: null, + stories: {}, // <--- + }, + Http({ // <--- + url: `https://zaceno.github.io/hatut/data/ocean.json`, // <--- + response: 'json', // <--- + action: GotStories, // <--- + }) + ], +``` + +Hyperapp treats the init-value the same way as it treats return values from actions. By adding the `Http` effect +in `init`, the app will fire the API request immediately, so we don't need the stories in the state from the start. + +![stories loaded from start](https://user-images.githubusercontent.com/6243887/73389586-2a1d7280-42dd-11ea-8642-f994c028a74f.png) + + +### Tracking state for asynchronous effects + +If we could display a spinner while we wait for stories to load, it would make for a smoother user experience. To +do that, we will need a new state property to tell us if we're waiting for a repsonse - and +consequently wether or not to render the spinner. + +Create this action: + +```js +const FetchStories = state => [ + {...state, fetching: true}, + Http({ + url: `https://zaceno.github.io/hatut/data/${state.filter.toLowerCase()}.json`, + response: 'json', + action: GotStories, + }) +] +``` + +Instead of dispatching this action, we will use it to simplify `StopEditingFilter`: + +```js +const StopEditingFilter = state => FetchStories({...state, editingFilter: false}) +``` + +... and `init` as well: + +```js + init: FetchStories({ + editingFilter: false, + autoUpdate: false, + filter: "ocean", + reading: null, + stories: {}, + }), +``` + +Now, when `StopEditingFilter` is dispatched, _and_ at initialization, the API call goes out and the +`fetching` prop is set to `true`. Also, notice how we refactored out the repetitive use of `Http`. + +We also need to set `fetching: false` in `GotStories`: + +```js +const GotStories = (state, response) => { + const stories = {} + Object.keys(response).forEach(id => { + stories[id] = {...response[id], seen: false} + if (state.stories[id] && state.stories[id].seen) { + stories[id].seen = true + } + }) + const reading = stories[state.reading] ? state.reading :  null + return { + ...state, + stories, + reading, + fetching: false, // <--- + } +} +``` + +With this, we know that when `fetching` is `true` we are waiting for a response, and should display +the spinner in the `StoryList` view: + +```js +const StoryList = props => h("div", {class: "stories"}, [ + + props.fetching && h("div", {class: "loadscreen"}, [ // <--- + h("div", {class: "spinner"}) // <--- + ]), // <--- + + h("ul", {}, Object.keys(props.stories).map(id => + StoryThumbnail({ + id, + title: props.stories[id].title, + author: props.stories[id].author, + unread: !props.stories[id].seen, + reading: props.reading === id, + filter: props.filter + }) + )) +]) +``` + +When the app loads, and when you change the filter, you should see the spinner appear until the stories are loaded. + +![loading spinner](https://user-images.githubusercontent.com/6243887/73389594-2db0f980-42dd-11ea-8bf8-95b96e7337b1.png) + +> If you aren't seeing the spinner, it might just be happening too fast. Try choking your network speed. In the Chrome +> browser you can set your network speed to "slow 3g" under the network tab in the developer tools. + +If you'd like to see a working example of the code so far, have a look [here](https://codesandbox.io/s/hyperapp-tutorial-step-3-2mmug). + +Subscriptions +------------------------------------------------------------------- + +The last feature we'll add is to make our app periodically check for new stories matching the filter. There won't actually +be any because it's not a real service, but you'll know it's happening when you see the spinner pop up every five +seconds. + +However, we want to make it opt-in. That's what the auto update checkbox at the bottom is for. We need a +property in the state to track wether the box is checked or not. + +Change the `AutoUpdate` view: + +```js +const AutoUpdate = props => h("div", {class: "autoupdate"}, [ + "Auto update: ", + h("input", { + type: "checkbox", + checked: props.autoUpdate, // <--- + onInput: ToggleAutoUpdate, // <--- + }) +]) +``` + +and implement the `ToggleAutoUpdate` action: + +```js +const ToggleAutoUpdate = state => ({...state, autoUpdate: !state.autoUpdate}) +``` + +Now we've got `autoUpdate` in the state tracking the checkbox. All we need now, is to set up `FetchStories` +to be dispatched every five seconds when `autoUpdate` is `true`. + +Import the `interval` _subscription creator_: + +```js +import {interval} from "https://unpkg.com/@hyperapp/time?module" +``` + +Add a `subscriptions` property to your app, with a conditional declaration of `interval` like this: + +```js + subscriptions: state => [ + state.autoUpdate && interval(FetchStories, {delay: 5000}) + ] +``` + +Hyperapp will call `subscriptions` every time the state changes. If it notices a +new subscription, it will be started, or if one has been removed it will be stopped. + +The options we passed to the `interval` subscription state that `FetchStories` should be dispatched every five seconds. It +will start when we check the auto update box, and stop when it is unchecked. + +![auto updating](https://user-images.githubusercontent.com/6243887/73389603-3275ad80-42dd-11ea-9270-bc8be471db8b.png) + +> As with effects, Hyperapp offers subscriptions for the most common cases, but you +> may need to implement your own. Refer to the [API reference](./ref.md). Again, +> it is no big deal - just not in scope for this tutorial. + +If you'd like to see a working example of the final code, have a look [here](https://codesandbox.io/s/hyperapp-tutorial-step-4-8u9q8). + +Conclusion +------------------ + +Congratulations on completing this Hyperapp tutorial! + +Along the way you've familiarized yourself with +the core concepts: _view_, _state_, _actions_, _effects_ & _subscriptions_. And that's really all you need to +build any web application. + +So now, go build your dream app, or browse our [Examples](./examples.md) for more +inspiration. diff --git a/write-what-not-how.68035d11.svg b/write-what-not-how.68035d11.svg new file mode 100644 index 0000000..4cf629e --- /dev/null +++ b/write-what-not-how.68035d11.svg @@ -0,0 +1,17 @@ + + + + WriteWhatNotHow + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file