package ex01
import (
- "git.earlybird.gay/today-engine/cmd/run-today-examples/parts"
"git.earlybird.gay/today-engine/page"
)
-var Page = page.New("ex01", "page.html", page.Includes(parts.ExampleNav))
+// Make a page like this, with page.New(name, file). Name doesn't matter much
+// for pages. File is a path to a file relative to *this Go file*, NOT the
+// working directory.
+// A Page is an http.Handler!
+var Page = page.New("ex01", "page.html")
<!DOCTYPE html>
-<html>
- <header>
- <title>Example 1</title>
- </header>
- <body>
- <h1>Static HTML</h1>
- <p>This is a static page. Hooray.</p>
-
- <!-- Ignore me!! I'll be explained in a second! -->
- <example-nav>
- <a href="ex02" slot="next">Parts</a>
- </example-nav>
- </body>
+<html lang="en-US">
+<head>
+ <title>Example 1</title>
+ <link rel="stylesheet" href="/style.css">
+</head>
+
+<body>
+ <main>
+ <h1>Static HTML</h1>
+ <p>Today supports regular HTML, without any modification. All of the
+ features you'll see in these examples add on to the "normal experience".
+ For each example, you'll see the rendered result in the browser, plus
+ source files to show how that example is constructed.
+ </p>
+
+ <nav>
+ <a href="/ex02">Parts ></a>
+ <a href="/">Back to Overview</a>
+ </nav>
+
+ <h2>Sources for this example</h2>
+ <!-- source-preview does not belong to the HTML specification, but elements
+ with - in the name are allowed! We're going to do something with this in
+ a second. -->
+ <source-preview>
+ <h3>page.html</h3>
+ <pre><code>ᐸ!DOCTYPE htmlᐳ
+ᐸhtmlᐳ
+ᐸheadᐳ
+ ᐸtitleᐳExample 1ᐸ/titleᐳ
+ ᐸlink rel="stylesheet" href="/style.css"ᐳ
+ᐸ/headᐳ
+
+ᐸbodyᐳ
+ ᐸmainᐳ
+ ᐸh1ᐳStatic HTMLᐸ/h1ᐳ
+ ᐸpᐳToday supports regular HTML, without any modification. All of the
+ features you'll see in these examples add on to the "normal experience".
+ For each example, you'll see the rendered result in the browser, plus
+ source files to show how that example is constructed.
+ ᐸ/pᐳ
+
+ ᐸh2ᐳSources for this exampleᐸ/h2ᐳ
+ ᐸ!-- source-preview does not belong to the HTML specification, but elements
+ with - in the name are allowed! We're going to do something with this in
+ a second. --ᐳ
+ ᐸsource-previewᐳ
+ ᐸh3ᐳpage.htmlᐸ/h3ᐳ
+ ᐸpreᐳᐸcodeᐳ...ᐸ/codeᐳᐸ/preᐳ
+ ᐸ/source-previewᐳ
+ ᐸsource-previewᐳ
+ ᐸh3ᐳexample.goᐸ/h3ᐳ
+ ᐸpreᐳᐸcodeᐳ...ᐸ/codeᐳᐸ/preᐳ
+ ᐸ/source-previewᐳ
+ ᐸnavᐳ
+ ᐸa href="/ex02"ᐳPartsᐸ/aᐳ
+ ᐸa href="/"ᐳBack to Overviewᐸ/aᐳ
+ ᐸ/navᐳ
+ ᐸ/mainᐳ
+ᐸ/bodyᐳ
+
+ᐸ/htmlᐳ</code></pre>
+ </source-preview>
+ <source-preview>
+ <h3>example.go</h3>
+ <pre><code>// Copyright (C) 2024 early (LGPL)
+package ex01
+
+import (
+ "git.earlybird.gay/today-engine/page"
+)
+
+// Make a page like this, with page.New(name, file). Name doesn't matter much
+// for pages. File is a path to a file relative to *this Go file*, NOT the
+// working directory.
+// A Page is an http.Handler!
+var Page = page.New("ex01", "page.html")
+ </code></pre>
+ </source-preview>
+ </main>
+</body>
+
</html>
\ No newline at end of file
package ex02
import (
+ "math/rand/v2"
+
"git.earlybird.gay/today-engine/cmd/run-today-examples/parts"
+ "git.earlybird.gay/today-engine/include"
"git.earlybird.gay/today-engine/page"
"git.earlybird.gay/today-engine/part"
+ "git.earlybird.gay/today-engine/render"
)
-var section = part.New("my-section", "section.html")
+// Like pages, you make parts with part.New. The name is more important for
+// parts, since the name is the name of the element in HTML.
+var RandomGenerator = part.New("random-generator", "random-generator.html",
+ part.OnLoad(func(data render.Data) error {
+ // Generate a random number on load.
+ data.Set("number", rand.Int()%100)
+ return nil
+ }),
+)
-// Use *.Includes to include a part as a dependency.
-var Page = page.New("ex02", "page.html", page.Includes(section, parts.ExampleNav))
+var Page = page.New("ex01", "page.html",
+ // When you want to use a part, you need to say so in page.Includes.
+ page.Includes(RandomGenerator, parts.SourcePreview),
+ page.OnLoad(func(data render.Data) error {
+ // This is all for SourcePreview, don't worry about it just yet.
+ sources := make(render.Data)
+ sources.Set("page", include.File("page.html"))
+ sources.Set("go", include.File("example.go"))
+ data.Set("sources", sources)
+ return nil
+ }),
+)
<!DOCTYPE html>
<html>
- <header>
- <title>Example 2</title>
- </header>
- <body>
- <h1>Page with Parts</h1>
- <p>This page uses "parts" to create sections on the page. Parts are
- HTML blocks that you can re-use.
- </p>
- </body>
- <my-section>
- <h2>Section</h2>
- <p>This is inner HTML for a my-section "part". It replaces the slot tag
- in the part.
- </p>
- </my-section>
- <example-nav>
- <a href="ex01" slot="previous">Pages</a>
- <a href="ex03" slot="next">Slots</a>
- </example-nav>
+<head>
+ <title>Example 1</title>
+ <link rel="stylesheet" href="/style.css">
+</head>
+
+<body>
+ <main>
+ <h1>Parts</h1>
+ <p>
+ In the previous example, you saw a <code>source-preview</code> element,
+ which is not part of the HTML specification. However, custom elements with
+ a - in the name are allowed by the specification, because they are used
+ for the Web Components feature of the web platform. Today Engine borrows
+ this for use with server-side "parts". Examples will focus on these for
+ now, but we'll get into Web Components and how they're different later.
+ </p>
+ <p>
+ Parts allow you to define custom elements that are rendered server-side,
+ before any content is sent to the user agent. Each part can execute some
+ behavior in Go, and decide how it is rendered based on the result. Here
+ is a part that generates a random number every time the page is reloaded.
+ </p>
+
+ <random-generator></random-generator>
+
+ <p>This isn't super useful.</p>
+ <ul>
+ <li>Generating a random number doesn't do much.</li>
+ <li>We don't have any control over how the part chooses a number.</li>
+ <li>
+ It has an h3, but what if we need an h4 because of where it is in the
+ document, or we want to change what the header says?
+ </li>
+ </ul>
+ <p>
+ Fortunately, these are solvable problems. In the next few examples, we'll
+ evaluate how to make replacable markup in parts using slots and pass data
+ using attributes.
+ </p>
+
+ <nav>
+ <a href="/ex01">< Pages</a>
+ <!--a href="/ex02">Parts ></a-->
+ <a href="/">Back to Overview</a>
+ </nav>
+
+ <!--
+ Source previews will now use this source-preview part instead of
+ manually pasting in safe "html" or other code.
+ -->
+ <h2>Sources for this example</h2>
+ <source-preview :source=".sources.page">
+ <h3 slot="title">page.html</h3>
+ </source-preview>
+ <source-preview :source=".sources.go">
+ <h3 slot="title">example.go</h3>
+ </source-preview>
+ </main>
+</body>
+
</html>
\ No newline at end of file
--- /dev/null
+<template>
+ <h3>Oh wow, a random number.</h3>
+ <!-- This value is set in random-generator.OnLoad. -->
+ <p>{{ .number }}</p>
+</template>
\ No newline at end of file
+++ /dev/null
-<template>
- <section>
- <!-- Slots are where the inner HTML of the part goes when rendered. -->
- <slot></slot>
- </section>
-</template>
\ No newline at end of file
+++ /dev/null
-// Copyright (C) 2024 early (LGPL)
-package ex03
-
-import (
- "git.earlybird.gay/today-engine/cmd/run-today-examples/parts"
- "git.earlybird.gay/today-engine/page"
- "git.earlybird.gay/today-engine/part"
-)
-
-var section = part.New("my-section", "section.html")
-
-// Use *.Includes to include a part as a dependency.
-var Page = page.New("ex03", "page.html", page.Includes(section, parts.ExampleNav))
+++ /dev/null
-<!DOCTYPE html>
-<html>
- <header>
- <title>Example 3</title>
- </header>
- <body>
- <h1>Page with Parts</h1>
- <p>This page uses "parts" to create sections on the page. Parts are
- HTML blocks that you can re-use.
- </p>
- </body>
- <!-- Use named slots to choose where HTML goes in the part. -->
- <my-section>
- <h2 slot="header">Section with Named Slots</h2>
- <p slot="content">Use the slot tag to fill in named slots.</p>
- <p slot="content">You can do this with multiple HTML elements!</p>
- </my-section>
- <!-- Slots left empty have default HTML; see section.html for details! -->
- <my-section/>
- <my-section>
- <h2 slot="header">Section with a Subsection</h2>
- <p slot="content">You can also include parts inside parts.</p>
- <my-section slot="content">
- <h3 slot="header">Subsection</h3>
- <p slot="content">This my-section is a subsection!</p>
- </my-section>
- </my-section>
-
- <example-nav>
- <a href="ex02" slot="previous">Parts</a>
- <a href="ex04" slot="next">Go Templates</a>
- </example-nav>
-</html>
\ No newline at end of file
+++ /dev/null
-<template>
- <section>
- <!-- Slots are where the inner HTML of the part goes when rendered. -->
- <slot name="header">
- <h2>Section</h2>
- </slot>
- <slot name="content">
- <p>When no HTML is provided for a slot, it uses the HTML provided
- in the template.
- </p>
- </slot>
- </section>
-</template>
\ No newline at end of file
+++ /dev/null
-// Copyright (C) 2024 early (LGPL)
-package ex04
-
-import (
- "text/template"
-
- "git.earlybird.gay/today-engine/cmd/run-today-examples/parts"
- "git.earlybird.gay/today-engine/page"
- "git.earlybird.gay/today-engine/render"
-)
-
-var Page = page.New("ex04", "page.html",
- page.OnLoad(func(data render.Data) error {
- data.Set("message", "Hello, Today!")
- return nil
- }),
- page.Funcs(template.FuncMap{
- "reverse": func(s string) string {
- out := make([]byte, len(s))
- for i := range (len(s) + 1) / 2 {
- j := len(s) - i - 1
- out[i] = s[j]
- out[j] = s[i]
- }
- return string(out)
- },
- }),
- page.Includes(parts.ExampleNav),
-)
+++ /dev/null
-<!DOCTYPE html>
-<html>
- <header>
- <title>Example 4</title>
- </header>
- <body>
- <h1>Template Data</h1>
- <p>Let's step back to talk about template data. Go provides templates
- using "handlebars" notation to create server-side behavior on your
- pages. For example, on this page, {{ `{{ .message }}` }} is
- "{{ .message }}".
- </p>
- <p>You can also add functions, if the ones provided do not do everything
- you need. On this page, {{ `{{ reverse "hello" }}`}} is
- "{{ reverse "hello" }}".
- </p>
- <pre><code>var index = page.New("index", "index.html",
- page.OnLoad(func(data render.Data) error {
- data.Set("message", "Hello, Today!")
- return nil
- }),
- page.Funcs(template.FuncMap{
- "reverse": func(s string) string {
- out := make([]byte, len(s))
- for i := range (len(s) + 1) / 2 {
- j := len(s) - i - 1
- out[i] = s[j]
- out[j] = s[i]
- }
- return string(out)
- },
- }),
-)</code></pre>
-
- <example-nav>
- <a href="ex03" slot="previous">Slots</a>
- </example-nav>
- </body>
-</html>
\ No newline at end of file
+++ /dev/null
-// Copyright (C) 2024 early (LGPL)
-package ex06
-
-import (
- "errors"
- "strings"
-
- "git.earlybird.gay/today-engine/cmd/run-today-examples/parts"
- "git.earlybird.gay/today-engine/page"
- "git.earlybird.gay/today-engine/part"
- "git.earlybird.gay/today-engine/render"
-)
-
-type exampleStruct struct {
- Data string
-}
-
-var Page = page.New("ex01", "page.html",
- page.OnLoad(func(data render.Data) error {
- // This sets .some.really.nested.Data to "hello!".
- // Bit of a silly example, but it shows the point.
- data.Set("some", map[string]any{
- "really": map[string]any{
- "nested": exampleStruct{
- Data: "hello, nested data!",
- },
- },
- })
- return nil
- }),
- page.Includes(parts.ExampleNav, messagePrinter),
-)
-var messagePrinter = part.New("message-printer", "message-printer.html",
- part.OnLoad(func(data render.Data) error {
- message, ok := data.Get("message").(string)
- if !ok {
- return errors.New("no message set")
- }
- data.Set("transformedMessage", strings.ToUpper(message))
- return nil
- }),
-)
+++ /dev/null
-<template>
- <p>{{ .message }}</p>
- <p>{{ .transformedMessage }}</p>
-</template>
\ No newline at end of file
+++ /dev/null
-<!DOCTYPE html>
-<html>
- <header>
- <title>Example 6</title>
- </header>
- <body>
- <h1>Passing Data to Parts</h1>
- <p>Sometimes, you may want to provide more data to a part than just the
- HTML to include. To do this, write attributes on the part that start
- with a colon ":".
- </p>
- <p>You can even do this with template fields by starting with a ".".
- Unfortunately, full template pipelines aren't supported this way
- right now. You can only pass a string or a value contained in the
- parent data.
- </p>
-
- <message-printer :message="hello"></message-printer>
- <message-printer :message=".some.really.nested.Data"></message-printer>
-
- <example-nav>
- <a href="ex02" slot="next">Parts</a>
- </example-nav>
- </body>
-</html>
\ No newline at end of file
+++ /dev/null
-<template id="example-counter-template">
- <slot name="text">Count:</slot>
-</template>
-<script>
-class ExampleCounter extends HTMLElement {
- constructor() {
- super();
- const shadowRoot = this.attachShadow({ mode: "open" });
- const template = document.getElementById("example-counter-template");
-
- shadowRoot.appendChild(template.content.cloneNode(true));
- }
-}
-customElements.define('example-counter', ExampleCounter);
-</script>
+++ /dev/null
-// Copyright (C) 2024 early (LGPL)
-package ex07
-
-import (
- "git.earlybird.gay/today-engine/cmd/run-today-examples/parts"
- "git.earlybird.gay/today-engine/component"
- "git.earlybird.gay/today-engine/page"
-)
-
-var Page = page.New("ex01", "page.html", page.Includes(parts.ExampleNav, counter))
-var counter = component.New("example-counter", "counter.html")
+++ /dev/null
-<!DOCTYPE html>
-<html>
- <header>
- <title>Example 1</title>
- </header>
- <body>
- <h1>Web Components</h1>
-
- <example-counter>
- <span slot="text">Example:</span>
- </example-counter>
- <!-- Ignore me!! I'll be explained in a second! -->
- <example-nav>
- <a href="ex02" slot="next">Parts</a>
- </example-nav>
- </body>
-</html>
\ No newline at end of file
"git.earlybird.gay/today-engine/cmd/run-today-examples/ex01-pages"
"git.earlybird.gay/today-engine/cmd/run-today-examples/ex02-parts"
- "git.earlybird.gay/today-engine/cmd/run-today-examples/ex03-slots"
- "git.earlybird.gay/today-engine/cmd/run-today-examples/ex04-templates"
- "git.earlybird.gay/today-engine/cmd/run-today-examples/ex06-data"
- "git.earlybird.gay/today-engine/cmd/run-today-examples/ex07-components"
"git.earlybird.gay/today-engine/page"
)
mux.Handle("GET /{$}", index)
mux.Handle("GET /ex01", ex01.Page)
mux.Handle("GET /ex02", ex02.Page)
- mux.Handle("GET /ex03", ex03.Page)
- mux.Handle("GET /ex04", ex04.Page)
- mux.Handle("GET /ex06", ex06.Page)
- mux.Handle("GET /ex07", ex07.Page)
+ mux.Handle("GET /", http.FileServer(http.Dir("cmd/run-today-examples/public")))
http.ListenAndServe("0.0.0.0:3000", mux)
}
+++ /dev/null
-<template>
- <nav>
- <span>Previous: <slot name="previous">None</slot></span>
- <span>Next: <slot name="next">None</slot></span>
- <span style="grid-column: span 2;"><a href="/">Back to Overview</a></span>
- </nav>
-</template>
\ No newline at end of file
// Copyright (C) 2024 early (LGPL)
package parts
-import "git.earlybird.gay/today-engine/part"
+import (
+ "errors"
+ "io"
+ "strings"
-var ExampleNav = part.New("example-nav", "example-nav.html")
+ "git.earlybird.gay/today-engine/include"
+ "git.earlybird.gay/today-engine/part"
+ "git.earlybird.gay/today-engine/render"
+)
+
+var SourcePreview = part.New("source-preview", "source-preview.html",
+ part.OnLoad(func(data render.Data) error {
+ opener, ok := data.Get("source").(include.Opener)
+ if !ok || opener == nil {
+ return errors.New("source-preview requires include.Opener source")
+ }
+ file, err := opener.Open()
+ if err != nil {
+ return err
+ }
+ buf := new(strings.Builder)
+ io.Copy(buf, file)
+ data.Set("source_raw", buf.String())
+ return nil
+ }),
+)
--- /dev/null
+<template>
+ <slot name="title"><h3>Page preview:</h3></slot>
+ <pre><code>{{ .source_raw }}</code></pre>
+</template>
\ No newline at end of file
-nav {
+/* The Great Reset */
+* {
+ padding: 0;
+ margin: 0;
+}
+
+/* Containers */
+
+main, section, nav {
display: flex;
flex-direction: column;
- gap: 1rem;
-}
\ No newline at end of file
+ gap: .5rem;
+}
+
+main {
+ margin: 2rem 20%;
+}
+
+/* Typography */
+
+/* Styling for parts and app-specific stuff */
+
+source-preview {
+ width: fit-content;
+ border: 1px solid black;
+ border-radius: 5px;
+ overflow: hidden;
+}
+
+source-preview > * {
+ padding: 1rem;
+}
+
+source-preview pre {
+ background-color: lightgray;
+}
import (
"errors"
- "fmt"
"git.earlybird.gay/today-engine/htmltree"
"golang.org/x/net/html"
var template, script, body *html.Node
for _, node := range innerHTML {
- fmt.Printf("%+v\n", node)
if node.Type == html.ElementNode && node.DataAtom == atom.Template {
template = node
}