diff --git a/app/api/cookbook.mjs b/app/api/cookbook.mjs new file mode 100644 index 00000000..fe3ec2e1 --- /dev/null +++ b/app/api/cookbook.mjs @@ -0,0 +1,6 @@ +export async function get () { + return { + statusCode: 301, + location: '/cookbook/', + } +} diff --git a/app/api/cookbook/$$.mjs b/app/api/cookbook/$$.mjs new file mode 100644 index 00000000..d91bf6c9 --- /dev/null +++ b/app/api/cookbook/$$.mjs @@ -0,0 +1,64 @@ +/* eslint-disable filenames/match-regex */ +import { readFileSync } from 'fs' +import { URL } from 'url' +import { Arcdown } from 'arcdown' +import arcStaticImg from 'markdown-it-arc-static-img' +import navDataLoader, { + other as otherLinks, +} from '../../docs/nav-data.mjs' +import HljsLineWrapper from '../../docs/hljs-line-wrapper.mjs' + +const arcdown = new Arcdown({ + pluginOverrides: { + markdownItToc: { + containerClass: 'toc mbe2 mis-2 leading2', + listType: 'ul', + level: [ 1, 2, 3 ], + }, + }, + plugins: [ arcStaticImg ], + hljs: { + sublanguages: { javascript: [ 'xml', 'css' ] }, + plugins: [ new HljsLineWrapper({ className: 'code-line' }) ], + }, +}) + +/** @type {import('@enhance/types').EnhanceApiFn} */ +export async function get (request) { + const { path: activePath } = request + let recipePath = activePath.replace(/^\/?docs\//, '') || 'index' + + let recipeURL = new URL(`../../${recipePath}.md`, import.meta.url) + + const navData = navDataLoader('docs', activePath) + + let recipeMarkdown + try { + recipeMarkdown = readFileSync(recipeURL.pathname, 'utf-8') + } + catch (e) { + return { + location: '/404' + } + } + + const recipe = await arcdown.render(recipeMarkdown) + + const initialState = { + recipe, + otherLinks, + navData, + } + + let cacheControl = + process.env.ARC_ENV === 'production' + ? 'max-age=3600;' + : 'no-cache, no-store, must-revalidate, max-age=0, s-maxage=0' + + return { + headers: { + 'cache-control': cacheControl, + }, + json: initialState, + } +} diff --git a/app/api/cookbook/index.mjs b/app/api/cookbook/index.mjs new file mode 100644 index 00000000..34cb3600 --- /dev/null +++ b/app/api/cookbook/index.mjs @@ -0,0 +1,21 @@ +import navDataLoader from '../../docs/nav-data.mjs' + +export async function get (req) { + const { path: activePath } = req + + const cacheControl = + process.env.ARC_ENV === 'production' + ? 'max-age=3600' + : 'no-cache, no-store, must-revalidate, max-age=0, s-maxage=0' + + const navData = navDataLoader('docs', activePath) + + return { + headers: { + 'cache-control': cacheControl, + }, + json: { + navData, + }, + } +} diff --git a/app/docs/md/patterns/building-for-the-browser.md b/app/cookbook/build-for-the-browser.md similarity index 99% rename from app/docs/md/patterns/building-for-the-browser.md rename to app/cookbook/build-for-the-browser.md index 4afb9c9c..8341e852 100644 --- a/app/docs/md/patterns/building-for-the-browser.md +++ b/app/cookbook/build-for-the-browser.md @@ -1,5 +1,5 @@ --- -title: Building for the browser +title: Build for the browser --- ## The `@bundles` plugin diff --git a/app/docs/md/patterns/architect-migration.md b/app/cookbook/migrate-from-architect.md similarity index 99% rename from app/docs/md/patterns/architect-migration.md rename to app/cookbook/migrate-from-architect.md index 22173e78..347e128b 100644 --- a/app/docs/md/patterns/architect-migration.md +++ b/app/cookbook/migrate-from-architect.md @@ -1,7 +1,5 @@ --- -title: Architect Migration -links: - - "arc.codes": https://arc.codes +title: Migrate from Architect --- Enhance uses [Architect](https://arc.codes) under the hood for local development and deployment. It is possible to migrate between a typical Architect project structure and the Enhance file-based routing. It is also possible to mix the two approaches together in the same app. They are incrementally adoptable in both directions. diff --git a/app/docs/md/patterns/rendering-markdown.md b/app/cookbook/render-markdown.md similarity index 98% rename from app/docs/md/patterns/rendering-markdown.md rename to app/cookbook/render-markdown.md index 03b3084d..e030e163 100644 --- a/app/docs/md/patterns/rendering-markdown.md +++ b/app/cookbook/render-markdown.md @@ -1,7 +1,5 @@ --- -title: Rendering Markdown -links: - - "Arcdown": https://github.com/architect/arcdown/blob/main/readme.md +title: Render Markdown --- Enhance can be used to render Markdown with minimal effort — in fact, this very site is itself an Enhance app that renders Markdown to HTML on demand. You can dig into the [source code](https://github.com/enhance-dev/enhance.dev) to see exactly how we've set it up, or follow along below. diff --git a/app/cookbook/translate-react.md b/app/cookbook/translate-react.md new file mode 100644 index 00000000..711732de --- /dev/null +++ b/app/cookbook/translate-react.md @@ -0,0 +1,84 @@ +--- +title: Translate React syntax to Enhance elements +--- + +We’re often asked by React developers why patterns they’ve learned while writing JSX don’t translate to writing web components. In this doc, we’ll describe some common gotchas that developers coming from React or other JavaScript frameworks may run into when writing plain vanilla web components. + +## String Interpolation + + + + + +```javascript +const element = `

${title}

`; +``` + +
+ + + + +```javascript +const element =

{title}

; +``` + +
+ +
+ +## Attribute Quoting + + + + + +```javascript +const image = ``; +``` + + + + + + +```javascript +const image = +``` + + + + + +## Rendering Markup from Arrays + + + + + +```javascript +const todoList = `` +``` + + + + + + +```javascript +const todoList = +``` + + + + + +For a more in depth look at the differences between Enhance and React components, [read this post on the Begin blog](https://begin.com/blog/posts/2024-03-08-a-react-developers-guide-to-writing-enhance-components). diff --git a/app/docs/md/patterns/event-listeners.md b/app/cookbook/use-event-listeners.md similarity index 99% rename from app/docs/md/patterns/event-listeners.md rename to app/cookbook/use-event-listeners.md index d93ce45b..6882c5f9 100644 --- a/app/docs/md/patterns/event-listeners.md +++ b/app/cookbook/use-event-listeners.md @@ -1,5 +1,5 @@ --- -title: Event Listeners +title: Use event listeners --- An event is a signal that something has happened on your page. The browser notifies you so you can react to them. diff --git a/app/cookbook/use-typescript.md b/app/cookbook/use-typescript.md new file mode 100644 index 00000000..a101c9c9 --- /dev/null +++ b/app/cookbook/use-typescript.md @@ -0,0 +1,104 @@ +--- +title: Use TypeScript +--- + +If you prefer to work with TypeScript, we recommend starting with our [TypeScript starter project](https://github.com/enhance-dev/enhance-starter-typescript). + + +## Getting Started + +Assuming you’re starting a new Enhance project, you would run the command: + +```bash +npx "@enhance/cli@latest" new ./myproject \ + --template https://github.com/enhance-dev/enhance-starter-typescript -y +``` + +This will set up a new Enhance project where you’ll code your APIs, elements and pages in TypeScript instead of JavaScript. Instead of editing files in the `app` folder, you’ll do your editing in the `ts` folder. + + +### Project Structure + +``` \ +ts +├── api ............... data routes +│ └── index.mts +├── browser ........... browser JavaScript +│ └── index.mts +├── components ........ single file web components +│ └── my-card.mts +├── elements .......... custom element pure functions +│ └── my-header.mts +├── pages ............. file-based routing +│ └── index.html +└── head.mts .......... custom component +``` + +Note: We are using `.mts` to tell the TypeScript Compiler to generate ES Modules as `.mjs` files.. + + +## Local Development + +Running the local development environment is the same as any other Enhance project. The new `@enhance/plugin-typescript` is responsible for watching the `ts` folder for any file changes. If a file with an `.mts` extension is updated, it will be re-compiled with the compilation target being the `app` folder. All other file types are simply copied to their corresponding locations in the `app` folder. + +## Authoring Code + +Write your code in TypeScript. We already have [types](https://github.com/enhance-dev/types) that you can import into your elements: + + + +```typescript +import type { EnhanceElemArg } from "@enhance/types" + +export default ({ html, state: { attrs } }: EnhanceElemArg) => { + const { state = "" } = attrs + return html` + ${state === "complete" ? "☑" : "☐"} + + ` +} +``` + + + +Or APIs: + + + +```typescript +import type { + EnhanceApiFn, + EnhanceApiReq, + EnhanceApiRes, +} from "@enhance/types"; + +type Todo = { + title: string; + completed?: boolean; +}; + +export const get: EnhanceApiFn = async function ( + request: EnhanceApiReq, +): Promise { + + console.log(`Handling ${request.path}...`); + + const todos: Todo[] = [ + { title: "todo 1", completed: false }, + { title: "todo 2", completed: true }, + { title: "todo 3" }, + ]; + + const response: EnhanceApiRes = { + json: { todos }, + }; + + return response; +}; +``` + + + +## Deploying + +Use the [`@begin/deploy`](https://begin.com/deploy/docs/workflows/deploying-code) package to deploy your application. Alternatively, you can write a GitHub Action to [deploy on every commit](https://github.com/enhance-dev/enhance-starter-typescript/blob/main/.github/workflows/CI.yml). diff --git a/app/docs/md/patterns/form-validation.md b/app/cookbook/validate-forms.md similarity index 99% rename from app/docs/md/patterns/form-validation.md rename to app/cookbook/validate-forms.md index f101a51e..835b85dd 100644 --- a/app/docs/md/patterns/form-validation.md +++ b/app/cookbook/validate-forms.md @@ -1,5 +1,5 @@ --- -title: Form Validation +title: Validate forms --- HTML forms are very powerful on their own. diff --git a/app/docs/md/patterns/testing/index.md b/app/cookbook/write-unit-tests.md similarity index 98% rename from app/docs/md/patterns/testing/index.md rename to app/cookbook/write-unit-tests.md index 05c7e11a..3d67bff6 100644 --- a/app/docs/md/patterns/testing/index.md +++ b/app/cookbook/write-unit-tests.md @@ -1,5 +1,5 @@ --- -title: Testing +title: Write unit tests --- A big benefit of Enhance custom element [pure functions](https://en.wikipedia.org/wiki/Pure_function) is that they return a string that you can test against an expected output. It doesn't need to get any more complicated than that to get started. diff --git a/app/docs/md/patterns/testing/webdriverio.md b/app/docs/md/patterns/testing/webdriverio.md deleted file mode 100644 index f4999f5e..00000000 --- a/app/docs/md/patterns/testing/webdriverio.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Component Testing With WebdriverIO ---- - -Running component tests allow you to mount, render and interact with a single Enhance component in isolation. -To ensure that test conditions are as close as possible to the real world, we recommend to run them in actual browser rather than using e.g. [JSDOM](https://github.com/jsdom/jsdom) which is just a JavaScript implementation of web standards provided by browsers. -To run Enhance component tests you can use [WebdriverIO](https://webdriver.io/) as a browser automation framework. -It is based on the [WebDriver](https://www.w3.org/TR/webdriver/) protocol, a browser automation web standard which guarantees that your tests interact with your components as closely as possible to how a user would. - -To add a WebdriverIO test harness to your project, run: - -```shell -npm init wdio@latest ./ -``` - -The command initializes a configuration wizard that helps you set-up the test harness. -Make sure to select on the first question: "_Where should your tests be launched?_" the answer: "_browser - for unit and component testing in the browser_". - -An example test file should be created. Replace the content with e.g.: - -```javascript -import { expect, browser, $$ } from '@wdio/globals' -import enhance from '@enhance/ssr' - -// see actual component here: https://github.com/webdriverio/component-testing-examples/blob/main/enhance/app/elements/my-header.mjs -import MyHeader from '../../app/elements/my-header.mjs' - -describe('Enhance Framework', () => { - it('should render MyHeader element correctly', async () => { - const html = enhance({ - elements: { - 'my-header': MyHeader - } - }) - const actual = document.createElement('div') - actual.innerHTML = (html``).replace(/<\/?\s*(html|head|body)>/g, '') - document.body.appendChild(actual) - expect(await $$('img').length).toBe(2) - }) -}) -``` - -You can find the WebdriverIO API docs at [webdriver.io/docs/api](https://webdriver.io/docs/api) and the full example for Enhance component testing [on GitHub](https://github.com/webdriverio/component-testing-examples/tree/main/enhance). -If you have any questions, join the project's [Discord](https://discord.webdriver.io/) server. \ No newline at end of file diff --git a/app/docs/nav-data.mjs b/app/docs/nav-data.mjs index 841853ec..c9c44727 100644 --- a/app/docs/nav-data.mjs +++ b/app/docs/nav-data.mjs @@ -137,17 +137,6 @@ export const data = [ slug: 'patterns', items: [ 'progressive-enhancement', - 'building-for-the-browser', - 'form-validation', - { - slug: 'testing', - path: '/docs/patterns/testing/', - hasChildren: true, - items: [ { slug: 'webdriverio', label: 'WebdriverIO' } ], - }, - 'architect-migration', - 'rendering-markdown', - 'event-listeners', ], }, /* diff --git a/app/elements/code-compare.mjs b/app/elements/code-compare.mjs new file mode 100644 index 00000000..3fd6f1f3 --- /dev/null +++ b/app/elements/code-compare.mjs @@ -0,0 +1,20 @@ +export default function CodeCompare ({ html }) { + return html` + + + ` +} diff --git a/app/elements/cookbook/article.mjs b/app/elements/cookbook/article.mjs new file mode 100644 index 00000000..f3acab0d --- /dev/null +++ b/app/elements/cookbook/article.mjs @@ -0,0 +1,221 @@ +export default function CookbookArticle ({ html }) { + return html` + +
+ + + More recipes + + +
+ +
+ + + ` +} diff --git a/app/elements/cookbook/header.mjs b/app/elements/cookbook/header.mjs new file mode 100644 index 00000000..5deb5b2c --- /dev/null +++ b/app/elements/cookbook/header.mjs @@ -0,0 +1,58 @@ +export default function CookbookHeader ({ html }) { + return html` + +
+
+
+ Axol wearing a chef hat +
+
+ +

+ Let’s get cooking! +

+ +

+ Learning new things can be fun — but also challenging. The Enhance Cookbook is here to show you around the kitchen and help you get your hands dirty. +

+
+ ` +} diff --git a/app/elements/cookbook/recipe-box.mjs b/app/elements/cookbook/recipe-box.mjs new file mode 100644 index 00000000..0fa58629 --- /dev/null +++ b/app/elements/cookbook/recipe-box.mjs @@ -0,0 +1,13 @@ +export default function RecipeBox ({ html }) { + return html` + + + ` +} diff --git a/app/elements/cookbook/recipe-card.mjs b/app/elements/cookbook/recipe-card.mjs new file mode 100644 index 00000000..8a6d8d88 --- /dev/null +++ b/app/elements/cookbook/recipe-card.mjs @@ -0,0 +1,42 @@ +export default function Recipe ({ html, state }) { + const { attrs } = state + const { href, name } = attrs + + return html` + + +
+

+ ${name} +

+ + +
+
+ ` +} diff --git a/app/elements/cookbook/recipes.mjs b/app/elements/cookbook/recipes.mjs new file mode 100644 index 00000000..2db18f28 --- /dev/null +++ b/app/elements/cookbook/recipes.mjs @@ -0,0 +1,54 @@ +export default function CookbookRecipes ({ html }) { + return html` + + +

+ Use Arcdown to render Markdown content into your Enhance app. +

+
+ + +

+ Use DOM events to respond to dynamic user input. +

+
+ + +

+ Improve UX and prevent errors by validating forms on the client and the server. +

+
+ + +

+ Ship and run code on the browser within a server side rendered Enhance app. +

+
+ + +

+ Test Enhance elements and API routes. +

+
+ + +

+ Learn how to migrate your Architect app to an Enhance app. +

+
+ + +

+ Watch out for common gotchas when coming from React and JSX. +

+
+ + +

+ Work with Typescript in your Enhance project +

+
+ +
+ ` +} diff --git a/app/elements/docs/footer.mjs b/app/elements/docs/footer.mjs index 3414eec6..7c884699 100644 --- a/app/elements/docs/footer.mjs +++ b/app/elements/docs/footer.mjs @@ -3,8 +3,7 @@ function readNext (nextLink) { return /* html */ ` ${nextLink.label} → - ${ - nextLink.description + ${nextLink.description ? `

${nextLink.description}

` : '' } @@ -25,20 +24,18 @@ function communityResources (communityLinks) { const description = link?.description || '' return /* html */ ` -
- ${label} -
-
+

${label}

+

${description} -

+

` }) .join('') return /* html */ ` -