├── .gitignore ├── .liquidrc ├── .npmignore ├── .prettierignore ├── .vscode ├── defaults │ └── pages │ │ ├── page-a.md │ │ ├── page-b.md │ │ ├── page-c.md │ │ └── page-d.md ├── extensions.json ├── hooks │ ├── include │ │ └── counter.liquid │ ├── page-a.md │ └── page-b.md └── settings.json ├── LICENSE ├── docs ├── .eleventy.cjs ├── .eleventyignore ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── readme.md ├── src │ ├── app │ │ ├── bundle.ts │ │ ├── components │ │ │ ├── drawer.ts │ │ │ ├── dropdown.ts │ │ │ ├── indicator.ts │ │ │ ├── scrollspy.ts │ │ │ ├── search.ts │ │ │ └── sidebar.ts │ │ ├── examples │ │ │ ├── alias.ts │ │ │ ├── counter.ts │ │ │ ├── hooks.ts │ │ │ └── iframe.ts │ │ ├── iframe.ts │ │ └── tutorial │ │ │ ├── counter.ts │ │ │ └── tabs.ts │ ├── assets │ │ ├── flow.svg │ │ ├── fonts │ │ │ ├── proxima-nova-bold.woff2 │ │ │ ├── proxima-nova-light.woff2 │ │ │ └── proxima-nova-regular.woff2 │ │ ├── img │ │ │ ├── 1.webp │ │ │ ├── 10.webp │ │ │ ├── 11.webp │ │ │ ├── 12.webp │ │ │ ├── 13.webp │ │ │ ├── 14.webp │ │ │ ├── 15.webp │ │ │ ├── 16.webp │ │ │ ├── 17.webp │ │ │ ├── 18.webp │ │ │ ├── 19.webp │ │ │ ├── 2.webp │ │ │ ├── 20.webp │ │ │ ├── 21.webp │ │ │ ├── 22.webp │ │ │ ├── 23.webp │ │ │ ├── 24.webp │ │ │ ├── 25.webp │ │ │ ├── 26.webp │ │ │ ├── 27.webp │ │ │ ├── 28.webp │ │ │ ├── 29.webp │ │ │ ├── 3.webp │ │ │ ├── 30.webp │ │ │ ├── 4.webp │ │ │ ├── 5.webp │ │ │ ├── 6.webp │ │ │ ├── 7.webp │ │ │ ├── 8.webp │ │ │ ├── 9.webp │ │ │ └── social-banner.png │ │ └── svg │ │ │ ├── anchor.svg │ │ │ ├── arrow-right.svg │ │ │ ├── check.svg │ │ │ ├── chevron-down.svg │ │ │ ├── chevron-left.svg │ │ │ ├── chevron-right.svg │ │ │ ├── clown.svg │ │ │ ├── copy.svg │ │ │ ├── discord.svg │ │ │ ├── github-avatar.svg │ │ │ ├── github.svg │ │ │ ├── home.svg │ │ │ ├── logo copy.svg │ │ │ ├── logo.svg │ │ │ ├── menu.svg │ │ │ ├── minus-square.svg │ │ │ ├── npm.svg │ │ │ ├── plus-square.svg │ │ │ ├── refresh.svg │ │ │ ├── search-code.svg │ │ │ ├── search-goto.svg │ │ │ ├── search-heading.svg │ │ │ ├── search-list.svg │ │ │ ├── search-quote.svg │ │ │ └── search-text.svg │ ├── sass │ │ ├── examples.scss │ │ ├── extend │ │ │ ├── anchors.scss │ │ │ ├── base.scss │ │ │ ├── blockquote.scss │ │ │ ├── iframes.scss │ │ │ ├── landing.scss │ │ │ ├── layout.scss │ │ │ ├── navbar.scss │ │ │ ├── patches.scss │ │ │ ├── search.scss │ │ │ ├── sidebar.scss │ │ │ ├── svg.scss │ │ │ └── tabs.scss │ │ ├── stylesheet.scss │ │ └── variables.scss │ └── views │ │ ├── base.liquid │ │ ├── data │ │ ├── comparison.json │ │ ├── events.json │ │ ├── iframes.json │ │ ├── lifecycles.json │ │ ├── meta.json │ │ └── navigation.json │ │ ├── docs │ │ ├── api │ │ │ ├── capture.md │ │ │ ├── clear.md │ │ │ ├── disconnect.md │ │ │ ├── dom.md │ │ │ ├── fetch.md │ │ │ ├── http.md │ │ │ ├── hydrate.md │ │ │ ├── live.md │ │ │ ├── methods.md │ │ │ ├── observe.md │ │ │ ├── off.md │ │ │ ├── on.md │ │ │ ├── prefetch.md │ │ │ ├── reload.md │ │ │ ├── render.md │ │ │ ├── session.md │ │ │ ├── store.md │ │ │ └── visit.md │ │ ├── components │ │ │ ├── algorithm.md │ │ │ ├── aliases.md │ │ │ ├── bindings.md │ │ │ ├── directives.md │ │ │ ├── events.md │ │ │ ├── extends.md │ │ │ ├── functions.md │ │ │ ├── hooks.md │ │ │ ├── nodes.md │ │ │ ├── register.md │ │ │ ├── state.md │ │ │ └── sugar.md │ │ ├── directives │ │ │ ├── attributes.md │ │ │ ├── spx-append.md │ │ │ ├── spx-bind.md │ │ │ ├── spx-cache.md │ │ │ ├── spx-component.md │ │ │ ├── spx-data.md │ │ │ ├── spx-disable.md │ │ │ ├── spx-eval.md │ │ │ ├── spx-fragment.md │ │ │ ├── spx-history.md │ │ │ ├── spx-hover.md │ │ │ ├── spx-hydrate.md │ │ │ ├── spx-intersect.md │ │ │ ├── spx-morph.md │ │ │ ├── spx-mount.md │ │ │ ├── spx-node.md │ │ │ ├── spx-position.md │ │ │ ├── spx-prepend.md │ │ │ ├── spx-progress.md │ │ │ ├── spx-promixity.md │ │ │ ├── spx-render.md │ │ │ ├── spx-replace.md │ │ │ ├── spx-scroll.md │ │ │ ├── spx-ssr.md │ │ │ ├── spx-target.md │ │ │ ├── spx-threshold.md │ │ │ ├── spx-track.md │ │ │ └── spx-watch.md │ │ ├── getting-started │ │ │ ├── getting-help.md │ │ │ ├── key-concepts.md │ │ │ ├── lifecycle.md │ │ │ ├── tutorial.md │ │ │ ├── using-spx.md │ │ │ └── what-is-spx.md │ │ ├── index.md │ │ ├── miscellaneous │ │ │ ├── acknowledgements.md │ │ │ ├── benchmarks.md │ │ │ ├── lexicon.md │ │ │ ├── methods.md │ │ │ ├── reference.md │ │ │ └── sessions.md │ │ └── usage │ │ │ ├── basic-example.md │ │ │ ├── components.md │ │ │ ├── connection.md │ │ │ ├── examples │ │ │ ├── hooks │ │ │ │ ├── component.liquid │ │ │ │ ├── component.ts │ │ │ │ ├── onmount.md │ │ │ │ └── unmount.md │ │ │ └── resource-evaluation │ │ │ │ ├── include │ │ │ │ └── scripts.liquid │ │ │ │ └── pages │ │ │ │ ├── page-a.md │ │ │ │ ├── page-b.md │ │ │ │ └── page-c.md │ │ │ ├── installation.md │ │ │ ├── lifecycle-events.md │ │ │ ├── methods-list.md │ │ │ ├── options.md │ │ │ ├── resource-evaluation.md │ │ │ ├── typescript.md │ │ │ └── vscode-extension.md │ │ ├── iframes │ │ └── code.liquid │ │ └── include │ │ ├── breadcrumb.liquid │ │ ├── comparison.liquid │ │ ├── event-attrs.liquid │ │ ├── event-options.liquid │ │ ├── flow.liquid │ │ ├── head.liquid │ │ ├── hooks-table.liquid │ │ ├── iframe.liquid │ │ ├── landing.liquid │ │ ├── lifecycle-events.liquid │ │ ├── navbar.liquid │ │ ├── paginate.liquid │ │ ├── search.liquid │ │ ├── sidebar-header.liquid │ │ └── sidebar.liquid └── tsup.config.ts ├── index.d.ts ├── index.js ├── package.json ├── pnpm-lock.yaml ├── readme.md ├── src ├── app │ ├── config.ts │ ├── controller.ts │ ├── events.ts │ ├── fetch.ts │ ├── location.ts │ ├── progress.ts │ ├── queries.ts │ ├── render.ts │ ├── session.ts │ └── snapshot.ts ├── components │ ├── class.ts │ ├── context.ts │ ├── instance.ts │ ├── listeners.ts │ ├── observe.ts │ ├── proxies.ts │ └── register.ts ├── index.ts ├── morph │ ├── attributes.ts │ ├── forms.ts │ ├── morph.ts │ ├── snapshot.ts │ └── walk.ts ├── observe │ ├── components.ts │ ├── fragment.ts │ ├── history.ts │ ├── hovers.ts │ ├── hrefs.ts │ ├── intersect.ts │ ├── mutations.ts │ └── proximity.ts └── shared │ ├── const.ts │ ├── enums.ts │ ├── links.ts │ ├── logs.ts │ ├── native.ts │ ├── patch.ts │ ├── regexp.ts │ └── utils.ts ├── test ├── .eleventy.cjs ├── .eleventyignore ├── asset │ ├── fonts │ │ ├── proxima-nova-bold.woff2 │ │ ├── proxima-nova-light.woff2 │ │ └── proxima-nova-regular.woff2 │ ├── style.scss │ ├── style │ │ ├── explorer.scss │ │ ├── extend.scss │ │ ├── json-tree.scss │ │ └── resize.scss │ ├── suite.ts │ ├── suite │ │ ├── cases.ts │ │ ├── console.ts │ │ ├── explorer.ts │ │ ├── explorer │ │ │ ├── components.ts │ │ │ └── state.ts │ │ ├── logger.ts │ │ ├── logs.ts │ │ ├── refs.ts │ │ └── resize.ts │ └── svg │ │ ├── chevron-left.svg │ │ ├── chevron-right.svg │ │ └── logo.svg ├── cases │ ├── component-aliases │ │ ├── index.liquid │ │ ├── index.test.ts │ │ └── readme.md │ ├── component-bindings │ │ ├── index.liquid │ │ ├── index.test.ts │ │ ├── pages │ │ │ ├── page-a.liquid │ │ │ ├── page-b.liquid │ │ │ └── page-c.liquid │ │ └── readme.md │ ├── component-hooks │ │ ├── index.liquid │ │ ├── index.test.ts │ │ └── readme.md │ ├── component-incremental │ │ ├── include │ │ │ ├── component.liquid │ │ │ ├── pages.liquid │ │ │ └── target.liquid │ │ ├── index.liquid │ │ ├── index.test.ts │ │ ├── pages │ │ │ ├── page-a.liquid │ │ │ ├── page-b.liquid │ │ │ └── page-c.liquid │ │ └── readme.md │ ├── component-lifecycle │ │ ├── index.liquid │ │ ├── index.test.ts │ │ └── readme.md │ ├── component-literal │ │ ├── index.liquid │ │ ├── index.test.ts │ │ └── readme.md │ ├── component-merge │ │ ├── include │ │ │ ├── pages.liquid │ │ │ └── shared.liquid │ │ ├── index.liquid │ │ ├── index.test.ts │ │ ├── pages │ │ │ ├── page-a.liquid │ │ │ ├── page-b.liquid │ │ │ └── page-c.liquid │ │ └── readme.md │ ├── component-multiple │ │ ├── index.liquid │ │ └── readme.md │ ├── component-nesting │ │ ├── index.liquid │ │ ├── index.test.ts │ │ └── readme.md │ ├── component-observer │ │ ├── index.liquid │ │ ├── index.test.ts │ │ └── readme.md │ ├── component-refs │ │ ├── include │ │ │ ├── demo.liquid │ │ │ └── pages.liquid │ │ ├── index.liquid │ │ ├── index.test.ts │ │ ├── pages │ │ │ ├── page-a.liquid │ │ │ └── page-b.liquid │ │ └── readme.md │ ├── component-types │ │ ├── include │ │ │ ├── legend.liquid │ │ │ └── pages.liquid │ │ ├── index.liquid │ │ ├── index.test.ts │ │ └── pages │ │ │ ├── defined.liquid │ │ │ ├── dom-defined.liquid │ │ │ └── overwrite.liquid │ ├── config-fragments │ │ ├── index.liquid │ │ └── tests │ │ │ ├── error.liquid │ │ │ ├── error.test.ts │ │ │ ├── valid.liquid │ │ │ └── valid.test.ts │ ├── directive-target │ │ ├── include │ │ │ └── pages.liquid │ │ ├── index.liquid │ │ ├── pages │ │ │ ├── page-a.liquid │ │ │ ├── page-b.liquid │ │ │ └── page-c.liquid │ │ └── readme.md │ ├── lifecycle-events │ │ ├── index.liquid │ │ ├── index.test.ts │ │ └── readme.md │ └── morph-nodes │ │ ├── include │ │ └── pages.liquid │ │ ├── index.liquid │ │ ├── index.test.ts │ │ ├── pages │ │ ├── page-a.liquid │ │ ├── page-b.liquid │ │ └── page-c.liquid │ │ └── readme.md ├── package.json ├── pnpm-lock.yaml ├── tsup.config.ts └── views │ ├── base.liquid │ ├── data │ └── cases.json │ └── include │ ├── cases.liquid │ ├── explorer.liquid │ ├── index.liquid │ └── logger.liquid ├── tsconfig.json ├── tsup.config.ts └── types ├── components.d.ts ├── config.d.ts ├── context.d.ts ├── events.d.ts ├── global.d.ts ├── http.d.ts ├── options.d.ts ├── page.d.ts ├── session.d.ts └── types.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # DIRECTORIES 2 | node_modules 3 | public 4 | !types/public 5 | docs/.netlify 6 | docs/netlify/functions/search/** 7 | !docs/netlify/functions/search/index.js 8 | 9 | # FILES 10 | .pnpm-debug.log 11 | .vscode/settings.json 12 | .liquidrc 13 | -------------------------------------------------------------------------------- /.liquidrc: -------------------------------------------------------------------------------- 1 | { 2 | "engine": "11ty", 3 | "files": { 4 | "includes": [ 5 | "./docs/src/views/include/*.liquid" 6 | ], 7 | "data": [ 8 | "./docs/src/data/*.json" 9 | ] 10 | }, 11 | "format": { 12 | "wrap": 125, 13 | "crlf": false, 14 | "indentSize": 2, 15 | "preserveLine": 1, 16 | "endNewline": false, 17 | "liquid": { 18 | "commentIndent": true, 19 | "commentNewline": true, 20 | "delimiterPlacement": "consistent", 21 | "delimiterTrims": "tags", 22 | "lineBreakSeparator": "before", 23 | "indentAttribute": true, 24 | "normalizeSpacing": true, 25 | "preserveComment": false, 26 | "quoteConvert": "single", 27 | "forceFilter": 2, 28 | "forceArgument": 3 29 | }, 30 | "markup": { 31 | "quoteConvert": "double", 32 | "lineBreakValue": "preserve", 33 | "commentNewline": true, 34 | "forceIndent": false, 35 | "ignoreJS": false, 36 | "forceAttribute": 2, 37 | "preserveText": true 38 | }, 39 | "json": { 40 | "braceAllman": true, 41 | "arrayFormat": "indent", 42 | "objectIndent": "indent" 43 | }, 44 | "style": { 45 | "correct": true, 46 | "commentNewline": true, 47 | "commentIndent": true, 48 | "noLeadZero": true, 49 | "quoteConvert": "single", 50 | "classPadding": true 51 | }, 52 | "script": { 53 | "correct": true, 54 | "arrayFormat": "indent", 55 | "objectIndent": "indent", 56 | "methodChain": 3, 57 | "caseSpace": true, 58 | "quoteConvert": "single", 59 | "elseNewline": true, 60 | "functionNameSpace": true, 61 | "functionSpace": true, 62 | "commentNewline": true, 63 | "noCaseIndent": true 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # DIRECTORIES 2 | .vscode/ 3 | .github/ 4 | node_modules/ 5 | docs/ 6 | test/ 7 | src/ 8 | 9 | # FILES 10 | tsup.config.ts 11 | tsconfig.json 12 | pnpm-lock.yaml 13 | 14 | # DOT FILES 15 | .gitignore 16 | .prettierignore 17 | .npmignore 18 | 19 | # MARKDOWN 20 | readme.md 21 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.ts 3 | *.mjs 4 | *.cjs 5 | *.html 6 | *.liquid 7 | 8 | -------------------------------------------------------------------------------- /.vscode/defaults/pages/page-a.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: iframe.liquid 3 | group: 'directive' 4 | permalink: '/iframe/using-defaults/page-a' 5 | data: 'using-defaults' 6 | --- 7 | 8 | # Page A 9 | 10 | This is an example of SPX using the default options. In our JavaScript file we called `spx.connect()` with no additional options. This instructed SPX to replace the entire `` fragment of the page. 11 | -------------------------------------------------------------------------------- /.vscode/defaults/pages/page-b.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: iframe.liquid 3 | group: 'directive' 4 | permalink: '/iframe/using-defaults/page-b' 5 | data: 'using-defaults' 6 | --- 7 | 8 | # Page B 9 | 10 | This is Page B. SPX fetched this page when your cursor hovered the `` element. By default, SPX is morphing between pages, which means only content which differed has been swapped. 11 | -------------------------------------------------------------------------------- /.vscode/defaults/pages/page-c.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: iframe.liquid 3 | group: 'directive' 4 | permalink: '/iframe/using-defaults/page-c' 5 | data: 'using-defaults' 6 | --- 7 | 8 | # Page C 9 | 10 | This is **Page C** and unlike pages `A`, `B` and `D` this `` link element was annotated with `spx-disable` which resulted in a traditional navigation. Notice how there was a small flash when clicking this link. 11 | -------------------------------------------------------------------------------- /.vscode/defaults/pages/page-d.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: iframe.liquid 3 | group: 'directive' 4 | permalink: '/iframe/using-defaults/page-d' 5 | data: 'using-defaults' 6 | --- 7 | 8 | # Page D 9 | 10 | This is **Page D** and loaded the same as pages `A` and `B`. The SPX session has this page saved to its snapshot cache, however if you click `Page C` the snapshot cache will be reset. 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "ms-vscode.vscode-typescript-next", 6 | "vscode-icons-team.vscode-icons", 7 | "streetsidesoftware.code-spell-checker" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/hooks/include/counter.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 | 0 7 |
8 |
9 | 15 | 21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /.vscode/hooks/page-a.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: '/iframe/using-hooks/page-a/index.html' 3 | layout: iframe-block.liquid 4 | links: 'using-hooks' 5 | logger: true 6 | footer: true 7 | main: 'my-4 pr-5 pl-3' 8 | name: 'Lifecycle Hooks Demo' 9 | --- 10 | 11 | # Page A 12 | 13 | This page does not contain any components. Navigate to [Page B](/iframe/using-hooks/page-b/), then return to this page and notice to hooks being triggered within the component. 14 | -------------------------------------------------------------------------------- /.vscode/hooks/page-b.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: '/iframe/using-hooks/page-b/index.html' 3 | layout: iframe-block.liquid 4 | links: 'using-hooks' 5 | logger: true 6 | footer: true 7 | main: 'my-4 pr-5 pl-3' 8 | name: 'Lifecycle Hooks Demo' 9 | --- 10 | 11 | # Page B 12 | 13 | This page contains contains a counter component. Visiting this page demonstrates how lifecycle hooks incrementally execute. Notice how the `connect` hook executes only once. 14 | 15 | {% include 'docs/components/examples/hooks/include/counter' %} 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Νίκος Σαβίδης 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /docs/.eleventyignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/docs/.eleventyignore -------------------------------------------------------------------------------- /docs/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | 4 | [[headers]] 5 | for = "/*" 6 | 7 | [headers.values] 8 | Access-Control-Allow-Origin = "*" 9 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spx/docs", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "SPX Documentation", 6 | "homepage": "https://spx.js.org", 7 | "license": "MIT", 8 | "author": "ΝΙΚΟΛΑΣ ΣΑΒΒΙΔΗΣ", 9 | "scripts": { 10 | "dev": "rm -rf public && concurrently \"pnpm run 11ty:watch\" \"pnpm run sass:watch\" \"pnpm tsup:watch\"", 11 | "stage": "netlify dev -c \"eleventy --watch --serve --watch\" --targetPort 8080", 12 | "build": "rm -rf public && pnpm run 11ty:build && pnpm run sass:build && pnpm run postcss && pnpm run tsup:build", 13 | "postcss": "postcss public/style.css -r --no-map", 14 | "release": "netlify build && netlify deploy", 15 | "11ty:watch": "eleventy --config=.eleventy.cjs --serve --watch --quiet", 16 | "11ty:build": "ENV=prod eleventy --config=.eleventy.cjs ", 17 | "tsup:watch": "tsup --watch", 18 | "tsup:build": "tsup --minify --env.NODE_ENV production", 19 | "search:json": "tsup --env.NODE_ENV search", 20 | "sass:build": "sass ./src/sass/stylesheet.scss public/style.css --no-source-map", 21 | "sass:watch": "sass ./src/sass/stylesheet.scss public/style.css --poll --watch" 22 | }, 23 | "stylelint": { 24 | "extends": "@sissel/stylelint-config", 25 | "ignoreFiles": [ 26 | "public", 27 | "node_modules" 28 | ] 29 | }, 30 | "dependencies": { 31 | "fuse.js": "^7.1.0", 32 | "match-sorter": "^8.0.1", 33 | "qvp": "^0.3.2", 34 | "relapse": "^0.9.1", 35 | "spx": "link:.." 36 | }, 37 | "devDependencies": { 38 | "@11ty/eleventy": "^3.1.0", 39 | "@brixtol/bootstrap": "^1.7.5", 40 | "@fullhuman/postcss-purgecss": "^7.0.2", 41 | "@sissel/stylelint-config": "^1.2.4", 42 | "autoprefixer": "^10.4.21", 43 | "concurrently": "^9.1.2", 44 | "cssnano": "^7.0.7", 45 | "e11ty": "^0.1.4", 46 | "markdown-it-container": "^4.0.0", 47 | "netlify-cli": "^21.4.2", 48 | "papyrus": "^0.7.3", 49 | "postcss": "^8.5.3", 50 | "postcss-cli": "^11.0.1", 51 | "sass": "^1.89.0", 52 | "tsup": "^8.5.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const purge = require('@fullhuman/postcss-purgecss'); 2 | const autoprefixer = require('autoprefixer'); 3 | const cssnano = require('cssnano'); 4 | 5 | module.exports = { 6 | plugins: [ 7 | autoprefixer(), 8 | cssnano({ 9 | preset: 'default' 10 | }), 11 | purge( 12 | { 13 | variables: true, 14 | content: [ 15 | 'public/**/*.html', 16 | 'public/**/*.js' 17 | ] 18 | } 19 | ) 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /docs/src/app/bundle.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | import { Drawer } from './components/drawer'; 3 | import { Dropdown } from './components/dropdown'; 4 | import { Search } from './components/search'; 5 | import { Sidebar } from './components/sidebar'; 6 | import { ScrollSpy } from './components/scrollspy'; 7 | 8 | spx({ 9 | fragments: [ 10 | 'content', 11 | 'menu' 12 | ], 13 | components: { 14 | Dropdown, 15 | Drawer, 16 | Search, 17 | ScrollSpy, 18 | Sidebar 19 | }, 20 | logLevel: 1, 21 | hover: { 22 | threshold: 100, 23 | trigger: 'href' 24 | }, 25 | progress: { 26 | bgColor: 'red' 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /docs/src/app/components/indicator.ts: -------------------------------------------------------------------------------- 1 | import spx, { SPX } from 'spx'; 2 | 3 | export class Indicator extends spx.Component({ 4 | nodes: [ 5 | 'marker' 6 | ] 7 | }) { 8 | 9 | toggle ({ attrs: { list } }: SPX.Event<{ list: number }>) { 10 | 11 | console.log(this.markerNode); 12 | 13 | this.markerNode.style.setProperty( 14 | 'transform', 15 | `translateY(${this.view.offsetTop}px)` 16 | ); 17 | 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /docs/src/app/components/scrollspy.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | export class ScrollSpy extends spx.Component({ 4 | name: 'scrollspy', 5 | nodes: [ 6 | 'href', 7 | 'anchor' 8 | ], 9 | state: { 10 | class: String, 11 | anchors: Array 12 | } 13 | }) { 14 | 15 | onmount () { 16 | 17 | if (!this.hrefExists) return; 18 | 19 | window.onscroll = this.onScroll.bind(this); 20 | this.hrefNode.classList.add(this.state.class); 21 | this.state.anchors.length === 0 && this.hrefNodes.forEach(a => { 22 | this.state.anchors.push(a.href.slice(a.href.lastIndexOf('#') + 1)); 23 | }); 24 | 25 | this.onScroll(); 26 | 27 | } 28 | 29 | onScroll () { 30 | this.anchorNodes.forEach((node, i) => { 31 | if (!this.state.anchors.includes(node.id)) return; 32 | if (node.getBoundingClientRect().top < window.screenY && this.hrefNodes[i]) { 33 | this.hrefNodes.forEach(href => href.classList.remove(this.state.class)); 34 | this.hrefNodes[i].classList.add(this.state.class); 35 | } 36 | }); 37 | }; 38 | 39 | /* -------------------------------------------- */ 40 | /* TYPE VALUES */ 41 | /* -------------------------------------------- */ 42 | 43 | } 44 | -------------------------------------------------------------------------------- /docs/src/app/components/sidebar.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | import relapse, { Relapse } from 'relapse'; 3 | import spx from 'spx'; 4 | 5 | export class Sidebar extends spx.Component({ 6 | nodes: [], 7 | state: { 8 | multiple: Boolean, 9 | open: { 10 | default: 0, 11 | typeof: Number 12 | } 13 | } 14 | }) { 15 | 16 | onmount () { 17 | 18 | this.relapse ? this.relapse.reinit() : this.relapse = relapse(this.view); 19 | 20 | } 21 | 22 | unmount () { 23 | 24 | this.relapse.destroy(); 25 | 26 | } 27 | 28 | public relapse: Relapse; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/app/examples/alias.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | export class Alias extends spx.Component({ 4 | nodes: [ 5 | 'feedback' 6 | ], 7 | state: { 8 | count: Number, 9 | limit: Number, 10 | notify: Boolean 11 | } 12 | }) { 13 | 14 | increment () { 15 | if (this.state.count < this.state.limit) { 16 | ++this.state.count; 17 | } else if (!this.state.notify) { 18 | this.feedbackNode.innerText = `Reached Limit: ${this.state.limit}`; 19 | this.state.notify = true; 20 | } 21 | } 22 | 23 | decrement () { 24 | if (this.state.count === 0) return; 25 | --this.state.count; 26 | if (this.state.notify) { 27 | this.feedbackNode.innerText = ''; 28 | this.state.notify = false; 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /docs/src/app/examples/counter.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | import { IFrame } from './iframe'; 3 | 4 | export class Counter extends spx.Component({ 5 | state: { 6 | count: Number 7 | }, 8 | nodes: [ 9 | 'count' 10 | ] 11 | }) { 12 | 13 | get iframe () { 14 | return spx.component -------------------------------------------------------------------------------- /docs/src/views/include/landing.liquid: -------------------------------------------------------------------------------- 1 |
4 | 10 | 11 | 67 | 68 |
-------------------------------------------------------------------------------- /docs/src/views/include/lifecycle-events.liquid: -------------------------------------------------------------------------------- 1 |
2 |
5 | {{ meta.method }} 6 |
7 |
8 | {{ meta.description }} 9 |
10 |
11 | {%- for item in lifecycles.hooks -%} 12 |
13 | 16 | {{ item.name }}(){} 17 | 18 |
19 | {{ item.description }} 20 |
21 |
22 | {%- endfor -%} -------------------------------------------------------------------------------- /docs/src/views/include/navbar.liquid: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/views/include/search.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 32 |
33 |
34 |
-------------------------------------------------------------------------------- /docs/src/views/include/sidebar-header.liquid: -------------------------------------------------------------------------------- 1 | {%- unless page.url == '/' -%} 2 | 30 | {%- endunless -%} -------------------------------------------------------------------------------- /docs/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig( 4 | { 5 | entry: { 6 | 'bundle.min': './src/app/bundle.ts', 7 | 'iframe.min': './src/app/iframe.ts' 8 | }, 9 | outDir: './public', 10 | outExtension: () => ({ js: '.js' }), 11 | clean: false, 12 | treeshake: false, 13 | splitting: false, 14 | minify: false, 15 | target: 'es2017', 16 | platform: 'browser', 17 | format: [ 18 | 'iife' 19 | ] 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /src/app/session.ts: -------------------------------------------------------------------------------- 1 | import type { Session } from 'types'; 2 | import { m, o, p, s } from '../shared/native'; 3 | import { CTX } from 'types/context'; 4 | 5 | /** 6 | * Shared Contexts 7 | * 8 | * This object holds data used during the morph, context and rendering cycles. 9 | */ 10 | export const ctx: CTX = { 11 | marks: s(), 12 | store: undefined, 13 | snaps: [] 14 | }; 15 | 16 | export const $: Session = { 17 | index: '', 18 | eval: true, 19 | patched: false, 20 | loaded: false, 21 | logLevel: 2, 22 | qs: o(), 23 | fragments: m(), 24 | mounted: s(), 25 | registry: m(), 26 | instances: m(), 27 | maps: p({ get: (m, k) => $.instances.get(m[k]) }), 28 | events: o(), 29 | observe: o(), 30 | memory: o(), 31 | pages: o(), 32 | snaps: o(), 33 | resources: s(), 34 | config: { 35 | fragments: [ 'body' ], 36 | timeout: 30000, 37 | globalThis: true, 38 | schema: 'spx-', 39 | logLevel: 3, 40 | cache: true, 41 | components: null, 42 | maxCache: 100, 43 | reverse: true, 44 | preload: null, 45 | annotate: false, 46 | eval: { 47 | script: null, 48 | style: null, 49 | link: null, 50 | meta: false 51 | }, 52 | hover: { 53 | trigger: 'href', 54 | threshold: 250 55 | }, 56 | intersect: { 57 | rootMargin: '0px 0px 0px 0px', 58 | threshold: 0 59 | }, 60 | proximity: { 61 | distance: 75, 62 | threshold: 250, 63 | throttle: 500 64 | }, 65 | progress: { 66 | bgColor: '#111', 67 | barHeight: '3px', 68 | minimum: 0.08, 69 | easing: 'linear', 70 | speed: 200, 71 | threshold: 500, 72 | trickle: true, 73 | trickleSpeed: 200 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/morph/walk.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Walk Elements 3 | * 4 | * Walks the DOM and executes callback on all `Element` types (i.e, `4`). 5 | * We cannot `querySelector` attributes which SPX uses due to their syntactical patterns, 6 | * so we walk the DOM and cherry pick SPX Component specific directives. 7 | * 8 | * This function is will traverse the DOM and return Elements from which we analyze and 9 | * reason with to compose component scopes. 10 | */ 11 | export const walkElements = (node: T, callback: (node: T) => any) => { 12 | 13 | const cb = callback(node); 14 | 15 | if (cb === false) return; 16 | 17 | // @ts-expect-error 18 | if (cb === 1) node = node.nextSibling; 19 | 20 | let e: Element; 21 | let i: number; 22 | 23 | if (node.firstElementChild) { 24 | i = 0; 25 | e = node.children[i]; 26 | } 27 | 28 | while (e) { 29 | if (e) walkElements(e, callback); 30 | e = node.children[++i]; 31 | } 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /src/shared/const.ts: -------------------------------------------------------------------------------- 1 | import { s } from './native'; 2 | 3 | /** 4 | * UUID + UID Cache 5 | * 6 | * Ensures that no repeat identifers get generated 7 | */ 8 | export const Identifiers: Set = s(); 9 | 10 | /** 11 | * Character Entities Mapping 12 | */ 13 | export const Entities: Record = { 14 | '&': '&', 15 | '<': '<', 16 | '>': '>', 17 | '"': '"', 18 | ''': "'", 19 | '/': '/', 20 | '`': '`', 21 | '=': '=' 22 | }; 23 | 24 | /** 25 | * Array list of applied to session api method 26 | */ 27 | export const SessionMethod = [ 28 | 'config', 29 | 'snaps', 30 | 'pages', 31 | 'observers', 32 | 'fragments', 33 | 'instances', 34 | 'mounted', 35 | 'registry', 36 | 'reference', 37 | 'memory' 38 | ]; 39 | 40 | /** 41 | * TypedArray Prototype 42 | */ 43 | export const TypedArray = typeof Uint8Array === 'undefined' 44 | ? [] 45 | : [ Object.getPrototypeOf(Uint8Array) ]; 46 | 47 | /** 48 | * Rich Text HTTP Response 49 | */ 50 | export const RichText = 'Blob ArrayBuffer DataView FormData URLSearchParams File' 51 | .split(' ') 52 | .map(x => globalThis[x]).filter(x => x) 53 | .concat(TypedArray); 54 | -------------------------------------------------------------------------------- /src/shared/logs.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { $ } from '../app/session'; 3 | import { Colors, LogLevel, Æ } from './enums'; 4 | 5 | class SPXError extends Error { 6 | 7 | constructor (message: string, public context?: any) { 8 | super(message); 9 | this.name = 'SPX Error'; 10 | if (context) this.context = context; 11 | } 12 | 13 | } 14 | 15 | const PREFIX = 'SPX '; 16 | const START = '\x1b['; 17 | const END = '\x1b[0m'; 18 | 19 | /** 20 | * Color 21 | */ 22 | export const C = (COLOR: Æ, text: string) => START + COLOR + text + END; 23 | 24 | /** 25 | * Console Debug 26 | */ 27 | export const debug = (message: string | string[], color: Colors = Colors.GRAY) => { 28 | if ($.logLevel === LogLevel.DEBUG) { 29 | // @ts-ignore 30 | console.debug('%c' + PREFIX + (Array.isArray(message) ? message.join(' ') : message), `color: ${color};`); 31 | } 32 | }; 33 | 34 | /** 35 | * Console Warnings 36 | */ 37 | export const warn = (message: string, context?: any) => { 38 | if ($.logLevel >= LogLevel.WARN) { 39 | context ? console.warn(PREFIX + message, context) : console.warn(PREFIX + message); 40 | } 41 | }; 42 | 43 | /** 44 | * Console Info 45 | */ 46 | export const info = (...message: string[]) => { 47 | if ($.logLevel === LogLevel.INFO) { 48 | console.info(PREFIX + C(Æ.Gray, message.join(''))); 49 | } 50 | }; 51 | 52 | /** 53 | * Console Error (Will throw) 54 | */ 55 | export const error = (message: string, context?: any) => { 56 | 57 | throw new SPXError(message, context); 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /src/shared/patch.ts: -------------------------------------------------------------------------------- 1 | import { $ } from '../app/session'; 2 | 3 | /** 4 | * Monkey Patch `setAttribute` 5 | * 6 | * **Lifted from the alpine.js morph algorithm.** 7 | * 8 | * Ensures the native `setAttribute` wont throw when applying event attribute morphs 9 | * and attribute name is event type containing `@` symbols. The standard Element.setAttribute 10 | * method does not support this, thus this patch makes it possible by executing a dummy 11 | * clone of sorts. 12 | * 13 | * @see https://t.ly/u4U8W 14 | */ 15 | export const patchSetAttribute = () => { 16 | 17 | if ($.patched) return; 18 | 19 | $.patched = true; 20 | 21 | const n = Element.prototype.setAttribute; 22 | const e = document.createElement('i'); 23 | 24 | Element.prototype.setAttribute = function setAttribute (name, value) { 25 | 26 | if (name.indexOf('@') < 0) return n.call(this, name, value); 27 | 28 | e.innerHTML = ``; 29 | 30 | const attr = e.firstElementChild.getAttributeNode(name); 31 | 32 | e.firstElementChild.removeAttributeNode(attr); 33 | 34 | this.setAttributeNode(attr); 35 | 36 | }; 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /test/.eleventy.cjs: -------------------------------------------------------------------------------- 1 | const { defineConfig, sprite, markdown } = require('e11ty'); 2 | const papyrus = require('papyrus'); 3 | 4 | module.exports = defineConfig(function (config) { 5 | 6 | markdown(config, { 7 | papyrus: { 8 | default: { 9 | lineNumbers: true 10 | } 11 | }, 12 | options: { 13 | html: false, 14 | breaks: true, 15 | linkify: true 16 | } 17 | }); 18 | 19 | config.addPlugin(sprite, { inputPath: './asset/svg', spriteShortCode: 'sprite' }); 20 | config.addShortcode('version', function () { return require('../package.json').version; }); 21 | config.addShortcode('iframe', function (url, height = 180) { 22 | return ``; 23 | }); 24 | 25 | config.addPassthroughCopy({ 'asset/fonts/': 'asset/fonts/' }); 26 | 27 | return { 28 | htmlTemplateEngine: 'liquid', 29 | passthroughFileCopy: false, 30 | pathPrefix: '', 31 | templateFormats: [ 32 | 'liquid', 33 | 'html', 34 | 'md' 35 | ], 36 | dir: { 37 | input: '.', 38 | output: 'public', 39 | includes: '', 40 | layouts: '', 41 | data: 'views/data' 42 | } 43 | }; 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /test/.eleventyignore: -------------------------------------------------------------------------------- 1 | .eleventy.cjs 2 | .eleventyignore 3 | package.json 4 | tsup.config.ts 5 | asset -------------------------------------------------------------------------------- /test/asset/fonts/proxima-nova-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/asset/fonts/proxima-nova-bold.woff2 -------------------------------------------------------------------------------- /test/asset/fonts/proxima-nova-light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/asset/fonts/proxima-nova-light.woff2 -------------------------------------------------------------------------------- /test/asset/fonts/proxima-nova-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/asset/fonts/proxima-nova-regular.woff2 -------------------------------------------------------------------------------- /test/asset/style/resize.scss: -------------------------------------------------------------------------------- 1 | flex { 2 | display: flex; 3 | overflow: hidden; 4 | } 5 | 6 | /* flex-item > flex { 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | } */ 11 | 12 | flex.h { 13 | flex-direction: row; 14 | background-color: $white; 15 | z-index: 1; 16 | } 17 | 18 | flex.v { 19 | flex-direction: column; 20 | } 21 | 22 | flex-item { 23 | /* display: flex; */ 24 | /* position: relative; */ 25 | /* overflow: hidden; */ 26 | overflow: auto; 27 | } 28 | 29 | flex > flex-resizer { 30 | flex: 0 0 12px; 31 | background-color: $papyrus-complete-border-color; 32 | background-repeat: no-repeat; 33 | background-position: center; 34 | border-bottom: 2px solid $papyrus-code-bg; 35 | z-index: 1; 36 | } 37 | 38 | flex.h > flex-resizer { 39 | cursor: ew-resize; 40 | background-image: url("data:image/svg+xml;utf8,"); 41 | } 42 | 43 | flex.v > flex-resizer { 44 | cursor: ns-resize; 45 | 46 | background-image: url("data:image/svg+xml;utf8,"); 47 | } 48 | -------------------------------------------------------------------------------- /test/asset/suite.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | import { Alias } from '../cases/component-aliases/index.test'; 4 | import { Async } from '../cases/component-hooks/index.test'; 5 | import { Types } from '../cases/component-types/index.test'; 6 | import { Incremental } from '../cases/component-incremental/index.test'; 7 | import { Literal } from '../cases/component-literal/index.test'; 8 | import { Merge } from '../cases/component-merge/index.test'; 9 | import { Lifecycle } from '../cases/component-lifecycle/index.test'; 10 | import { Nesting } from '../cases/component-nesting/index.test'; 11 | import { Refs1, Refs2 } from '../cases/component-refs/index.test'; 12 | import { MorphNodes } from '../cases/morph-nodes/index.test'; 13 | import { Mounted } from '../cases/component-observer/index.test'; 14 | 15 | import { Events } from '../cases/lifecycle-events/index.test'; 16 | /* INTERNAL ----------------------------------- */ 17 | 18 | import { code } from './suite/resize'; 19 | import { Cases } from './suite/cases'; 20 | 21 | // import { Logger } from './suite/logger'; 22 | import { Explorer } from './suite/explorer'; 23 | // import { Refs } from './suite/refs'; 24 | 25 | spx.http('https://jsonplaceholder.typicode.com/todos/1').then(data => { 26 | 27 | console.log(data); 28 | }); 29 | 30 | spx({ 31 | fragments: [ 32 | 'main' 33 | ], 34 | logLevel: 3, 35 | hover: { 36 | threshold: 100, 37 | trigger: 'href' 38 | }, 39 | progress: { 40 | bgColor: 'red' 41 | }, 42 | components: { 43 | // Logger, 44 | 45 | Events, 46 | Mounted, 47 | Explorer, 48 | Cases, 49 | Literal, 50 | Alias, 51 | Async, 52 | Types, 53 | Incremental, 54 | Merge, 55 | Lifecycle, 56 | Nesting, 57 | Refs1, 58 | Refs2, 59 | MorphNodes 60 | 61 | } 62 | })(function () { 63 | 64 | code(); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /test/asset/suite/cases.ts: -------------------------------------------------------------------------------- 1 | import spx, { SPX } from 'spx'; 2 | 3 | export class Cases extends spx.Component({ 4 | nodes: [ 'link' ] 5 | }) { 6 | 7 | onClick ({ target }: SPX.Event) { 8 | 9 | this.linkNodes.forEach(link => { 10 | 11 | link.isEqualNode(target) ? link.classList.add('fc-pink') : link.classList.remove('fc-pink'); 12 | 13 | }); 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /test/asset/svg/chevron-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/asset/svg/chevron-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cases/component-aliases/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | aliases: 4 | - foo 5 | - bar 6 | - baz 7 | --- 8 | 9 |
10 |
11 | {{ content }} 12 |
13 | {%- for alias in aliases -%} 14 |
15 |

{{ alias }} Alias {{ forloop.index }}

16 |
22 | 26 | 0 29 | 33 |
34 | 37 |
38 | {%- endfor -%} 39 |
40 |
41 |
-------------------------------------------------------------------------------- /test/cases/component-aliases/index.test.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | export class Alias extends spx.Component({ 4 | nodes: [ 5 | 'feedback', 6 | 'counter', 7 | 'bar' 8 | ], 9 | state: { 10 | min: Number, 11 | max: Number, 12 | qty: Number 13 | } 14 | }) { 15 | 16 | increment () { 17 | 18 | if (this.state.qty < this.state.max) { 19 | this.counterNode.innerHTML = `${++this.state.qty}`; 20 | } else { 21 | this.feedbackNode.innerText = `Maximum: ${this.state.max}`; 22 | } 23 | } 24 | 25 | decrement () { 26 | 27 | if (this.state.qty > this.state.min) { 28 | this.counterNode.innerHTML = `${--this.state.qty}`; 29 | } else { 30 | this.feedbackNode.innerText = `Minimum: ${this.state.min}`; 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /test/cases/component-aliases/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Component Alias' 3 | layout: cases/component-aliases/index.liquid 4 | permalink: '/components/alias/index.html' 5 | --- 6 | 7 | # Component Aliases 8 | 9 | Click component which uses an alias identifier. It is the same component but uses the id reference as identifier. 10 | 11 | --- 12 | -------------------------------------------------------------------------------- /test/cases/component-bindings/index.liquid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/cases/component-bindings/index.liquid -------------------------------------------------------------------------------- /test/cases/component-bindings/index.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/cases/component-bindings/index.test.ts -------------------------------------------------------------------------------- /test/cases/component-bindings/pages/page-a.liquid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/cases/component-bindings/pages/page-a.liquid -------------------------------------------------------------------------------- /test/cases/component-bindings/pages/page-b.liquid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/cases/component-bindings/pages/page-b.liquid -------------------------------------------------------------------------------- /test/cases/component-bindings/pages/page-c.liquid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/cases/component-bindings/pages/page-c.liquid -------------------------------------------------------------------------------- /test/cases/component-bindings/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/cases/component-bindings/readme.md -------------------------------------------------------------------------------- /test/cases/component-hooks/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Async Hooks' 3 | layout: cases/component-hooks/index.liquid 4 | permalink: '/components/async/index.html' 5 | --- 6 | 7 | # Component Hooks 8 | 9 | Test cases for component hooks. Covers both asynchronous and synchronous methods. 10 | 11 | --- 12 | -------------------------------------------------------------------------------- /test/cases/component-incremental/include/component.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 | 8 | 9 | 10 | 11 |
-------------------------------------------------------------------------------- /test/cases/component-incremental/include/pages.liquid: -------------------------------------------------------------------------------- 1 | {%- 2 | liquid 3 | assign pages = 'Page A,Page B,Page C' | split: ',' 4 | -%} 5 | 6 |
7 |
8 |
9 |
12 |
    13 | 14 | {%- for link in pages -%} 15 | {%- 16 | liquid 17 | assign slug = link | slugify 18 | assign url = link | slugify | prepend: '/components/incremental/' | append: '/' 19 | -%} 20 | 21 |
  • 22 | 28 | {{ link }} 29 | 30 |
  • 31 | {%- endfor -%} 32 |
33 |
34 |
35 |
36 |
-------------------------------------------------------------------------------- /test/cases/component-incremental/include/target.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
9 | 10 |
11 |
12 | Label: 13 |
14 | 15 | {{ title }} 16 | 17 |
18 | 19 |
20 |
21 | Counter: 22 |
23 | 0 26 |
27 | 28 |
29 |
30 | Color: 31 |
32 | 36 |
37 |
38 | 39 |
    40 |
  • 41 | CONNECT  42 | 45 |
  • 46 |
  • 47 | ONMMOUNT 48 | 51 |
  • 52 |
  • 53 | UNMMOUNT 54 | 0 57 |
  • 58 |
59 |
60 |
61 |
62 |
-------------------------------------------------------------------------------- /test/cases/component-incremental/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | --- 4 | 5 |

6 | Incremental 7 |

8 | 9 | {{ content }} 10 | 11 | {%- include 'cases/component-incremental/include/pages' -%} 12 | {%- include 'cases/component-incremental/include/target' -%} 13 | 14 |
17 | 18 | 24 | 25 |
26 | NOTE: 27 | The above button is a component event contained within a target element 28 |
29 |
-------------------------------------------------------------------------------- /test/cases/component-incremental/index.test.ts: -------------------------------------------------------------------------------- 1 | import spx, { SPX } from 'spx'; 2 | 3 | export class Incremental extends spx.Component({ 4 | name: 'incremental', 5 | state: { 6 | label: String, 7 | color: String, 8 | count: Number, 9 | click: Number, 10 | connect: Number, 11 | onmount: Number, 12 | unmount: 0 13 | }, 14 | nodes: [ 15 | 'color', 16 | 'count', 17 | 'label' 18 | ] 19 | }) { 20 | 21 | connect (): void { 22 | 23 | ++this.state.connect; 24 | 25 | } 26 | 27 | onmount (): void { 28 | 29 | ++this.state.onmount; 30 | 31 | } 32 | 33 | unmount (): void { 34 | 35 | ++this.state.unmount; 36 | 37 | } 38 | 39 | labelValue ({ attrs }: SPX.Event<{ text: string; }>) { 40 | 41 | this.labelNode.innerText = attrs.text; 42 | 43 | } 44 | 45 | labelInput (event: SPX.InputEvent) { 46 | if (event.target instanceof HTMLInputElement) { 47 | 48 | this.labelNode.innerText = event.target.value; 49 | 50 | } 51 | } 52 | 53 | changeColor (event: SPX.InputEvent<{}, HTMLInputElement>) { 54 | 55 | this.state.color = event.target.value; 56 | this.colorNodes.forEach(color => { 57 | color.style.backgroundColor = this.state.color; 58 | }); 59 | 60 | } 61 | 62 | reset () { 63 | 64 | this.state.count = 0; 65 | 66 | } 67 | 68 | onClick () { 69 | 70 | ++this.state.click; 71 | } 72 | 73 | increment () { 74 | 75 | ++this.state.count; 76 | 77 | } 78 | 79 | decrement () { 80 | 81 | --this.state.count; 82 | 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /test/cases/component-incremental/pages/page-a.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Incremental - Page A' 3 | layout: views/base.liquid 4 | permalink: '/components/incremental/page-a/index.html' 5 | base: /components/incremental/ 6 | --- 7 | 8 |

Incremental - Page A

9 | 10 | {{ content }} 11 | 12 | {%- include 'cases/component-incremental/include/pages' -%} 13 | {%- include 'cases/component-incremental/include/target' -%} 14 | 15 |
18 | 19 | 24 | 25 |
28 | New Nodes: 29 |
30 | 31 |
32 |
39 |
40 |
41 |
45 |
46 |
47 | NOTE: 48 | The above input element is a component event contained within a target element. We also added additional nodes which will reflect the input colors. In the component, changes will use the sugar helpers. 49 |
50 |
-------------------------------------------------------------------------------- /test/cases/component-incremental/pages/page-b.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Incremental - Page B' 3 | layout: views/base.liquid 4 | permalink: '/components/incremental/page-b/index.html' 5 | base: /components/incremental/ 6 | --- 7 | 8 |

Incremental - Page B

9 | 10 | {{ content }} 11 | 12 | {%- include 'cases/component-incremental/include/pages' -%} 13 | {%- include 'cases/component-incremental/include/target' -%} 14 | 15 |
18 | 19 | 25 | 26 | 33 | 34 |
35 | NOTE: 36 | The above buttons are component events contained within a target element 37 |
38 |
-------------------------------------------------------------------------------- /test/cases/component-incremental/pages/page-c.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Incremental - Page C' 3 | layout: views/base.liquid 4 | group: 'directive' 5 | permalink: '/components/incremental/page-c/index.html' 6 | base: /components/incremental/ 7 | --- 8 | 9 |

Incremental - Page C

10 | 11 | {{ content }} 12 | 13 | {%- include 'cases/component-incremental/include/pages' -%} 14 | {%- include 'cases/component-incremental/include/target' -%} 15 | 16 |
19 | 20 | 26 | 27 | 32 | 33 |
34 | NOTE: 35 | The above button and text input are component events contained within a target element 36 |
37 |
-------------------------------------------------------------------------------- /test/cases/component-incremental/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Component Incremental' 3 | layout: cases/component-incremental/index.liquid 4 | permalink: '/components/incremental/index.html' 5 | --- 6 | 7 | The test case is for incremental handling of a component. Incremental support in SPX occurs when a mounted component that is persisted outside of a defined fragment or specific target has additional nodes or events associated to the instance in an incremental manner. The below component is mounted, but **Page A**, **Page B** and **Page C** are targeting elements outside of its mount point. The intention here is to ensure that we can attach events and extend functionality to nodes while the component persists between navigations. 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /test/cases/component-lifecycle/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | --- 4 | 5 |
6 |
7 | {{ content }} 8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 | connect count 22 |
23 | 0 26 |

27 | Never more than 1 28 |

29 |
30 |
31 |
32 |
33 |
34 | onmount count 35 |
36 | 0 39 |

40 | Always 1 more than unmount 41 |

42 |
43 |
44 |
45 |
46 |
47 | unmount count 48 |
49 | 0 52 |

53 | Always 1 less than onmount. 54 |

55 |
56 |
57 |
58 |
59 |
-------------------------------------------------------------------------------- /test/cases/component-lifecycle/index.test.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | export class Lifecycle extends spx.Component({ 4 | state: { 5 | connect: Number, 6 | onmount: Number, 7 | unmount: 0 8 | } 9 | }) { 10 | 11 | connect (): void { 12 | 13 | ++this.state.connect; 14 | 15 | } 16 | 17 | onmount (): void { 18 | 19 | ++this.state.onmount; 20 | 21 | } 22 | 23 | unmount (): void { 24 | 25 | ++this.state.unmount; 26 | 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /test/cases/component-lifecycle/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Component Lifecycle' 3 | layout: cases/component-lifecycle/index.liquid 4 | permalink: '/components/lifecycle/index.html' 5 | --- 6 | 7 | # 🧪 Component Lifecycle Hooks 8 | 9 | Lifecycle hooks demonstration, tracking `connect`, `onmount` and `unmount` callback methods within component. The counters will reflect the number of times a hook has executed. We are using `spx-bind` to reflect the counter numbers in the component we have setup for this test case. 10 | 11 | Navigating to another page and then back to this page will determine the success. The `onmount` and `unmount` values will change each time this page re-visited. The initial visit will result in `connect` and `onmount` having a value of `1`, with `unmount` having a value of `0` given that no unmount hook has fired until we leave the page. 12 | 13 | --- 14 | -------------------------------------------------------------------------------- /test/cases/component-literal/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | --- 4 | 5 |
8 |
9 | {{ content }} 10 |
11 |
14 |
17 | 18 |
-------------------------------------------------------------------------------- /test/cases/component-literal/index.test.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | export class Literal extends spx.Component({ 4 | state: { 5 | syncTime: Number, 6 | connectDelay: Number, 7 | connectText: String, 8 | onmountDelay: Number, 9 | fetchExample: Boolean, 10 | fetchedData: Array, 11 | firstRun: { 12 | typeof: Boolean, 13 | default: true 14 | } 15 | }, 16 | sugar: true, 17 | nodes: [ 18 | 'div', 19 | 'pre' 20 | ] 21 | }) { 22 | 23 | get buttonDom () { 24 | 25 | return spx.dom` 26 | 29 | `; 30 | 31 | } 32 | 33 | get preDom () { 34 | 35 | return spx.dom` 36 |
37 |

38 | This is a pre element and will be preserved 39 |

40 | 41 | 42 |
43 | 
44 |           const xxx = 'newlines preserved'
45 | 
46 |           function whitespace () {           return      'preserved'           }
47 | 
48 |         
49 |
50 | `; 51 | 52 | } 53 | 54 | onmount () { 55 | 56 | this.divNode.appendChild(this.buttonDom); 57 | this.preNode.appendChild(this.preDom); 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /test/cases/component-literal/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Component Literal' 3 | layout: cases/component-literal/index.liquid 4 | permalink: '/components/literal/index.html' 5 | --- 6 | 7 | # Component Literal 8 | 9 | Test cases for rendering a DOM literal element. Using the `this.dom` key and passing it a template literal. 10 | 11 | --- 12 | -------------------------------------------------------------------------------- /test/cases/component-merge/include/pages.liquid: -------------------------------------------------------------------------------- 1 | {% 2 | liquid 3 | assign pages = 'Page A,Page B,Page C' | split: ',' 4 | %} 5 | 6 |
7 |
8 |
9 |
12 |
    13 | 14 | {% for link in pages %} 15 | 16 | {% 17 | liquid 18 | assign slug = link | slugify 19 | assign url = link | slugify | prepend: '/components/merge/' | append: '/' 20 | assign hasTarget = target | slice: 0, 1 21 | %} 22 | 23 |
  • 24 | 29 | {{ link }} 30 | 31 |
  • 32 | {% endfor %} 33 |
34 |
35 |
36 |
37 |
-------------------------------------------------------------------------------- /test/cases/component-merge/include/shared.liquid: -------------------------------------------------------------------------------- 1 |
4 |
5 | Foo 6 |
7 |
8 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quo voluptatem cum error quidem necessitatibus, doloribus suscipit esse quasi nihil nemo laudantium, voluptatum eius magnam? Harum quis repellendus explicabo nobis. Officia? 9 |
10 |
11 |
12 |
13 | Bar 14 |
15 |
16 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quo voluptatem cum error quidem necessitatibus, doloribus suscipit esse quasi nihil nemo laudantium, voluptatum eius magnam? Harum quis repellendus explicabo nobis. Officia? 17 |
18 |
19 |
20 |
21 | Baz 22 |
23 |
24 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quo voluptatem cum error quidem necessitatibus, doloribus suscipit esse quasi nihil nemo laudantium, voluptatum eius magnam? Harum quis repellendus explicabo nobis. Officia? 25 |
26 |
27 |
28 |
29 | Qux 30 |
31 |
32 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quo voluptatem cum error quidem necessitatibus, doloribus suscipit esse quasi nihil nemo laudantium, voluptatum eius magnam? Harum quis repellendus explicabo nobis. Officia? 33 |
34 |
35 |
36 |
-------------------------------------------------------------------------------- /test/cases/component-merge/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | 3 | import spx from 'spx'; 4 | import relapse, { Relapse } from 'relapse'; 5 | 6 | export class Merge extends spx.Component() { 7 | 8 | onmount () { 9 | 10 | this.relapse = relapse(this.view); 11 | 12 | } 13 | 14 | unmount () { 15 | 16 | this.relapse.destroy(); 17 | 18 | } 19 | 20 | public relapse: Relapse; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /test/cases/component-merge/pages/page-a.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Merge - Page A' 3 | layout: views/base.liquid 4 | permalink: '/components/merge/page-a/index.html' 5 | shared: 'cases/component-merge/include/shared' 6 | --- 7 | 8 | {% include 'cases/component-merge/include/pages' %} 9 |

SPX Merge - Page A

10 | 11 |
12 | 13 | {{ content }} 14 | 15 |
16 |
17 |
20 |
21 | Foo A 22 |
23 |
24 | Quo voluptatem cum error quidem necessitatibus, doloribus suscipit esse quasi nihil nemo laudantium, voluptatum eius magnam? Harum quis repellendus explicabo nobis. Officia? 25 |
26 |
27 |
28 |
29 | Bar A 30 |
31 |
32 | Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quo voluptatem cum error quidem necessitatibus, doloribus suscipit esse quasi nihil nemo laudantium, voluptatum eius magnam? Harum quis repellendus explicabo nobis. Officia? 33 |
34 |
35 |
36 |
37 | Qux A 38 |
39 |
40 | Lorem ipsum dolor sit amet consectetur 41 |
42 |
43 |
44 |
45 |
46 |
47 |
-------------------------------------------------------------------------------- /test/cases/component-merge/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Component Merge' 3 | layout: cases/component-merge/index.liquid 4 | permalink: '/components/merge/index.html' 5 | --- 6 | 7 | # Merge Tests 8 | 9 | The component augments the DOM after mounting. The static `{ define: { merge: true } }`is passed, so the snapshot will be saved in its current state. The snapshot will reflect the augmentations applied. 10 | 11 | --- 12 | -------------------------------------------------------------------------------- /test/cases/component-multiple/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Basic' 3 | layout: views/base.liquid 4 | group: 'directive' 5 | permalink: '/components/basic/index.html' 6 | --- 7 | 8 |
12 |

13 | Basic Tests 14 |

15 |

16 | Click component which increments a number each time the button is pressed. 17 |

18 |
19 | 28 | 0 31 |
32 |
33 | 34 |
35 | 36 |
40 |

41 | Identical click component structure, clicks will be isolated. 42 |

43 |
44 | 53 | 0 56 |
57 |
-------------------------------------------------------------------------------- /test/cases/component-multiple/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panoply/spx/e76196f9bad498e565e0b1282033038372e62d22/test/cases/component-multiple/readme.md -------------------------------------------------------------------------------- /test/cases/component-nesting/index.test.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | export class Nesting extends spx.Component({ 4 | name: 'nesting', 5 | nodes: [ 'foo', 'bar', 'buttonBaz' ], 6 | state: { 7 | foo: '', 8 | count: 0 9 | } 10 | }) { 11 | 12 | onClick () { 13 | 14 | ++this.state.count; 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /test/cases/component-nesting/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Component Nesting' 3 | layout: cases/component-nesting/index.liquid 4 | permalink: '/components/nesting/index.html' 5 | --- 6 | 7 | # 🧪 Nested Component Structures 8 | 9 | Cases where components are nested within one another multiple times. In this case, each component is a child of the some component. We are using **alias** identifiers and only a single component is used in this case that has been mounted multiple times in the DOM. We are testing 2 capabilities here, the first being nested structures, the second being alias based targeting. 10 | 11 | --- 12 | -------------------------------------------------------------------------------- /test/cases/component-observer/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Component Observer' 3 | layout: views/base.liquid 4 | --- 5 | 6 |
7 |
8 | {{ content }} 9 |
10 |
11 |
12 |
15 |
-------------------------------------------------------------------------------- /test/cases/component-observer/index.test.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | import m from 'mithril'; 3 | 4 | function createComponent () { 5 | 6 | const mounted = document.querySelector('#mounted'); 7 | 8 | if (!mounted) return; 9 | 10 | m.mount(mounted, { 11 | view: () => [ 12 | m('h1', 'Mounted'), 13 | m('.row', { 14 | 'spx-component': 'mounted', 15 | 'spx-mounted:added': 'added' 16 | }, [ 17 | m('.col-6', [ 18 | m('button[type="button"]', { 19 | 'spx@click': 'mounted.onClick' 20 | }, 'Click Here') 21 | ]), 22 | m('.col-6', [ 23 | m('code[spx-bind="mounted.count"]', '') 24 | ]), 25 | m('.col-12.mt-3[spx-node="mounted.insert"]', '') 26 | ]) 27 | ] 28 | }); 29 | 30 | } 31 | 32 | spx.on('load', ({ key }) => { 33 | if (key.indexOf('/components/observer') > -1) { 34 | setTimeout(() => createComponent(), 2000); 35 | } 36 | }); 37 | 38 | spx.on('connect', ({ key }) => { 39 | console.log(key); 40 | if (key.indexOf('/components/observer') > -1) { 41 | setTimeout(() => createComponent(), 2000); 42 | } 43 | }); 44 | 45 | export class Mounted extends spx.Component({ 46 | state: { 47 | added: String, 48 | count: Number 49 | }, 50 | nodes: [ 'insert' ] 51 | }) { 52 | 53 | onmount () { 54 | 55 | this.insertNode.innerText = this.state.added; 56 | 57 | } 58 | 59 | onClick () { 60 | 61 | ++this.state.count; 62 | 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /test/cases/component-observer/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Component Observer' 3 | layout: cases/component-observer/index.liquid 4 | permalink: '/components/observer/index.html' 5 | --- 6 | 7 | # 🧪 Component Observation 8 | 9 | This case is used to test mutation observation for component creation and instance establishment, along with associated events and nodes being added after loading has completed. These cases are situations where (for example), a component or component associate is added to the DOM via a virtual framework. 10 | 11 | --- 12 | -------------------------------------------------------------------------------- /test/cases/component-refs/include/demo.liquid: -------------------------------------------------------------------------------- 1 | {%- assign refs = 'Foo,Bar,Baz' | split: ',' -%} 2 | 3 |
4 | {%- for ref in refs -%} 5 | 6 | {%- assign click = ref | prepend: 'on' | append: 'Press' -%} 7 | 8 |
11 |
14 |
15 | {{- ref -}} Component 16 | {{- id -}} 17 |
18 | 23 | 24 |
27 | 28 | 29 | 30 | 31 |
32 |
33 | {%- endfor -%} 34 |
-------------------------------------------------------------------------------- /test/cases/component-refs/include/pages.liquid: -------------------------------------------------------------------------------- 1 | {% 2 | liquid 3 | assign pages = 'Page Æ,Page A,Page B' | split: ',' 4 | %} 5 | 6 |
7 |
8 |
9 |
10 |
    11 | 12 | {% for link in pages %} 13 | 14 | {% 15 | liquid 16 | assign slug = link | slugify 17 | if link == 'Page Æ' 18 | assign url = '/components/refs/' 19 | else 20 | assign url = link | slugify | prepend: '/components/refs/' | append: '/' 21 | endif 22 | assign hasTarget = target | slice: 0, 1 23 | %} 24 | 25 |
  • 26 | 31 | {{ link }} 32 | 33 |
  • 34 | {% endfor %} 35 |
36 |
37 |
38 | 39 |
40 |

41 | Navigate between pages. The components will be indentical to test re-connect 42 |

43 |
-------------------------------------------------------------------------------- /test/cases/component-refs/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | --- 4 | 5 | {{ content }} 6 | 7 |
8 |
9 | {% include 'cases/component-refs/include/pages' %} 10 |
11 |
12 | {% include 'cases/component-refs/include/demo', id: 'First' %} 13 |
14 |
-------------------------------------------------------------------------------- /test/cases/component-refs/index.test.ts: -------------------------------------------------------------------------------- 1 | import spx, { SPX } from 'spx'; 2 | 3 | export class Refs1 extends spx.Component({ 4 | sugar: true, 5 | nodes: [ 6 | 'qux', 7 | 'xxx' 8 | ] 9 | }) { 10 | 11 | onQuxPress ({ attrs }: SPX.Event<{ age: number, dob: string }>) { 12 | this.xxxNode.innerText = 'pressed'; 13 | } 14 | 15 | } 16 | 17 | export class Refs2 extends spx.Component({ 18 | state: { 19 | fooClicks: Number, 20 | barClicks: Number, 21 | bazClicks: Number 22 | }, 23 | nodes: [ 24 | 'foo', 25 | 'bar', 26 | 'baz' 27 | ] 28 | }) { 29 | 30 | onmount () { 31 | 32 | // console.log({ ...this }); 33 | 34 | } 35 | 36 | onFooPress ({ attrs }: SPX.Event<{ age: number, dob: string }>) { 37 | 38 | this.fooNode.innerText = `name ${++this.state.fooClicks}`; 39 | console.log(this.fooNodes, this.fooNode.nodeName, this.fooNode.innerText); 40 | 41 | this.barNode.append(this.fooNode); 42 | 43 | } 44 | 45 | onBarPress ({ attrs }: SPX.Event<{ age: number, dob: string }>) { 46 | 47 | this.barNode.innerText = `${++this.state.barClicks}`; 48 | console.log(attrs); 49 | 50 | } 51 | 52 | onBazPress ({ attrs }: SPX.Event<{ age: number, dob: string }>) { 53 | 54 | this.bazNode.innerText = `${++this.state.bazClicks}`; 55 | console.log(attrs); 56 | 57 | } 58 | /* -------------------------------------------- */ 59 | /* NODES */ 60 | /* -------------------------------------------- */ 61 | 62 | } 63 | -------------------------------------------------------------------------------- /test/cases/component-refs/pages/page-a.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | permalink: '/components/refs/page-a/index.html' 4 | base: /components/refs/ 5 | --- 6 | 7 |

Component Refs

8 |

9 | Tests elements which contains multiple references. In some cases a component will use `spx-node` and `spx-event` annotations. Here we have a couple of components, each associated elements of these components are applying multiple references. 10 |

11 |
12 |
13 |
14 | {% include 'cases/component-refs/include/pages' %} 15 |
16 |
17 | {% include 'cases/component-refs/include/demo', id: 'Page A', color: 'fc-pink' %} 18 |
19 |
-------------------------------------------------------------------------------- /test/cases/component-refs/pages/page-b.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | permalink: '/components/refs/page-b/index.html' 4 | base: /components/refs/ 5 | --- 6 | 7 |

Component Refs

8 |

9 | Tests elements which contains multiple references. In some cases a component will use `spx-node` and `spx-event` annotations. Here we have a couple of components, each associated elements of these components are applying multiple references. 10 |

11 |
12 |
13 |
14 | {% include 'cases/component-refs/include/pages' %} 15 |
16 |
17 | {% include 'cases/component-refs/include/demo', id: 'Page B', color: 'fc-green' %} 18 |
19 |
-------------------------------------------------------------------------------- /test/cases/component-refs/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Component Refs' 3 | layout: cases/component-refs/index.liquid 4 | permalink: '/components/refs/index.html' 5 | --- 6 | 7 | # Component Refs 8 | 9 | Tests elements which contains multiple references. In some cases a component will use `spx-node` and `spx-event` annotations. Here we have a couple of components, each associated elements of these components are applying multiple references. 10 | 11 | --- 12 | -------------------------------------------------------------------------------- /test/cases/component-types/include/legend.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
TYPE
4 |
5 |
6 |
DEFINED
7 |
8 |
9 |
ORIGINAL
10 |
11 |
-------------------------------------------------------------------------------- /test/cases/component-types/include/pages.liquid: -------------------------------------------------------------------------------- 1 | {%- assign pages = 'Defaults,DOM Defined,Defined,Overwrite' | split: ',' -%} 2 | 3 |
4 |
5 |
6 |
9 |
    10 | 11 | {%- for link in pages -%} 12 | 13 | {%- 14 | liquid 15 | assign slug = link | slugify 16 | if link == 'Defaults' 17 | assign url = '/components/types/' 18 | else 19 | assign url = link | slugify | prepend: '/components/types/' | append: '/' 20 | endif 21 | -%} 22 | 23 |
  • 24 | 29 | {{ link }} 30 | 31 |
  • 32 | {%- endfor -%} 33 |
34 |
35 |
36 |
37 |
-------------------------------------------------------------------------------- /test/cases/component-types/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | permalink: '/components/types/index.html' 4 | base: /components/types/ 5 | defaults: 6 | - name: String 7 | bind: defaultStringValue 8 | - name: Boolean 9 | bind: defaultBooleanValue 10 | - name: Number 11 | bind: defaultNumberValue 12 | - name: Object 13 | bind: defaultObjectValue 14 | - name: Array 15 | bind: defaultArrayValue 16 | --- 17 | 18 |
19 |
20 |

21 | 🧪 Default States 22 |

23 |

24 | The components state is using TypeConstructor and the component DOM does not assert any state directives. The outcome will be the SPX default state values. 25 |

26 |
27 |
28 |
29 | {%- include 'cases/component-types/include/pages' -%} 30 | {%- include 'cases/component-types/include/legend' -%} 31 |
32 |
33 |
36 | {%- for type in defaults -%} 37 |
38 | {{ type.name }} 39 | 42 | 45 |
46 | {%- endfor -%} 47 |
48 |
49 |
-------------------------------------------------------------------------------- /test/cases/component-types/pages/defined.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | permalink: '/components/types/defined/index.html' 4 | base: /components/defined/ 5 | defined: 6 | - name: String 7 | bind: definedStringValue 8 | original: definedOriginalStringValue 9 | - name: Boolean 10 | bind: definedBooleanValue 11 | original: definedOriginalBooleanValue 12 | - name: Number 13 | bind: definedNumberValue 14 | original: definedOriginalNumberValue 15 | - name: Object 16 | bind: definedObjectValue 17 | original: definedOriginalNumberValue 18 | - name: Array 19 | bind: definedArrayValue 20 | original: definedOriginalArrayValue 21 | --- 22 | 23 |
24 |
25 |

26 | 🧪 Component Defined States 27 |

28 |

29 | The state are define within the component, and undefined within the DOM. The outcome will be the component state values we've define within the component. 30 |

31 |
32 |
33 |
34 | {%- include 'cases/component-types/include/pages' -%} 35 | {%- include 'cases/component-types/include/legend' -%} 36 |
37 |
38 |
41 | {%- for type in defined -%} 42 |
43 | {{ type.name }} 44 | 47 | 50 |
51 | {%- endfor -%} 52 |
53 |
54 |
-------------------------------------------------------------------------------- /test/cases/component-types/pages/dom-defined.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | permalink: '/components/types/dom-defined/index.html' 4 | base: /components/dom-defined/ 5 | dom_defined: 6 | - name: String 7 | bind: domDefinedStringValue 8 | original: defaultStringValue 9 | - name: Boolean 10 | bind: domDefinedBooleanValue 11 | original: defaultBooleanValue 12 | - name: Number 13 | bind: domDefinedNumberValue 14 | original: defaultNumberValue 15 | - name: Object 16 | bind: domDefinedObjectValue 17 | original: defaultObjectValue 18 | - name: Array 19 | bind: domDefinedArrayValue 20 | original: defaultArrayValue 21 | --- 22 | 23 |
24 |
25 |

26 | 🧪 DOM Defined States 27 |

28 |

29 | The component DOM asserts state values, whereas the component state referenced are using TypeConstructors. The outcome will be the DOM state values. 30 |

31 |
32 |
33 |
34 | {%- include 'cases/component-types/include/pages' -%} 35 | {%- include 'cases/component-types/include/legend' -%} 36 |
37 |
38 |
46 | {%- for type in dom_defined -%} 47 |
48 | {{ type.name }} 49 | 52 | 55 |
56 | {%- endfor -%} 57 |
58 |
59 |
-------------------------------------------------------------------------------- /test/cases/config-fragments/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Options - Fragments' 3 | layout: views/base.liquid 4 | group: 'directive' 5 | permalink: '/options/fragments/index.html' 6 | --- 7 | 8 |
9 |
10 |

11 | Fragments 12 |

13 |

14 | Testing fragment defintions applied upon connection settings. 15 |

16 |
17 |
18 |
19 | Valid 20 |
21 |
22 |
23 |
24 | Error 25 |
26 |
27 |
28 | 29 |
30 |
31 | {% iframe '/options/fragments/valid' %} 32 |
33 |
34 | {% iframe '/options/fragments/error' %} 35 |
36 |
-------------------------------------------------------------------------------- /test/cases/config-fragments/tests/error.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Options - Fragments Error' 3 | layout: views/base.liquid 4 | iframe: true 5 | script: '/fragments-error.js' 6 | permalink: '/options/fragments/error/index.html' 7 | --- 8 | 9 |
10 |
11 |
Using Class
12 |
13 |
16 |
Using Hash
17 |
18 |
21 |
Is Valid
22 |
23 |
26 |
Element D
27 |
28 |
-------------------------------------------------------------------------------- /test/cases/config-fragments/tests/error.test.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | export class Log extends spx.Component({ 4 | nodes: [ 5 | 'console' 6 | ] 7 | }) { 8 | 9 | connect () { 10 | 11 | } 12 | 13 | } 14 | 15 | spx({ 16 | logLevel: 1, 17 | components: { Log }, 18 | fragments: [ 19 | '.using-class', // Fails 20 | '#using-hash', // Fails 21 | 'is-valid', // Passes 22 | '[data-attr]' // Fails 23 | ] 24 | }); 25 | -------------------------------------------------------------------------------- /test/cases/config-fragments/tests/valid.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Options - Fragments Valid' 3 | layout: views/base.liquid 4 | iframe: true 5 | script: '/fragments-valid.js' 6 | permalink: '/options/fragments/valid/index.html' 7 | --- 8 | 9 |
10 |
13 |
Element A
14 |
15 |
18 |
Element B
19 |
20 |
23 |
Element C
24 |
25 |
28 |
Element D
29 |
30 |
-------------------------------------------------------------------------------- /test/cases/config-fragments/tests/valid.test.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | spx( 4 | { 5 | logLevel: 1, 6 | fragments: [ 7 | '#element-a', 8 | '#element-b', 9 | '#element-c', 10 | '#element-d' 11 | ] 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /test/cases/directive-target/include/pages.liquid: -------------------------------------------------------------------------------- 1 | {% 2 | liquid 3 | assign pages = 'Page A,Page B,Page C' | split: ',' 4 | %} 5 | 6 |
7 |
8 |
9 |
12 |
    13 | 14 | {% for link in pages %} 15 | 16 | {% 17 | liquid 18 | assign slug = link | slugify 19 | assign url = link | slugify | prepend: '/directives/spx-target/' | append: '/' 20 | assign hasTarget = target | slice: 0, 1 21 | %} 22 | 23 |
  • 24 | 30 | {{ link }} 31 | 32 |
  • 33 | {% endfor %} 34 |
35 |
36 |
37 |
38 |
-------------------------------------------------------------------------------- /test/cases/directive-target/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | --- 4 | 5 |

SPX Targets

6 | 7 | {% include 'cases/directive-target/include/pages' %} 8 | 9 |
10 |
11 |
14 |
15 |
16 | Page A (Before) 17 |
18 |
19 | The target color will change from gray to cyan 20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 |
30 |
31 |
32 | Page B (Before) 33 |
34 |
35 | The target color will change from gray to green. 36 |
37 |
38 |
39 |
40 |
41 | 42 |
43 |
46 |
47 |
48 | Page C (Before) 49 |
50 |
51 | The target color will change from gray to red. 52 |
53 |
54 |
55 |
56 |
57 |
58 | 59 |
60 |
61 |
62 | {{ content }} 63 |
64 |
65 |
-------------------------------------------------------------------------------- /test/cases/directive-target/pages/page-a.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Basic' 3 | layout: views/base.liquid 4 | permalink: '/directives/spx-target/page-a/index.html' 5 | --- 6 | 7 |

SPX Targets - Page A

8 | 9 | {% include 'cases/directive-target/include/pages' %} 10 | 11 |
12 |
13 |
16 |
17 |
18 | Page A (Before) 19 |
20 |
21 | The target color has changed to cyan 22 |
23 |
26 |
27 |
28 |
29 | 30 |
31 |
34 |
35 |
36 | Page B (Before) 37 |
38 |
39 | The target color will change from gray to green. 40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 |
50 |
51 |
52 | Page C (Before) 53 |
54 |
55 | The target color will change from gray to red. 56 |
57 |
58 |
59 |
60 |
61 |
-------------------------------------------------------------------------------- /test/cases/directive-target/pages/page-b.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Basic' 3 | layout: views/base.liquid 4 | permalink: '/directives/spx-target/page-b/index.html' 5 | --- 6 | 7 |

SPX Targets - Page B

8 | 9 | {%- include 'cases/directive-target/include/pages' -%} 10 | 11 |
12 |
13 |
16 |
17 |
18 | Page A (Before) 19 |
20 |
21 | The target color will change from gray to cyan 22 |
23 |
24 |
25 |
26 |
27 | 28 |
29 |
32 |
33 |
34 | Page B (After) 35 |
36 |
37 | The target color has now changed green. 38 |
39 |
42 |
43 |
44 |
45 | 46 |
47 |
50 |
51 |
52 | Page C (Before) 53 |
54 |
55 | The target color will change from gray to red. 56 |
57 |
58 |
59 |
60 |
61 |
-------------------------------------------------------------------------------- /test/cases/directive-target/pages/page-c.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Basic' 3 | layout: views/base.liquid 4 | permalink: '/directives/spx-target/page-c/index.html' 5 | --- 6 | 7 |

SPX Targets - Page C

8 | 9 | {% include 'cases/directive-target/include/pages' %} 10 | 11 |
12 |
13 |
16 |
17 |
18 | Page A (Before) 19 |
20 |
21 | The target color will change from gray to cyan 22 |
23 |
24 |
25 |
26 |
27 | 28 |
29 |
32 |
33 |
34 | Page B (Before) 35 |
36 |
37 | The target color will change from gray to green. 38 |
39 |
40 |
41 |
42 |
43 | 44 |
45 |
48 |
49 |
50 | Page C (After) 51 |
52 |
53 | The target color has now changed to red. 54 |
55 |
58 |
59 |
60 |
61 |
-------------------------------------------------------------------------------- /test/cases/directive-target/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Directive - spx-target' 3 | layout: cases/directive-target/index.liquid 4 | permalink: '/directives/spx-target/index.html' 5 | --- 6 | 7 | # spx-target 8 | 9 | This tests the `spx-target` directive. This block of text will be present during an SPX session when the starting page URL equals `directives/spx-target` and it will persist when navigating to **Page A**, **Page B** and **Page C**. If the SPX session does not begin at page pathname `directives/spx-target` then this text block will not be present as we are only targeting certain elements using the attribute. 10 | -------------------------------------------------------------------------------- /test/cases/lifecycle-events/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | --- 4 | 5 | {%- assign events = 'visit,prefetch,fetch,cache,render,load' | split: ',' -%} 6 | 7 |
8 |
9 | {{ content }} 10 |
11 |
12 |
13 |
22 |
23 | {%- for item in events -%} 24 |
25 |
26 |
27 | {{ item }} 28 |
29 | 0 32 |
33 |
34 | {%- endfor -%} 35 | 36 |
37 | 38 |
39 |
42 |
43 |
44 |
-------------------------------------------------------------------------------- /test/cases/lifecycle-events/index.test.ts: -------------------------------------------------------------------------------- 1 | import spx from 'spx'; 2 | 3 | export class Events extends spx.Component({ 4 | nodes: [ 'logger', 'someButton' ], 5 | state: { 6 | connect: 1, 7 | visit: Number, 8 | prefetch: Number, 9 | fetch: Number, 10 | cache: Number, 11 | render: Number, 12 | load: Number, 13 | count: Number, 14 | store: Array 15 | } 16 | }) { 17 | 18 | connect () { 19 | 20 | spx.on('visit', (event) => { 21 | ++this.state.visit; 22 | console.log(event); 23 | this.insert('visit', 'green'); 24 | }); 25 | spx.on('fetch', (page) => { 26 | ++this.state.fetch; 27 | this.insert(`fetch ${page.key}`, 'blue'); 28 | }); 29 | spx.on('prefetch', (event: any) => { 30 | ++this.state.prefetch; 31 | this.insert(`prefetch ${event.getAttribute('href')}`, 'pink'); 32 | }); 33 | spx.on('cache', (event) => { 34 | ++this.state.cache; 35 | this.insert(`cache ${event.querySelector('title').outerHTML.replace(/\s+/g, ' ')}`, 'yellow'); 36 | }); 37 | spx.on('render', (curDom, newDom) => { 38 | ++this.state.render; 39 | this.insert(`render ${spx.$.page.key}`, 'gray'); 40 | }); 41 | spx.on('load', (event) => { 42 | ++this.state.load; 43 | this.insert('load', 'red'); 44 | }); 45 | 46 | } 47 | 48 | unmount () { 49 | 50 | this.insert('------------------------------------------', 'white'); 51 | } 52 | 53 | onmount () { 54 | 55 | this.loggerNode.append(...this.state.store); 56 | } 57 | 58 | insert (message: string, color: string) { 59 | 60 | const element = document.createElement('div'); 61 | element.className = `d-block pb-1 message fc-${color}`; 62 | element.ariaLabel = `${++this.state.count}`; 63 | element.innerText = message; 64 | 65 | if (this.loggerExists) { 66 | this.loggerNode.appendChild(element); 67 | this.loggerNode.scrollTop = this.loggerNode.scrollHeight; 68 | } else { 69 | this.state.store.push(element); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /test/cases/lifecycle-events/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Lifecycle Events' 3 | layout: cases/lifecycle-events/index.liquid 4 | permalink: '/lifecycle-events/index.html' 5 | --- 6 | 7 | # 🧪 Lifecycle Events 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /test/cases/morph-nodes/include/pages.liquid: -------------------------------------------------------------------------------- 1 | {% 2 | liquid 3 | assign pages = 'Page A,Page B,Page C' | split: ',' 4 | %} 5 | 6 |
7 |
8 |
9 |
12 |
    13 | 14 | {% for link in pages %} 15 | 16 | {% 17 | liquid 18 | assign slug = link | slugify 19 | assign url = link | slugify | prepend: '/morph/nodes/' | append: '/' 20 | assign hasTarget = target | slice: 0, 1 21 | %} 22 | 23 |
  • 24 | 29 | {{ link }} 30 | 31 |
  • 32 | {% endfor %} 33 |
34 |
35 |
36 |
37 |
-------------------------------------------------------------------------------- /test/cases/morph-nodes/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: views/base.liquid 3 | --- 4 | 5 |

Moprh Nodes

6 | 7 | {%- include 'cases/morph-nodes/include/pages' -%} 8 | 9 |
10 | 11 |
12 |
15 |
16 |
17 |
18 | Persist the 19 | data-spx 20 |
21 |
24 | Zero 25 |
26 |
27 |
28 |
29 | 30 |
33 |
34 |
35 |
36 | Changes the 37 | data-spx 38 |
39 |
42 | Zero 43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 |
53 |
54 |
55 | {{- content -}} 56 |
57 |
58 |
-------------------------------------------------------------------------------- /test/cases/morph-nodes/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | 3 | import spx, { SPX } from 'spx'; 4 | 5 | export class MorphNodes extends spx.Component({ 6 | nodes: [ 7 | 'alpha', 8 | 'omega' 9 | ] 10 | }) { 11 | 12 | onmount (page: SPX.Page) { 13 | 14 | if (page.key.indexOf('/page-c') > -1) { 15 | 16 | this.alphaNode.style.backgroundColor = 'blue'; 17 | 18 | } else if (page.key.indexOf('/page-b') > -1) { 19 | 20 | this.alphaNode.style.backgroundColor = 'red'; 21 | 22 | } else if (page.key.indexOf('/page-a') > -1) { 23 | 24 | this.alphaNode.style.backgroundColor = 'green'; 25 | 26 | } 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /test/cases/morph-nodes/pages/page-a.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Basic' 3 | layout: views/base.liquid 4 | permalink: '/morph/nodes/page-a/index.html' 5 | --- 6 |

Moprh Nodes - Page A

7 | 8 | {% include 'cases/morph-nodes/include/pages' %} 9 | 10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 | Persist the 18 | data-spx 19 |
20 |
23 | First 24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 | Changes the 34 | data-spx 35 |
36 |
39 | First 40 |
41 |
42 |
43 |
44 |
45 |
46 |
49 |
50 |
51 | {{ content }} 52 |
53 |
54 |
-------------------------------------------------------------------------------- /test/cases/morph-nodes/pages/page-b.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Basic' 3 | layout: views/base.liquid 4 | permalink: '/morph/nodes/page-b/index.html' 5 | --- 6 |

Moprh Nodes - Page B

7 | 8 | {% include 'cases/morph-nodes/include/pages' %} 9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 | Persist the 17 | data-spx 18 |
19 |
22 | Second 23 |
24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 | Changes the 33 | data-spx 34 |
35 |
38 | Second 39 |
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 | {{ content }} 51 |
52 |
53 |
-------------------------------------------------------------------------------- /test/cases/morph-nodes/pages/page-c.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Basic' 3 | layout: views/base.liquid 4 | permalink: '/morph/nodes/page-c/index.html' 5 | --- 6 |

Moprh Nodes

7 | 8 | {% include 'cases/morph-nodes/include/pages' %} 9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 | Persist the 17 | data-spx 18 |
19 |
22 | Third 23 |
24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 | Changes the 33 | data-spx 34 |
35 |
38 | Third 39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 |
49 |
50 |
51 | {{ content }} 52 |
53 |
54 |
-------------------------------------------------------------------------------- /test/cases/morph-nodes/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Morph - Nodes' 3 | layout: cases/morph-nodes/index.liquid 4 | permalink: '/morph/nodes/index.html' 5 | --- 6 | 7 | # Morph Nodes 8 | 9 | This tests the `data-spx` nodes reference value attribute applied to elements. The nodes reference uses a `n.` value. When morphing, the current node will be morphed with a new node, but the current node will have the `data-spx` value added dynamically. 10 | 11 | The snapshot will be marked in post cycles, which means that a component node marked with `data-spx` will attempt to morph with identical nodes in the snapshot. Before performing an attribute morph operation, the `data-spx` is removed and the current node is matched with the new node to determine whether or not attributes match and the node contents are equal. 12 | 13 | In this test we setup a component and mark elements as nodes. When moving between pages elements which are not equal will have their node reference changed, whereas equal elements will persist their node reference. 14 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spx/test", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "type": "module", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "rm -rf public && concurrently \"pnpm run e2e\" \"pnpm run tsup\" \"pnpm run sass\"", 10 | "tsup": "tsup --watch", 11 | "e2e": "eleventy --config=.eleventy.cjs --serve --watch --quiet", 12 | "sass": "sass ./asset/style.scss public/style.css --poll --watch --no-source-map" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "tsup": { 18 | "entry": { 19 | "bundle": "./asset/suite.ts" 20 | }, 21 | "outDir": "./public", 22 | "clean": false, 23 | "treeshake": false, 24 | "minify": false, 25 | "minifyIdentifiers": false, 26 | "minifySyntax": false, 27 | "minifyWhitespace": false, 28 | "splitting": false, 29 | "platform": "browser", 30 | "format": [ 31 | "iife" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@11ty/eleventy": "^2.0.1", 36 | "@brixtol/bootstrap": "^1.7.5", 37 | "@types/mergerino": "^0.4.5", 38 | "@types/node": "^22.6.1", 39 | "concurrently": "^9.0.1", 40 | "e11ty": "^0.1.1", 41 | "mergerino": "^0.4.0", 42 | "sass": "^1.79.3", 43 | "tsup": "^8.3.0" 44 | }, 45 | "dependencies": { 46 | "esthetic": "0.6.4-beta.1", 47 | "mithril": "^2.2.2", 48 | "papyrus": "^0.6.8", 49 | "relapse": "^0.9.1", 50 | "spx": "link:.." 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig( 4 | { 5 | entry: { 6 | bundle: './asset/suite.ts' 7 | }, 8 | outDir: './public', 9 | outExtension: () => ({ js: '.js' }), 10 | clean: false, 11 | treeshake: false, 12 | bundle: true, 13 | minify: false, 14 | minifyIdentifiers: false, 15 | minifySyntax: false, 16 | minifyWhitespace: false, 17 | splitting: false, 18 | platform: 'browser', 19 | format: [ 20 | 'iife' 21 | ] 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /test/views/base.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | 13 | 16 | 19 | 22 | 26 | 29 | 30 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | {%- if iframe -%} 44 | 45 | {{ content }} 46 | 47 | {%- else -%} 48 | 49 | 53 | 56 | 59 |
60 |
61 | {%- include 'views/include/cases' -%} 62 |
65 | {{ content }} 66 |
67 |
68 |
69 |
70 | 71 | {%- include 'views/include/explorer' -%} 72 |
73 |
74 | 75 | {%- endif -%} 76 | {%- sprite -%} 77 | 78 | 79 | -------------------------------------------------------------------------------- /test/views/data/cases.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [ 3 | { 4 | "title": "Testing", 5 | "links": [ 6 | { 7 | "title": "Overview", 8 | "url": "/" 9 | } 10 | ] 11 | }, 12 | { 13 | "title": "API", 14 | "links": [ 15 | { 16 | "title": "Lifecycle Events", 17 | "url": "/lifecycle-events/" 18 | } 19 | ] 20 | }, 21 | { 22 | "title": "Components", 23 | "links": [ 24 | { 25 | "title": "Lifecycle Hooks", 26 | "url": "/components/lifecycle/" 27 | }, 28 | { 29 | "title": "State Types", 30 | "url": "/components/types/" 31 | }, 32 | { 33 | "title": "Incremental", 34 | "url": "/components/incremental/" 35 | }, 36 | { 37 | "title": "Nesting", 38 | "url": "/components/nesting/" 39 | }, 40 | { 41 | "title": "Observer", 42 | "url": "/components/observer/" 43 | }, 44 | { 45 | "title": "Literal", 46 | "url": "/components/literal/" 47 | }, 48 | { 49 | "title": "Merge", 50 | "url": "/components/merge/" 51 | }, 52 | { 53 | "title": "Refs", 54 | "url": "/components/refs/" 55 | }, 56 | { 57 | "title": "Alias", 58 | "url": "/components/alias/" 59 | }, 60 | 61 | { 62 | "title": "Async", 63 | "url": "/components/async/" 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /test/views/include/cases.liquid: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/views/include/explorer.liquid: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 10 | 14 |
15 |
16 |
19 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /test/views/include/logger.liquid: -------------------------------------------------------------------------------- 1 | 5 | 8 | 9 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "node_modules/**", 4 | "tsplugin/**" 5 | ], 6 | "include": [ 7 | "types/*.ts", 8 | "src/**/*.ts", 9 | "docs/src/app/**/*.ts", 10 | "test/**/*.ts", 11 | ], 12 | "typeAcquisition": { 13 | "enable": true, 14 | }, 15 | "compilerOptions": { 16 | "allowSyntheticDefaultImports": true, 17 | "esModuleInterop": true, 18 | "declaration": true, 19 | "removeComments": true, 20 | "skipDefaultLibCheck": true, 21 | "target": "ES2018", 22 | "lib": [ 23 | "DOM" 24 | ], 25 | "typeRoots": [ 26 | "node_modules/@types" 27 | ], 28 | "module": "ESNext", 29 | "moduleResolution": "node", 30 | "emitDeclarationOnly": true, 31 | "isolatedModules": true, 32 | "baseUrl": ".", 33 | "paths": { 34 | "types": [ 35 | "types/types.d.ts", 36 | ] 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | import { utimes } from 'fs/promises'; 3 | import { version } from './package.json'; 4 | const PROD = process.env.ENV === 'PROD'; 5 | const BANNER = [ 6 | '/**', 7 | '* SPX ~ Single Page XHR | https://spx.js.org', 8 | '*', 9 | '* @license CC BY-NC-ND 4.0', 10 | `* @version ${version}`, 11 | '* @copyright 2024 Nikolas Savvidis', 12 | '*/' 13 | ]; 14 | 15 | export default defineConfig({ 16 | entry: [ 17 | './src/index.ts' 18 | ], 19 | format: [ 'esm' ], 20 | clean: false, 21 | outDir: './', 22 | minify: PROD, 23 | outExtension: () => ({ js: '.js' }), 24 | platform: 'browser', 25 | keepNames: false, // Possible breaks may need to use true in v1 26 | splitting: false, 27 | target: 'es2017', 28 | globalName: 'spx', 29 | treeshake: 'smallest', 30 | banner: () => ({ js: BANNER.join('\n') }), 31 | esbuildOptions (options) { 32 | if (PROD) { 33 | options.mangleQuoted = false; 34 | options.mangleProps = /^\$[a-z]/; 35 | } 36 | }, 37 | async onSuccess () { 38 | if (!PROD) { 39 | 40 | const time = new Date(); 41 | 42 | await Promise.all([ 43 | utimes('./docs/src/app/bundle.ts', time, time), 44 | utimes('./docs/src/app/iframe.ts', time, time), 45 | utimes('./test/asset/suite.ts', time, time), 46 | utimes('./test/views/base.liquid', time, time) 47 | ]); 48 | 49 | } 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /types/context.d.ts: -------------------------------------------------------------------------------- 1 | import type { Scope } from './components'; 2 | 3 | export interface Context { 4 | /** 5 | * Alias Maps 6 | */ 7 | $aliases: { [alias: string]: string; }; 8 | /** 9 | * Component Scopes 10 | */ 11 | $scopes: { [component: string]: Scope[]; }; 12 | /** 13 | * When we are applying incremental context generation (i.e: during morphs) 14 | * this value will be `true`, otherwise `false`. 15 | * 16 | * @default false 17 | */ 18 | $morph: HTMLElement; 19 | /** 20 | * Holds a reference to the snapshot, used for creating data-spx="" references 21 | * 22 | * @default false 23 | */ 24 | $snapshot: HTMLElement; 25 | /** 26 | * Holds a reference to the last known element identifier 27 | * 28 | * @default null 29 | */ 30 | $element: string; 31 | /** 32 | * Holds a reference to the last known element identifier 33 | * 34 | * @default null 35 | */ 36 | $snaps: string; 37 | } 38 | 39 | export interface CTX { 40 | /** 41 | * This holds `id=""` values 42 | */ 43 | marks: Set, 44 | /** 45 | * This reference consists of a selector and `data-spx` reference value. 46 | * The Map **key** is a selector whereas the `string[]` **value** represents 47 | * each reference to be assigned. 48 | */ 49 | refs?: Map; 50 | /** 51 | * Records describing context states of components 52 | */ 53 | snaps: Array<[ element: HTMLElement, refs: Map]>; 54 | /** 55 | * Component state contexts, consisting of a Context store 56 | */ 57 | store: Context; 58 | } 59 | -------------------------------------------------------------------------------- /types/http.d.ts: -------------------------------------------------------------------------------- 1 | import { LiteralUnion } from 'types'; 2 | 3 | export interface HTTPConfig { 4 | /** 5 | * The URL for the request, constructed with path and optional base origin. 6 | * 7 | * > This is optional, you can pass url as the first argument of `spx.http('/url', {})` 8 | * @type {URL} 9 | */ 10 | url?: URL; 11 | /** 12 | * The HTTP method for the request (e.g., 'GET', 'POST', 'PUT', 'DELETE'). 13 | * 14 | * @default 'GET' 15 | */ 16 | method?: LiteralUnion< 'HEAD' | 'GET' | 'PUT' | 'POST' | 'DELETE' | 'PATCH', string>; 17 | /** 18 | * The expected response type (e.g., '', 'arraybuffer', 'blob', 'document', 'json', 'text'). 19 | * 20 | * @default 21 | * 'json' 22 | */ 23 | response?: XMLHttpRequestResponseType; 24 | /** 25 | * Query parameters to append to the URL. 26 | * 27 | * @default 28 | * {} 29 | */ 30 | query?: Record | URLSearchParams; 31 | /** 32 | * The request body, typically for POST/PUT requests (e.g., string, FormData, Blob). 33 | * 34 | * @default 35 | * undefined 36 | */ 37 | body?: any; 38 | /** 39 | * Custom headers for the request. 40 | * 41 | * @default {} 42 | */ 43 | headers?: Record; 44 | /** 45 | * Timeout duration for the request in milliseconds. 46 | * 47 | * @default 0 48 | */ 49 | timeout?: number; 50 | /** 51 | * A username for HTTP authorization. 52 | * 53 | * @default undefined 54 | */ 55 | user?: string; 56 | /** 57 | * A password for HTTP authorization. This option is provided for XMLHttpRequest compatibility, 58 | * but you should avoid using it because it sends the password in plain text over the network. 59 | * 60 | * @default undefined 61 | */ 62 | pass?: string; 63 | /** 64 | * Exposes the underlying XMLHttpRequest object for low-level configuration and optional 65 | * replacement (by returning a new XHR). 66 | */ 67 | config?: (xhr: XMLHttpRequest) => void | XMLHttpRequest; 68 | } 69 | -------------------------------------------------------------------------------- /types/types.d.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './session'; 3 | export * from './events'; 4 | export * from './options'; 5 | export * from './config'; 6 | export * from './page'; 7 | export * from './context'; 8 | export { SPX } from './global'; 9 | export { Merge, LiteralUnion, ValueOf } from 'type-fest'; 10 | export type Identity = T; 11 | export type Match = [T[keyof T]] extends [U[keyof U]] ? [U[keyof U]] extends [T[keyof T]] ? true : false : false; 12 | export type Attrs = { [key: string]: unknown; } 13 | --------------------------------------------------------------------------------