├── .eslintrc.cjs
├── .github
└── workflows
│ ├── node.js.yml
│ └── npm-publish.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE.md
├── README.md
├── images
└── logo.svg
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── common.js
├── dom.js
├── knobs.js
├── server
│ ├── dom.js
│ ├── element.js
│ └── wafer.js
├── types.js
├── wafer-mixin.js
└── wafer.js
├── test
├── client
│ ├── changed-updated.test.js
│ ├── events.test.js
│ ├── helpers.test.js
│ ├── instantiation
│ │ ├── defined-after-created-with-createelement.test.js
│ │ ├── defined-after-created-with-innerhtml.test.js
│ │ ├── defined-after-created-with-new.test.js
│ │ ├── defined-after-dom-all-types.test.js
│ │ ├── defined-before-created-with-createelement.test.js
│ │ ├── defined-before-created-with-innerhtml.test.js
│ │ ├── defined-before-created-with-new.test.js
│ │ ├── defined-before-dom-all-types.test.js
│ │ └── general.test.js
│ └── rendering.test.js
├── configs.js
├── server
│ ├── changed-updated.test.js
│ ├── element.test.js
│ ├── helpers.test.js
│ ├── instantiation
│ │ ├── as-object.js
│ │ ├── as-string.js
│ │ └── general.test.js
│ └── rendering.test.js
└── testing.js
├── tsconfig.json
├── typedoc-theme
└── partials
│ └── header.hbs
└── web-test-runner.config.js
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | },
7 | extends: ["eslint:recommended", "prettier"],
8 | parserOptions: {
9 | ecmaVersion: 12,
10 | sourceType: "module",
11 | },
12 | rules: {},
13 | };
14 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Tests
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [12.x, 14.x, 16.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v2
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: 'npm'
29 | - run: npm ci
30 | - run: npm run lint
31 | - run: npm run tsc
32 | - run: npm run test
33 | - run: npm run test:server
34 | - run: npm run dist
35 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | publish-npm:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v2
16 | with:
17 | node-version: 14
18 | registry-url: https://registry.npmjs.org/
19 | - run: npm ci
20 | - run: npm run build
21 | - run: npm publish
22 | env:
23 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode/
3 | node_modules/
4 | coverage/
5 | coverage-server/
6 | dist/
7 | lib/
8 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Chris Haynes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Wafer
6 |
7 | [](https://github.com/lamplightdev/wafer/actions/workflows/node.js.yml)
8 | [](https://www.npmjs.com/package/@lamplightdev/wafer)
9 | [](https://bundlephobia.com/package/@lamplightdev/wafer)
10 |
11 | Welcome to Wafer: a simple and lightweight base library for building Web Components that can be used on the browser, server or both.
12 |
13 | Wafer is:
14 |
15 | - **Small** 🪶
16 | <2kb (minified and compressed)
17 |
18 | - **Fast** ⚡️
19 | Template updates are declared using CSS selectors leveraging native browser performance
20 |
21 | - **Efficient** 🔋
22 | Updates are batched preventing any unnecessary renders
23 |
24 | - **Flexible** 💪🏾
25 | Import directly, drop in a `
62 |
67 | ```
68 |
69 | On the server Wafer is available as both an ES and a CJS module:
70 |
71 | ```js
72 | // ES module
73 | import WaferServer from "@lamplightdev/wafer/server/wafer.js";
74 | // or CJS
75 | const WaferServer = require("@lamplightdev/wafer/server/wafer");
76 |
77 | class MyExample extends WaferServer {}
78 | ```
79 |
80 | ## Building
81 |
82 | The source files are located in the `/src` folder, and can be built with:
83 |
84 | ```
85 | npm run build
86 | ```
87 |
88 | This will lint the code, create Typescript types from JSDoc comments, run tests and finally build the package files. To run these steps individually please see the `scripts` entries in `package.json`.
89 |
90 | ## Contributing
91 |
92 | All contributions are welcome - please file an issue or submit a PR.
93 |
94 | ## License
95 |
96 | The code is released under the [MIT license](./LICENSE.md).
97 |
98 | ## Credits
99 |
100 | Wafer was created by [Chris](https://lamplightdev.com/) ([@lamplightdev](https://twitter.com/lamplightdev)) - say hi!
101 |
--------------------------------------------------------------------------------
/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@lamplightdev/wafer",
3 | "version": "1.0.14",
4 | "description": "Web Component Base Class",
5 | "keywords": [
6 | "webcomponents",
7 | "web-components",
8 | "web components"
9 | ],
10 | "type": "module",
11 | "main": "./lib/wafer.cjs",
12 | "module": "./lib/wafer.js",
13 | "unpkg": "./dist/wafer.js",
14 | "exports": {
15 | ".": {
16 | "import": "./lib/wafer.js",
17 | "default": "./lib/wafer.cjs"
18 | },
19 | "./dom": {
20 | "import": "./lib/dom.js",
21 | "default": "./lib/dom.cjs"
22 | },
23 | "./dom.js": {
24 | "import": "./lib/dom.js",
25 | "default": "./lib/dom.cjs"
26 | },
27 | "./server": {
28 | "import": "./lib/server/wafer.js",
29 | "default": "./lib/server/wafer.cjs"
30 | },
31 | "./lib/server/wafer": {
32 | "import": "./lib/server/wafer.js",
33 | "default": "./lib/server/wafer.cjs"
34 | },
35 | "./lib/server/wafer.js": {
36 | "import": "./lib/server/wafer.js",
37 | "default": "./lib/server/wafer.cjs"
38 | },
39 | "./server/dom": {
40 | "import": "./lib/server/dom.js",
41 | "default": "./lib/server/dom.cjs"
42 | },
43 | "./lib/server/dom": {
44 | "import": "./lib/server/dom.js",
45 | "default": "./lib/server/dom.cjs"
46 | },
47 | "./lib/server/dom.js": {
48 | "import": "./lib/server/dom.js",
49 | "default": "./lib/server/dom.cjs"
50 | },
51 | "./server/element": {
52 | "import": "./lib/server/element.js",
53 | "default": "./lib/server/element.cjs"
54 | },
55 | "./lib/server/element": {
56 | "import": "./lib/server/element.js",
57 | "default": "./lib/server/element.cjs"
58 | },
59 | "./lib/server/element.js": {
60 | "import": "./lib/server/element.js",
61 | "default": "./lib/server/element.cjs"
62 | }
63 | },
64 | "types": "./lib/wafer.d.ts",
65 | "sideEffects": false,
66 | "files": [
67 | "dist/**",
68 | "lib/**"
69 | ],
70 | "scripts": {
71 | "test": "wtr test/client --recursive --node-resolve",
72 | "test:watch": "wtr test/client --recursive --node-resolve --watch",
73 | "test:coverage": "wtr test/client --recursive --exclude test/configs.js --node-resolve --coverage",
74 | "test:server": "mocha test/server --recursive --exclude test/configs.js",
75 | "test:server:coverage": "c8 --reporter=html --reports-dir=coverage-server/ mocha test/server --recursive --exclude test/configs.js",
76 | "dist": "rollup -c",
77 | "tsc": "tsc",
78 | "typedoc": "typedoc --theme ./typedoc-theme --listInvalidSymbolLinks --logLevel Verbose",
79 | "lint": "eslint \"src/*.js\" \"src/server/*.js\"",
80 | "format": "npm run format:eslint && npm run format:prettier",
81 | "format:eslint": "eslint \"src/*.js\" \"src/server/*.js\" --fix",
82 | "format:prettier": "prettier \"src/*.js\" \"src/server/*.js\" --write",
83 | "build": "npm run lint && npm run tsc && npm run test && npm run test:server && npm run dist"
84 | },
85 | "repository": {
86 | "type": "git",
87 | "url": "git+https://github.com/lamplightdev/wafer.git"
88 | },
89 | "author": "",
90 | "license": "MIT",
91 | "bugs": {
92 | "url": "https://github.com/lamplightdev/wafer/issues"
93 | },
94 | "homepage": "https://github.com/lamplightdev/wafer#readme",
95 | "devDependencies": {
96 | "@open-wc/testing": "^2.5.33",
97 | "@web/test-runner": "^0.13.15",
98 | "c8": "^7.7.3",
99 | "chai": "^4.3.4",
100 | "chai-dom": "^1.9.0",
101 | "chai-html": "^2.0.1",
102 | "eslint": "^7.31.0",
103 | "eslint-config-prettier": "^8.3.0",
104 | "mocha": "^9.0.2",
105 | "prettier": "^2.3.2",
106 | "rollup": "^2.53.2",
107 | "rollup-plugin-filesize": "^9.1.1",
108 | "rollup-plugin-terser": "^7.0.2",
109 | "sinon": "^11.1.1",
110 | "sinon-chai": "^3.7.0",
111 | "typedoc": "^0.21.4",
112 | "typescript": "^4.3.x"
113 | },
114 | "dependencies": {
115 | "node-html-parser": "^4.1.0"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { terser } from "rollup-plugin-terser";
2 | import filesize from "rollup-plugin-filesize";
3 |
4 | export default [
5 | {
6 | input: "src/wafer.js",
7 | output: [
8 | {
9 | file: "dist/wafer.js",
10 | format: "es",
11 | sourcemap: true,
12 | },
13 | {
14 | file: "dist/wafer.browser.js",
15 | format: "iife",
16 | name: "Wafer",
17 | },
18 | ],
19 | plugins: [
20 | terser(),
21 | filesize({
22 | showBrotliSize: true,
23 | showGzippedSize: true,
24 | showMinifiedSize: true,
25 | }),
26 | ],
27 | },
28 | {
29 | input: "src/dom.js",
30 | output: [
31 | {
32 | file: "dist/dom.js",
33 | format: "es",
34 | sourcemap: true,
35 | },
36 | {
37 | file: "dist/dom.browser.js",
38 | format: "iife",
39 | name: "WaferDOM",
40 | },
41 | ],
42 | plugins: [
43 | terser(),
44 | filesize({
45 | showBrotliSize: true,
46 | showGzippedSize: true,
47 | showMinifiedSize: true,
48 | }),
49 | ],
50 | },
51 | {
52 | input: "src/knobs.js",
53 | output: [
54 | {
55 | file: "dist/knobs.js",
56 | format: "es",
57 | sourcemap: true,
58 | },
59 | {
60 | file: "dist/knobs.browser.js",
61 | format: "iife",
62 | },
63 | ],
64 | plugins: [
65 | terser(),
66 | filesize({
67 | showBrotliSize: true,
68 | showGzippedSize: true,
69 | showMinifiedSize: true,
70 | }),
71 | ],
72 | },
73 |
74 | // CJS for node environments using CJS
75 | {
76 | input: "src/wafer.js",
77 | output: {
78 | file: "lib/wafer.cjs",
79 | format: "cjs",
80 | exports: "default",
81 | },
82 | },
83 | {
84 | input: "src/dom.js",
85 | output: {
86 | file: "lib/dom.cjs",
87 | format: "cjs",
88 | },
89 | },
90 | {
91 | input: "src/server/wafer.js",
92 | output: {
93 | file: "lib/server/wafer.cjs",
94 | format: "cjs",
95 | exports: "default",
96 | },
97 | external: ["node-html-parser", "module"],
98 | },
99 | {
100 | input: "src/server/dom.js",
101 | output: {
102 | file: "lib/server/dom.cjs",
103 | format: "cjs",
104 | },
105 | external: ["node-html-parser", "module"],
106 | },
107 | {
108 | input: "src/server/element.js",
109 | output: {
110 | file: "lib/server/element.cjs",
111 | format: "cjs",
112 | },
113 | external: ["node-html-parser", "module"],
114 | },
115 | ];
116 |
--------------------------------------------------------------------------------
/src/common.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Common functionality shared between {@link WaferClient} and {@link WaferServer}
3 | *
4 | * @module Common
5 | */
6 |
7 | /**
8 | * Updated the the targets of a component
9 | *
10 | * @param {function} apply - function that will query DOM and apply changes to matches
11 | * @param {Element|import("./server/element").ServerElement} el - the DOM root to query
12 | * @param {Object} opts - options for how to update the targets
13 | * @param {any} opts.value - the value to use in updates
14 | * @param {import("./types").Target[]} [opts.targets] - array of selectors and which updates that should be applied
15 | *
16 | * @returns {Promise} Return a promise so the updates can be `await`ed in server context
17 | *
18 | */
19 | const updateTargets = async (apply, el, { value, targets = [] }) => {
20 | for (const target of targets) {
21 | const { selector, attribute, text, dom, property, use } = target;
22 |
23 | /**
24 | * Apply selector function, or use value directly
25 | */
26 | const selectorVal =
27 | typeof selector === "function" ? selector(value, el) : selector;
28 |
29 | /**
30 | * Find all matches for `selector` in `el`, and apply updates
31 | */
32 | await apply(
33 | el,
34 | selectorVal,
35 | /**
36 | * @param {Element} targetEl
37 | */
38 | async (targetEl) => {
39 | /**
40 | * Apply use function, or use value directly
41 | */
42 | const useValue = use ? use(value, el, targetEl) : value;
43 |
44 | if (attribute) {
45 | if ([null, false, undefined].includes(useValue)) {
46 | /**
47 | * Remove this attribute if new value is null/false/undefined
48 | */
49 | targetEl.removeAttribute(attribute);
50 | } else {
51 | /**
52 | * Update attribute with new value. If value is Boolean and true,
53 | * set attribute to empty string for consistency
54 | */
55 | targetEl.setAttribute(attribute, useValue === true ? "" : useValue);
56 | }
57 | }
58 |
59 | if (property) {
60 | /**
61 | * Update property unless
62 | */
63 | // @ts-ignore: allow to set any prop
64 | targetEl[property] = useValue;
65 | }
66 |
67 | if (text) {
68 | /**
69 | * Update element's text
70 | */
71 | targetEl.textContent = useValue;
72 | }
73 |
74 | if (dom) {
75 | /**
76 | * Apply DOM updates if defined
77 | */
78 | await dom(targetEl, useValue, el);
79 | }
80 | }
81 | );
82 | }
83 | };
84 |
85 | export { updateTargets };
86 |
--------------------------------------------------------------------------------
/src/dom.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Helpers for rendering in {@link WaferClient}
3 | *
4 | * @module DOMClient
5 | */
6 |
7 | import { updateTargets } from "./common.js";
8 |
9 | /**
10 | * Keep a cache of created templates
11 | */
12 | const templateCache = new Map();
13 |
14 | /**
15 | * Stamp out an element from a string template
16 | *
17 | * @param {string} html - The string template
18 | * @param {boolean} [firstChild] - if `true` use the `firstChild` of created element, used when a single element is required rather than a DocumentFragment
19 | *
20 | * @returns {DocumentFragment | Element} - The stamped out element
21 | */
22 | const stamp = (html, firstChild = false) => {
23 | /**
24 | * Keep things tidy
25 | */
26 | const trimmed = html.trim();
27 |
28 | /**
29 | * Retrieve from cache if possible
30 | */
31 | let cached = templateCache.get(trimmed);
32 | if (!cached) {
33 | /**
34 | * Create and populate a template element
35 | */
36 | const template = document.createElement("template");
37 | template.innerHTML = trimmed;
38 |
39 | /**
40 | * Update cache
41 | */
42 | templateCache.set(trimmed, template);
43 | cached = template;
44 | }
45 |
46 | /**
47 | * Clone away!
48 | */
49 |
50 | if (firstChild) {
51 | return cached.content.firstElementChild.cloneNode(true);
52 | }
53 |
54 | return cached.content.cloneNode(true);
55 | };
56 |
57 | /**
58 | * Helper function for emitting DOM events
59 | *
60 | * @param {Element} target - the element from which the event should be dispatched
61 | * @param {string} name - the event name
62 | * @param {object} [detail] - the detail object to pass with event
63 | * @param {EventInit} [opts] - event options
64 | */
65 | const emit = (
66 | target,
67 | name,
68 | detail = {},
69 | opts = {
70 | bubbles: true,
71 | composed: true,
72 | }
73 | ) => {
74 | target.dispatchEvent(
75 | new CustomEvent(name, {
76 | ...opts,
77 | detail,
78 | })
79 | );
80 | };
81 |
82 | /**
83 | * Utility to efficiently repeat and update a series of Elements into a
84 | * container, taking in an html template from which the element will be stamped
85 | * out, and a series of items and targets used to create and/or update
86 | * elements
87 | *
88 | * @param {Object} opts - Repeat options
89 | * @param {Element} opts.container - Container into which to render repeated items
90 | * @param {any[]} opts.items - Array of items which are used to create/update elements
91 | * @param {string} opts.html - HTML template which will be used to create elements
92 | * @param {((value: any, index: number) => string)} opts.keyFn - Key function which is used to create unique id for each item/element
93 | * @param {import("./types").Target[]} [opts.targets] - How to update an element when an item changes
94 | * @param { ((el: Element, item?: any, index?: number) => void) | null} [opts.init] - function to apply when an element is first created
95 | * @param { Object.} [opts.events] - list of events to bind to element when created
96 | *
97 | */
98 | const repeat = async ({
99 | container,
100 | items,
101 | html,
102 | keyFn,
103 | targets = [],
104 | init = null,
105 | events = {},
106 | }) => {
107 | /**
108 | * A record of an item's index (position) in the array to its key
109 | *
110 | * @type {Object.}
111 | */
112 |
113 | const indexToKey = {};
114 |
115 | /**
116 | * A record of an item's key to it's index (position) and existing element
117 | *
118 | * @type {Object.}
119 | */
120 | const keyMap = {};
121 |
122 | /**
123 | * Update records
124 | */
125 | for (const [index, item] of items.entries()) {
126 | /**
127 | * ensure key is a string so we can compare with key when set as an
128 | * attribute (that will always be a string)
129 | */
130 | const key = "" + keyFn(item, index);
131 | indexToKey[index] = key;
132 | keyMap[key] = { index, el: null };
133 | }
134 |
135 | /**
136 | * A record of key to existing elements
137 | *
138 | * @type {Object.}
139 | */
140 | const existingEls = {};
141 |
142 | /**
143 | * A record of existing elements to remove (i.e. they are not related by
144 | * key to any current item)
145 | */
146 | const toRemove = [];
147 |
148 | /**
149 | * All the keys of existing elements in DOM order
150 | */
151 | const childrenKeys = [];
152 |
153 | for (const el of container.children) {
154 | const key = el.getAttribute("wafer-key");
155 | if (key) {
156 | childrenKeys.push(key);
157 |
158 | /**
159 | * For each element in the container that has a key, keep a record of
160 | * this element
161 | */
162 | existingEls[key] = el;
163 |
164 | if (!keyMap[key]) {
165 | /**
166 | * If there's no match with the keys of the current items the element
167 | * should be removed
168 | */
169 | toRemove.push(el);
170 | } else {
171 | /**
172 | * If there is a match then reference the element in the keyMap so
173 | * it can be used for updates below
174 | */
175 | keyMap[key].el = el;
176 | }
177 | }
178 | }
179 |
180 | /**
181 | * Remove any unmatched elements
182 | */
183 | for (const el of toRemove) {
184 | el.remove();
185 | }
186 |
187 | /**
188 | * Update and move existing elements, moving those with the greatest distance
189 | * to travel first.
190 | */
191 |
192 | /**
193 | * For existing elements that need to be moved keep a track of where
194 | * they need to move to and how far they have travel
195 | *
196 | * @typedef ElInfo
197 | * @prop {Element} el - The existing element reference
198 | * @prop {number} targetIndex - The index the element should be moved to
199 | * @prop {number} distance - The distance the element needs to move
200 | *
201 | * @type {ElInfo[]}
202 | */
203 | const elementsToMove = [];
204 |
205 | /**
206 | * A counter indicating the new index position
207 | */
208 | let targetIndex = 0;
209 |
210 | for (const [index, item] of items.entries()) {
211 | const key = indexToKey[index];
212 | const currentIndex = childrenKeys.indexOf(key);
213 |
214 | if (existingEls[key]) {
215 | /**
216 | * Apply updates to existing elements
217 | */
218 | await updateTargets(apply, existingEls[key], {
219 | value: item,
220 | targets,
221 | });
222 |
223 | /**
224 | * Attach event listeners since the existing elements may have been
225 | * rendered on the server but won't have event listeners attached yet.
226 | * Adding the same bound listener multiple times is a no-op
227 | * TODO: think of a way to only do this if necessary
228 | */
229 | for (const selector of Object.keys(events)) {
230 | const eventNames = Object.keys(events[selector]);
231 |
232 | for (const name of eventNames) {
233 | const def = events[selector][name];
234 |
235 | bindEvent(existingEls[key], selector, name, def);
236 | }
237 | }
238 |
239 | if (targetIndex !== currentIndex) {
240 | /**
241 | * If the element has a new position, calculate the distance it has to
242 | * travel
243 | */
244 | const distance = Math.abs(currentIndex - targetIndex);
245 |
246 | /**
247 | * Update the records
248 | */
249 | elementsToMove.push({
250 | el: existingEls[key],
251 | targetIndex,
252 | distance,
253 | });
254 | }
255 |
256 | /**
257 | * Increment the new index
258 | */
259 | targetIndex++;
260 | }
261 | }
262 |
263 | /**
264 | * Sort by distance so we move the elements that have the greatest distance
265 | * to travel first
266 | */
267 | elementsToMove.sort((a, b) => b.distance - a.distance);
268 |
269 | /**
270 | * Move the elements
271 | */
272 | for (const item of elementsToMove) {
273 | if (item.targetIndex !== [...container.children].indexOf(item.el)) {
274 | /**
275 | * Move the element if it's not already in the correct place
276 | */
277 | container.children[item.targetIndex].after(item.el);
278 | }
279 | }
280 |
281 | /**
282 | * Add any new elements into their correct position inside the container
283 | */
284 |
285 | /**
286 | * Reverse items as we want to use `insertBefore` and will need the
287 | * (n+1)th element to exist before we can insert the (n)th element
288 | */
289 | const reversedItems = items.slice().reverse();
290 |
291 | for (const [reversedIndex, item] of reversedItems.entries()) {
292 | /**
293 | * Original index
294 | */
295 | const index = items.length - 1 - reversedIndex;
296 |
297 | const key = indexToKey[index];
298 |
299 | if (!existingEls[key]) {
300 | /**
301 | * If the element doesn't already exist, create it
302 | */
303 | const el = /** @type{Element} **/ (stamp(html, true));
304 |
305 | /**
306 | * Run the initialisation function if passed
307 | */
308 | if (init) {
309 | init(el, item, index);
310 | }
311 |
312 | /**
313 | * Update the element with desired target updates, and add the wafer-key
314 | * attribute for use later
315 | */
316 | await updateTargets(apply, el, {
317 | value: item,
318 | targets: targets.concat({
319 | selector: "self",
320 | use: () => key,
321 | attribute: "wafer-key",
322 | }),
323 | });
324 |
325 | /**
326 | * Bind events to new element
327 | */
328 | for (const selector of Object.keys(events)) {
329 | const eventNames = Object.keys(events[selector]);
330 |
331 | for (const name of eventNames) {
332 | const def = events[selector][name];
333 | bindEvent(el, selector, name, def);
334 | }
335 | }
336 |
337 | /**
338 | * Get a reference to the element after the one to be inserted.
339 | * If there isn't one then the element will be inserted at the end
340 | */
341 | const afterIndex = index + 1;
342 | const elAfter =
343 | keyMap[indexToKey[afterIndex]] && keyMap[indexToKey[afterIndex]].el;
344 | container.insertBefore(el, elAfter || null);
345 |
346 | /**
347 | * Update the keyMap with the new element reference so it's available
348 | * for the next element that's going to be inserted
349 | */
350 | keyMap[indexToKey[index]].el = el;
351 | }
352 | }
353 | };
354 |
355 | /**
356 | * Take a target DOM element and a selector to apply to that element
357 | * and run a function on every match
358 | *
359 | * @param {Element} el - target DOM element
360 | * @param {string} selector - CSS3 selector
361 | * @param {(el: Element) => void} func - function to run on all matches
362 | */
363 | const apply = async (el, selector, func) => {
364 | if (selector === "self") {
365 | /**
366 | * Special case - 'self' applies function to DOM element itself
367 | */
368 | await func(el);
369 | } else {
370 | /**
371 | * Special case - the selector should be applied to the
372 | * DOM node's shadow root
373 | */
374 | const shadow = selector[0] === "$";
375 |
376 | /**
377 | * Special case - the selector should be applied to the parent document
378 | */
379 | const doc = selector[0] === "@";
380 |
381 | /**
382 | * Use the correct target
383 | */
384 | const target = shadow ? el.shadowRoot || el : doc ? document : el;
385 |
386 | /**
387 | * Remove any leading character used for the special cases above
388 | */
389 | const targetSelector = shadow || doc ? selector.substr(1) : selector;
390 |
391 | /**
392 | * Apply function to all matches
393 | */
394 | for (const el of target.querySelectorAll(targetSelector)) {
395 | await func(el);
396 | }
397 | }
398 | };
399 |
400 | /**
401 | * Bind an event listener to a series of CSS selector matches in a DOM node
402 | *
403 | * @param {Element} el - the element then listener should be attached to (unless overidden by specifying a different target in `def`)
404 | * @param {string} selector - the selector whose matches in `el` this event listener should be added
405 | * @param {string} name - the name of the event whose listener is being added
406 | * @param {((ev: Event) => any) | import("./types").TargetEvent} func - the listener being added, or a `TargetEvent`
407 | */
408 | const bindEvent = (el, selector, name, func) => {
409 | /**
410 | * The bound function to add as a listener
411 | * @type {(this: Element, ev: Event) => any}
412 | */
413 | let boundFn;
414 |
415 | /**
416 | * The event listener options to use
417 | * @type {boolean | AddEventListenerOptions | undefined}
418 | */
419 | let fnOpts = undefined;
420 |
421 | if (typeof func !== "function") {
422 | /**
423 | * `func` is an `TargetEvent`, so pull out the details
424 | */
425 | const { fn, target = el, opts = {} } = func;
426 | fnOpts = opts;
427 |
428 | /**
429 | * Bind `TargetEvent` function to target
430 | */
431 | boundFn = fn.bind(target);
432 | } else {
433 | /**
434 | * `func` is a function so bind to `el`
435 | */
436 | boundFn = func.bind(el);
437 | }
438 |
439 | /**
440 | * Attach event listener to all matches of `selector` in `el`
441 | */
442 | apply(el, selector, (eventEl) => {
443 | eventEl.addEventListener(name, boundFn, fnOpts);
444 | });
445 | };
446 |
447 | export { stamp, emit, repeat, apply, bindEvent };
448 |
--------------------------------------------------------------------------------
/src/knobs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Component for inspecting {@link WaferClient} elements
3 | *
4 | * @module Knobs
5 | */
6 |
7 | import Wafer from "./wafer.js";
8 | import { repeat } from "./dom.js";
9 |
10 | // define an no-op html function to get syntax highlighting in template strings
11 | const html = String.raw;
12 |
13 | class Knobs extends Wafer {
14 | connectedCallback() {
15 | super.connectedCallback();
16 |
17 | const root = /** @type {Element} **/ (this.getRootNode());
18 |
19 | const el = root.querySelector(this.for);
20 |
21 | const componentProps = [];
22 | for (const [name, def] of Object.entries(el.constructor.props)) {
23 | componentProps.push({
24 | name,
25 | ...def,
26 | });
27 | }
28 |
29 | this.el = el;
30 | this.componentProps = componentProps;
31 |
32 | this.updateHTML();
33 | }
34 |
35 | static get template() {
36 | return html`
37 |
170 |
171 |
KNOBS
172 |
173 | about
174 |
175 |
176 | Knobs lets you inspect and edit both the properties and attributes
177 | of a Wafer component live. The current HTML can also be shown.
178 |
179 |
180 |
181 |
182 |
183 | Properties and attributes
184 |
185 |
186 |
Properties
187 |
188 |
189 |
190 |
Attributes
191 |
192 |
193 |
194 |
195 |
196 | HTML
197 |
198 |
199 | `;
200 | }
201 |
202 | static get props() {
203 | return {
204 | for: {
205 | type: String,
206 | targets: [
207 | {
208 | selector: "$h1 > div > span",
209 | /**
210 | *
211 | * @type {import("./types").UseFn}
212 | */
213 | use: (val) => `[${val}]`,
214 | text: true,
215 | },
216 | ],
217 | },
218 | el: {
219 | type: Object,
220 | },
221 | html: {
222 | type: String,
223 | targets: [
224 | {
225 | selector: "$#tag pre",
226 | text: true,
227 | },
228 | ],
229 | },
230 | componentProps: {
231 | type: Array,
232 | initial: [],
233 | targets: [
234 | {
235 | selector: "$#props .knobs, #attrs .knobs",
236 | /**
237 | * @type {import("./types").DOMUpdateFn}
238 | */
239 | dom: (el, props, self) => {
240 | const elWafer = /** @type {Wafer} */ (el);
241 | const selfWafer = /** @type {Wafer} */ (self);
242 |
243 | const isProps =
244 | elWafer.parentElement && elWafer.parentElement.id === "props";
245 |
246 | if (selfWafer._firstUpdate) {
247 | const updatedFn = selfWafer.el.updated.bind(selfWafer.el);
248 | /** @type {Wafer}*/ (selfWafer.el).updated = (changed) => {
249 | updatedFn(changed);
250 |
251 | for (const name of changed.keys()) {
252 | const input = /** @type {HTMLInputElement} */ (
253 | elWafer.querySelector(`#input[data-name=${name}]`)
254 | );
255 | if (!input) continue;
256 |
257 | const prop = selfWafer.componentProps.find(
258 | (/** @type {{name:string}} **/ info) => info.name === name
259 | );
260 |
261 | if (isProps) {
262 | if (prop.type === Boolean) {
263 | input.checked = selfWafer.el[name];
264 | } else if ([Number, String].includes(prop.type)) {
265 | input.value = selfWafer.el[name];
266 | } else {
267 | input.value = JSON.stringify(
268 | selfWafer.el[name],
269 | null,
270 | 2
271 | );
272 | }
273 | } else {
274 | if (prop.type === Boolean) {
275 | input.checked = selfWafer.el.hasAttribute(
276 | prop.attributeName || name
277 | );
278 | } else {
279 | input.value = selfWafer.el.getAttribute(
280 | prop.attributeName || name
281 | );
282 | }
283 | }
284 | }
285 |
286 | selfWafer.updateHTML();
287 | };
288 | }
289 |
290 | return repeat({
291 | container: elWafer,
292 | items: props,
293 | html: html`
294 |
295 |
300 |
301 | `,
302 | keyFn: (prop) => prop.name,
303 | targets: [
304 | {
305 | selector: "$div.propname",
306 | text: true,
307 | use: (prop) =>
308 | isProps ? prop.name : prop.attributeName || prop.name,
309 | },
310 | {
311 | selector: "$div.propinfo",
312 | text: true,
313 | use: (prop) => `
314 | ${prop.type.name}, reflect: ${
315 | prop.reflect ? "true" : "false"
316 | }, initial: ${JSON.stringify(prop.initial)}
317 | `,
318 | },
319 | {
320 | selector: "$#input",
321 | property: "value",
322 | use: (prop) => {
323 | if (prop.type !== Boolean) {
324 | if (isProps) {
325 | if ([Number, String].includes(prop.type)) {
326 | return selfWafer.el[prop.name];
327 | } else {
328 | return JSON.stringify(
329 | selfWafer.el[prop.name],
330 | null,
331 | 2
332 | );
333 | }
334 | } else {
335 | if (prop.reflect) {
336 | return selfWafer.el.getAttribute(
337 | prop.attributeName || prop.name
338 | );
339 | }
340 | }
341 | }
342 | },
343 | },
344 | {
345 | selector: "$#input",
346 | attribute: "checked",
347 | use: (prop) => {
348 | if (prop.type === Boolean) {
349 | if (isProps) {
350 | return selfWafer.el[prop.name] ? "" : null;
351 | } else {
352 | if (prop.reflect) {
353 | return selfWafer.el.hasAttribute(
354 | prop.attributeName || prop.name
355 | )
356 | ? ""
357 | : null;
358 | }
359 | }
360 | }
361 | },
362 | },
363 | {
364 | selector: "$#input",
365 | attribute: "data-name",
366 | use: (prop) => prop.name,
367 | },
368 | {
369 | selector: "$#input",
370 | attribute: "type",
371 | use: (prop) => {
372 | switch (prop.type) {
373 | case Number:
374 | if (isProps) {
375 | return "number";
376 | }
377 | return "text";
378 | case Boolean:
379 | return "checkbox";
380 | case String:
381 | return "text";
382 | default:
383 | return;
384 | }
385 | },
386 | },
387 | ],
388 | events: {
389 | "#input": {
390 | change: (event) => {
391 | const input = /** @type {HTMLInputElement} */ (
392 | event.target
393 | );
394 | const name = input.dataset.name || "";
395 | const prop = selfWafer.componentProps.find(
396 | (/** @type {{name: string}}*/ a) => a.name === name
397 | );
398 |
399 | if (prop) {
400 | if (prop.type === Boolean) {
401 | if (isProps) {
402 | selfWafer.el[name] = input.checked;
403 | } else {
404 | if (input.checked) {
405 | selfWafer.el.setAttribute(
406 | prop.attributeName || name,
407 | ""
408 | );
409 | } else {
410 | selfWafer.el.removeAttribute(
411 | prop.attributeName || name
412 | );
413 | }
414 | }
415 | } else {
416 | const value = input.value.trim();
417 |
418 | if (isProps) {
419 | if (prop.type === Number) {
420 | selfWafer.el[name] = Number(value);
421 | } else if (prop.type === String) {
422 | selfWafer.el[name] = value;
423 | } else {
424 | selfWafer.el[name] = JSON.parse(value);
425 | }
426 | } else {
427 | const value = input.value.trim();
428 | selfWafer.el.setAttribute(
429 | prop.attributeName || name,
430 | value
431 | );
432 | }
433 | }
434 | }
435 | },
436 | },
437 | },
438 | init: (el, prop) => {
439 | if ([Boolean, Number, String].includes(prop.type)) {
440 | const input = document.createElement("input");
441 | input.id = "input";
442 | const container = el.querySelector("#input-container");
443 | if (container) {
444 | container.append(input);
445 | }
446 | } else {
447 | const input = document.createElement("textarea");
448 | input.id = "input";
449 | const container = el.querySelector("#input-container");
450 | if (container) {
451 | container.append(input);
452 | }
453 | }
454 | },
455 | });
456 | },
457 | },
458 | ],
459 | },
460 | };
461 | }
462 |
463 | updateHTML() {
464 | this.html = this.el.outerHTML;
465 | }
466 | }
467 |
468 | customElements.define("wafer-knobs", Knobs);
469 |
--------------------------------------------------------------------------------
/src/server/dom.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Helpers for rendering in {@link WaferServer}
3 | *
4 | * @module DOMServer
5 | */
6 |
7 | import { ServerElement, parse } from "./element.js";
8 | import { updateTargets } from "../common.js";
9 |
10 | /**
11 | * Utility to render a series of items as Elements into a container
12 | *
13 | * @param {Object} opts - Repeat options
14 | * @param {ServerElement} opts.container - Container into which to render repeated items
15 | * @param {any[]} opts.items - Array of items which are used to create/update elements
16 | * @param {string} opts.html - HTML template which will be used to create elements
17 | * @param {((value: any, index: number) => string)} opts.keyFn - Key function which is used to create unique id for each item/element
18 | * @param {import("../types").Target[]} [opts.targets] - How to update an element when an item changes
19 | * @param { ((el: ServerElement, item?: any, index?: number) => void) | null} [opts.init] - function to apply when an element is first created
20 | * @param {import("../types").Registry} [opts.registry] - Object of tag names to Wafer component definitions
21 | * @returns
22 | */
23 | const repeat = async ({
24 | container,
25 | items,
26 | html,
27 | keyFn,
28 | targets = [],
29 | init = null,
30 | registry = {},
31 | }) => {
32 | /**
33 | * Empty the container
34 | */
35 | container.innerHTML = "";
36 |
37 | for (const [index, item] of items.entries()) {
38 | const key = "" + keyFn(item, index);
39 |
40 | /**
41 | * Create a {@link ServerElement} for each item by rendering from the
42 | * template and extracting the first {@link ServerElement} instance
43 | */
44 | const el = /** @type {ServerElement[]} */ (
45 | (await parse(html.trim(), registry)).childNodes
46 | ).filter((node) => node instanceof ServerElement)[0];
47 |
48 | /**
49 | * Run the initialisation function if passed
50 | */
51 | if (init) {
52 | init(el, item, index);
53 | }
54 |
55 | /**
56 | * Update the element with desired target updates, and add the wafer-key
57 | * attribute for use later. The result is `await`ed so that asynchronous
58 | * updates can complete before response is sent
59 | */
60 | await updateTargets(apply, el, {
61 | value: item,
62 | targets: targets.concat({
63 | selector: "self",
64 | use: () => key,
65 | attribute: "wafer-key",
66 | }),
67 | });
68 |
69 | /**
70 | * Add element into container
71 | */
72 | container.appendChild(el);
73 | }
74 | };
75 |
76 | /**
77 | * Take a target DOM element and a selector to apply to that element
78 | * and run a function on every match. A promise is returned so that
79 | * the result can be `await`ed - important in the server context
80 | * where we want all asynchronous updates can complete before the
81 | * response is sent
82 | *
83 | * @param {Element|import("./wafer").default} el
84 | * @param {string} selector
85 | * @param {(el: Element|import("./element").ServerElement) => void} func
86 | *
87 | * @returns {Promise}
88 | */
89 | const apply = (el, selector, func) => {
90 | /**
91 | * An array containing the results of each update
92 | */
93 | const promises = [];
94 |
95 | if (selector === "self") {
96 | /**
97 | * Special case - 'self' applies function to DOM element itself
98 | */
99 | promises.push(func(el));
100 | } else {
101 | /**
102 | * Special case - the selector should be applied to the
103 | * DOM node's shadow root
104 | */
105 | const shadow = selector[0] === "$";
106 |
107 | /**
108 | * Special case - the selector should be applied to the parent 'document'
109 | * 'document' in a server context is defined as the root element, whatever
110 | * element that is - i.e. no necessarily the document, which is the case
111 | * for the client implementation
112 | */
113 | const doc = selector[0] === "@";
114 |
115 | /**
116 | * The element on which the selector will be run
117 | */
118 | let target;
119 |
120 | if (doc) {
121 | /**
122 | * If we are applying the selector to the 'document', then ascent the
123 | * hierarchy until we reach the top
124 | */
125 | target = el;
126 | while (target.parentNode) {
127 | target = target.parentNode;
128 | }
129 | } else {
130 | /**
131 | * Use the shadow root as the target in the special case
132 | */
133 | target = shadow ? el.shadowRoot : el;
134 | }
135 |
136 | if (target) {
137 | /**
138 | * Remove any leading character used for the special cases above
139 | */
140 | const targetSelector = shadow || doc ? selector.substr(1) : selector;
141 |
142 | /**
143 | * Apply function to all matches, store in results array
144 | */
145 | for (const el of target.querySelectorAll(targetSelector)) {
146 | promises.push(func(el));
147 | }
148 | }
149 | }
150 |
151 | /**
152 | * Return a promise that will resolve when all updates have resolved
153 | */
154 | return Promise.all(promises);
155 | };
156 |
157 | export { repeat, apply };
158 |
--------------------------------------------------------------------------------
/src/server/element.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Server implementation of DOM Element
3 | *
4 | * @module ServerElement
5 | */
6 |
7 | import { createRequire } from "module";
8 | const require = createRequire(import.meta.url);
9 |
10 | const { parse, HTMLElement } = require("node-html-parser");
11 |
12 | /**
13 | * Escape values for attributes and text content
14 | * @param {any} unsafe
15 | * @returns {any}
16 | */
17 | const escape = (unsafe) => {
18 | if (typeof unsafe !== "string") {
19 | return unsafe;
20 | }
21 |
22 | return unsafe
23 | .replace(/&/g, "&")
24 | .replace(//g, ">")
26 | .replace(/"/g, """)
27 | .replace(/'/g, "'");
28 | };
29 |
30 | /**
31 | * Provides an interface compatible with the browser `HTMLElement` - at least
32 | * in so much as it provides the capabilities Wafer requires. The actual
33 | * implementation is proxied to node-html-parser implementation of
34 | * `HTMLElement`. It should be relatively straight forward to swap this
35 | * implementation out for an alternative one if required.
36 | */
37 | class ServerElement {
38 | /**
39 | * @param {string} tagName - the tag name of this element
40 | * @param {Object.} attrs - Object of initial attribute name/value pairs
41 | */
42 | constructor(tagName, attrs = {}) {
43 | /**
44 | * Element containing reference to underlying `HTMLElement` implementation.
45 | * The element is proxied in this class to:
46 | *
47 | * - avoid clashes with `HTMLElement` implementation details
48 | *
49 | * - make it easy to swap out for alternative implementations
50 | */
51 | this._element = /** @type { ServerElement} **/ (
52 | /** @type { unknown } **/
53 | (new HTMLElement(tagName, {}, "", null))
54 | );
55 | this._element.setAttributes(attrs);
56 | }
57 |
58 | /**
59 | * Update the underlying element to new element
60 | * @param {ServerElement} element
61 | */
62 | setElement(element) {
63 | this._element = element;
64 | }
65 |
66 | /**
67 | * Retrieve the underlying element
68 | *
69 | * @returns {ServerElement}
70 | */
71 | getElement() {
72 | return this._element;
73 | }
74 |
75 | /**
76 | * Does this element have an attribute set with named `key`
77 | *
78 | * @param {string} key - Attribute name
79 | * @returns {boolean}
80 | */
81 | hasAttribute(key) {
82 | return this._element.hasAttribute(key);
83 | }
84 |
85 | /**
86 | * Return the current value of names attribute
87 | *
88 | * @param {string} key - Attribute name
89 | * @returns {string}
90 | */
91 | getAttribute(key) {
92 | return this._element.getAttribute(key);
93 | }
94 |
95 | /**
96 | * Sets the value for the name attribute
97 | *
98 | * @param {string} key - Attribute name
99 | * @param {string} value - Value to set
100 | * @returns
101 | */
102 | setAttribute(key, value) {
103 | this._element.setAttribute(key, escape(value));
104 | }
105 |
106 | /**
107 | * Sets all attributes at once
108 | *
109 | * @param {Object.} attrs - Value to set
110 | * @returns
111 | */
112 | setAttributes(attrs) {
113 | /**
114 | * @type {Object.}
115 | */
116 | const escaped = {};
117 |
118 | for (const [name, value] of Object.entries(attrs)) {
119 | escaped[name] = escape(value);
120 | }
121 |
122 | this._element.setAttributes(escaped);
123 | }
124 |
125 | /**
126 | * Remove names attribute
127 | *
128 | * @param {string} key - Attribute name
129 | */
130 | removeAttribute(key) {
131 | this._element.removeAttribute(key);
132 | }
133 |
134 | /**
135 | * Append a child to the underlying element implementation
136 | *
137 | * @param {ServerElement} el - Element to append
138 | * @returns {void}
139 | */
140 | appendChild(el) {
141 | return this._element.appendChild(el.getElement());
142 | }
143 |
144 | /**
145 | * Query underlying element for single match
146 | *
147 | * @param {string} selector - CSS3 selector
148 | * @returns {ServerElement}
149 | */
150 | querySelector(selector) {
151 | return this._element.querySelector(selector);
152 | }
153 |
154 | /**
155 | * Query underlying element for all matches
156 | *
157 | * @param {string} selector
158 | * @returns {ServerElement[]}
159 | */
160 | querySelectorAll(selector) {
161 | return this._element.querySelectorAll(selector);
162 | }
163 |
164 | /**
165 | * The element's tag name
166 | *
167 | * @returns {string}
168 | */
169 | get tagName() {
170 | return this._element.tagName;
171 | }
172 |
173 | /**
174 | * Object containing the element's attributes
175 | *
176 | * @returns {Object.}
177 | */
178 | get attributes() {
179 | return this._element.attributes;
180 | }
181 |
182 | /**
183 | * The first child of this element
184 | *
185 | * @returns {ServerElement}
186 | */
187 | get firstChild() {
188 | return this._element.firstChild;
189 | }
190 |
191 | /**
192 | * Set the child nodes of this element
193 | *
194 | * @returns
195 | */
196 | set childNodes(nodes) {
197 | this._element.childNodes = nodes;
198 | }
199 |
200 | /**
201 | * The child nodes of this element
202 | *
203 | * @returns {ServerElement[]}
204 | */
205 | get childNodes() {
206 | return this._element.childNodes;
207 | }
208 |
209 | /**
210 | * The parent of this element
211 | *
212 | * @returns {ServerElement}
213 | */
214 | set parentNode(node) {
215 | this._element.parentNode = node;
216 | }
217 |
218 | /**
219 | * The parent of this element
220 | *
221 | * @returns {ServerElement}
222 | */
223 | get parentNode() {
224 | return this._element.parentNode;
225 | }
226 |
227 | /**
228 | * Element's node type
229 | *
230 | * @returns {import('node-html-parser').NodeType}
231 | */
232 | get nodeType() {
233 | return this._element.nodeType;
234 | }
235 |
236 | /**
237 | * The tag name of the original element. This is used internally by
238 | * node-html-parser and needs to br proxied here so a ServerElement can
239 | * be used everywhere node-html-parser's HTMLElement is
240 | *
241 | * @returns {string}
242 | */
243 | get rawTagName() {
244 | return this._element.rawTagName;
245 | }
246 |
247 | /**
248 | * Set the text content of underlying element
249 | * @param {string} content
250 | */
251 | set textContent(content) {
252 | this._element.textContent = escape(content);
253 | }
254 |
255 | /**
256 | * Get the text content of underlying element
257 | *
258 | * @returns {string}
259 | */
260 | get textContent() {
261 | return this._element.textContent;
262 | }
263 |
264 | /**
265 | * Set the innerHTML of underlying element
266 | *
267 | * @param {string} html
268 | */
269 | set innerHTML(html) {
270 | this._element.innerHTML = html;
271 | }
272 |
273 | /**
274 | * Render this element to an HTML string
275 | *
276 | * @returns {string}
277 | */
278 | toString() {
279 | return this._element.toString();
280 | }
281 |
282 | /**
283 | * Promise resolves when all pending updates have been processed
284 | *
285 | * @param {import("../types").Registry} registry - registry of tag names to Wafer component definitions
286 | *
287 | * @returns {Promise}
288 | */
289 | async updateDone(registry) {
290 | for (const child of this.childNodes) {
291 | await child.updateDone(registry);
292 | }
293 |
294 | if (registry[this.tagName && this.tagName.toLowerCase()]) {
295 | await this.updateDone(registry);
296 | }
297 | }
298 | }
299 |
300 | /**
301 | * Convert an instance of node-html-parser's HTMLElement into a ServerElement
302 | * instance
303 | *
304 | * @param {ServerElement} el - the HTMLElement instance
305 | * @param {import("../types").Registry} registry - registry of tag names to Wafer component definitions
306 | *
307 | * @returns {Promise}
308 | */
309 | const convert = async (el, registry = {}) => {
310 | /**
311 | * Convert any child nodes of `el` into `ServerElement` instances too
312 | */
313 | const children = [];
314 |
315 | for (const node of el.childNodes) {
316 | children.push(await convert(node, registry));
317 | }
318 |
319 | /**
320 | * Swap current children with their `ServerElement` counterparts
321 | */
322 | el.childNodes = children;
323 |
324 | /**
325 | * Lower case tag name of the element
326 | */
327 | const tagName = el.tagName && el.tagName.toLowerCase();
328 |
329 | /**
330 | * The new ServerElement
331 | */
332 | let newEl;
333 |
334 | if (Object.keys(registry).includes(tagName)) {
335 | /**
336 | * If this element has a tag name of a Wafer component, create Wafer instance
337 | */
338 | newEl = new registry[tagName].def({ tagName }, registry);
339 |
340 | /**
341 | * Set as underlying element
342 | */
343 | newEl.setElement(el);
344 |
345 | /**
346 | * Await construction
347 | */
348 | await newEl.construct();
349 |
350 | /**
351 | * Take attributes of element and copy them over to new ServerElement
352 | */
353 | for (const [attribute, value] of Object.entries(el.attributes)) {
354 | newEl.setAttribute(attribute, value);
355 | }
356 |
357 | /**
358 | * Simulate connection to the 'DOM'
359 | */
360 | await newEl.connectedCallback();
361 | } else {
362 | /**
363 | * Not a Wafer component, so just creating a ServerElement requires no
364 | * special setup
365 | */
366 | newEl = new ServerElement(tagName);
367 | newEl.setElement(el);
368 | }
369 |
370 | return newEl;
371 | };
372 |
373 | /**
374 | * Render an HTML string as a tree of `ServerElement`s
375 | *
376 | * @param {string} htmlString - string to parse
377 | * @param {import("../types").Registry} registry - registry of tag names to Wafer component definitions
378 | *
379 | * @returns {Promise}
380 | */
381 | const waferParse = async (htmlString, registry = {}) => {
382 | /**
383 | * Parse string into a tree of `HTMLElement`s
384 | */
385 | const tree = /** @type {ServerElement} **/ (
386 | /** @type {unknown} **/ (parse(htmlString))
387 | );
388 |
389 | /**
390 | * Convert `HTMLElement` tree into a `ServerElement` tree
391 | */
392 | return convert(tree, registry);
393 | };
394 |
395 | export { ServerElement, waferParse as parse };
396 |
--------------------------------------------------------------------------------
/src/server/wafer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Server implementation of Wafer
3 | *
4 | * @module WaferServer
5 | */
6 |
7 | import { WaferMixin } from "../wafer-mixin.js";
8 | import { ServerElement, parse } from "./element.js";
9 | import { updateTargets } from "../common.js";
10 | import { apply } from "./dom.js";
11 |
12 | export default class WaferServer extends WaferMixin(ServerElement) {
13 | /**
14 | *
15 | * @param {import("../types").ServerOpts} opts
16 | * @param {import("../types").Registry} registry
17 | * Registry of components and their Wafer definitions that should be upgraded if
18 | * found in the component template
19 | */
20 | constructor({ shadow = "open", tagName, attrs = {} }, registry = {}) {
21 | /**
22 | * Initialise parent {@link ServerElement} with the tag name and any attributes
23 | * to set on initialisation
24 | */
25 | super(tagName, attrs);
26 |
27 | /**
28 | * Type of shadow root (open/closed) or falsey for none
29 | *
30 | * @type {import("../types").ShadowOpts}
31 | */
32 | this._shadow = shadow;
33 |
34 | /**
35 | * Tag name that this component will be rendered within
36 | *
37 | * @type {string}
38 | */
39 | this._tagName = tagName;
40 |
41 | /**
42 | * @type {import('../types').Registry}
43 | */
44 | this._registry = registry;
45 |
46 | /**
47 | * In the server context only remove unreflected attributes
48 | * if there is no intention to rehydrate on the client (since attribute
49 | * values are the source for rehydrating)
50 | */
51 | super._removeUnreflectedAttributes = !!(
52 | this._registry &&
53 | this._registry[tagName] &&
54 | this._registry[tagName].serverOnly
55 | );
56 |
57 | /**
58 | * Holds the server implementation of a shadow root if required
59 | *
60 | * @type {ServerElement | null}
61 | */
62 | this.shadowRoot = null;
63 | }
64 |
65 | /**
66 | * Called to initialise the component
67 | */
68 | async construct() {
69 | if (this._shadow) {
70 | /**
71 | * Attach a shadow root
72 | */
73 | const shadowRoot = this.attachShadow({ mode: this._shadow });
74 |
75 | /**
76 | * Render template as a series of {@link ServerElement}s
77 | */
78 | const template = await parse(
79 | /** @type {typeof WaferServer} */ (this.constructor).template,
80 | this._registry
81 | );
82 |
83 | /**
84 | * Append template contents into shadow root
85 | */
86 | for (const el of template.childNodes) {
87 | shadowRoot.appendChild(el);
88 | }
89 | }
90 |
91 | this.initialiseProps();
92 | }
93 |
94 | /**
95 | * Create and attach a shadow root, which on the server is an instance
96 | * of a template {@link ServerElement}
97 | *
98 | * @param {Object} opts
99 | * @param {"open"|"closed"} opts.mode
100 | * @returns {ServerElement}
101 | */
102 | attachShadow({ mode = "open" }) {
103 | this.shadowRoot = new ServerElement("template", {
104 | shadowroot: mode,
105 | });
106 | this.appendChild(this.shadowRoot);
107 |
108 | return this.shadowRoot;
109 | }
110 |
111 | /**
112 | * Called when an element is 'attached' to the DOM on the server. This is
113 | * called explicitly on the server before rendering takes place
114 | *
115 | * @returns {Promise}
116 | */
117 | async connectedCallback() {
118 | if (!this._connectedOnce) {
119 | /**
120 | * Initialise the component
121 | */
122 | if (!this.shadowRoot) {
123 | /**
124 | * The Light DOM is being used so stamp out the template
125 | * and append to element content as a series of {@link ServerElement}s
126 | */
127 |
128 | const container = await parse(
129 | /** @type {typeof WaferServer} */ (this.constructor).template,
130 | this._registry
131 | );
132 |
133 | /**
134 | * Append template contents into shadow root
135 | */
136 | for (const el of container.childNodes) {
137 | this.appendChild(el);
138 | }
139 | }
140 |
141 | if (!this._removeUnreflectedAttributes) {
142 | /**
143 | * Indicate to the client that this component should be
144 | * rehydrated on client
145 | */
146 | this.setAttribute("wafer-ssr", "");
147 | }
148 |
149 | this.setupPropValues();
150 | }
151 |
152 | /**
153 | * Wait for all initial rendering to complete
154 | */
155 | await this.updateDone();
156 |
157 | /**
158 | * Indicate the initial set up has been completed
159 | */
160 | super._connectedOnce = true;
161 | }
162 |
163 | /**
164 | * Returns true if the component has been rendered on the server
165 | * and has not yet been hydrated. Only true in client context
166 | *
167 | * @type {boolean}
168 | */
169 | get _needsRehydrating() {
170 | return false;
171 | }
172 |
173 | /**
174 | * Update the targets for property
175 | *
176 | * @param {string} name
177 | */
178 | updateTargets(name) {
179 | const value = this._props[name];
180 | const { targets = [] } = this.props[name];
181 |
182 | return updateTargets(apply, this, { value, targets });
183 | }
184 |
185 | /**
186 | * Proxy for `setAttribute` in {@link ServerElement} ensuring
187 | * `attributeChangedCallback` is called on this component
188 | *
189 | * @param {string} key
190 | * @param {string} value
191 | */
192 | setAttribute(key, value) {
193 | const oldValue = this.getAttribute(key);
194 | super.setAttribute(key, value);
195 |
196 | if (
197 | /** @type {typeof WaferServer} */ (
198 | this.constructor
199 | ).observedAttributes.includes(key)
200 | ) {
201 | this.attributeChangedCallback(key, oldValue, value);
202 | }
203 | }
204 |
205 | /**
206 | * Proxy for `getAttribute` in {@link ServerElement} ensuring
207 | * `attributeChangedCallback` is called on this component
208 | *
209 | * @param {string} key
210 | */
211 | removeAttribute(key) {
212 | const oldValue = this.getAttribute(key);
213 | super.removeAttribute(key);
214 |
215 | if (
216 | /** @type {typeof WaferServer} */ (
217 | this.constructor
218 | ).observedAttributes.includes(key)
219 | ) {
220 | this.attributeChangedCallback(key, oldValue, null);
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/types.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Collection of type definitions used throughout Wafer
3 | *
4 | * @module Types
5 | */
6 |
7 | /**
8 | * @typedef { import("node-html-parser").Node } ParserNode
9 | * @typedef { import("node-html-parser").HTMLElement } ParserHTMLElement
10 | * @typedef { import("./server/element").ServerElement } ServerElement
11 | * @typedef { import("./server/wafer").default } WaferServer
12 | */
13 |
14 | /**
15 | * @typedef Prop - Property declaration
16 | * @prop {StringConstructor | NumberConstructor | BooleanConstructor | ObjectConstructor | ArrayConstructor} type - Property type
17 | * @prop {any} [initial] - Initial value for property
18 | * @prop {boolean} [reflect=false] - Should the property be reflected to an attribute with the same name
19 | * @prop {Target[]} [targets=[]] - A list of targets that should be updated when the property value changes
20 | * @prop {string[]} [triggers=[]] - A list of other properties whose targets should be updated when this property changes
21 | * @prop {string} [attributeName] - The name of the attribute this property can be initialised from / reflected to
22 | */
23 |
24 | /**
25 | * @typedef Target
26 | * Target declaration defining how to update elements matching `selector`
27 | * @prop {string | ((value: any, el: ServerElement | Element) => string)} selector CSS3 selector
28 | * @prop {string} [attribute] - which attribute on matches to update with the property value
29 | * @prop {string} [property] - which property on matches to update with the property value
30 | * @prop {boolean} [text=false] - should the textContent of matches be set to the property value
31 | * @prop {DOMUpdateFn} [dom] - a function that will be called when the property value changes - usually used for general DOM updates
32 | * @prop {UseFn} [use] - a function that returns the value that should be used in updates (defaults to the property value itself)
33 | */
34 |
35 | /**
36 | * @callback DOMUpdateFn - Function to run for a {@link ./Target} when a property value changes
37 | * @param {Element | ServerElement} target - The target element itself
38 | * @param {any} value - The property value (or the value returned from `use` if defined)
39 | * @param {Element | ServerElement} el - Reference to the component
40 | * @returns {Promise | void} - In the server context only a returned promise will be `await`ed
41 | */
42 |
43 | /**
44 | * @callback UseFn - Function to run before updating {@link ./Target}s, returning the value to use in updates
45 | * @param {any} value - The new property value
46 | * @param {HTMLElement | {}} el - Reference to the component
47 | * @param {HTMLElement | {}} [targetEl] - Reference to the target element
48 | * @returns {any} - The value to use in updates, in place of the new property value
49 | */
50 |
51 | /**
52 | * @typedef ShadowOpts - If an what type of Shadow Root to attach
53 | * @type {'open' | 'closed' | false} - False indicates no Shadow Root
54 | */
55 |
56 | /**
57 | * @typedef ClientOpts - Possible arguments passed to client constructor
58 | * @prop {ShadowOpts} [shadow] - What type of Shadow Root to attach
59 | */
60 |
61 | /**
62 | * @typedef ServerOpts - Possible arguments passed to server constructor
63 | * @prop {ShadowOpts} [shadow] - What type of Shadow Root to attach
64 | * @prop {string} tagName - What tag should this component be defined with
65 | * @prop {Object.} [attrs] - Object of initial attribute name/values
66 | */
67 |
68 | /**
69 | * @typedef TargetEvent - Explicit event handler
70 | * @prop {(this: Element, ev: Event) => any} fn - Function to call
71 | * @prop {HTMLElement} [target] - Element to bind the function to
72 | * @prop {boolean | AddEventListenerOptions | undefined} [opts] - Event listener options
73 | */
74 |
75 | /**
76 | * @typedef TargetEvents - Object of event names to event handlers
77 | * @type {Object. any) | TargetEvent>} - explicit {@link TargetEvent} definition, or a function that will be automatically bound to the component
78 | */
79 |
80 | /**
81 | * @typedef RegistryEntry
82 | * @prop {new (...args: any[]) => WaferServer} def - The component definition
83 | * @prop {boolean} [serverOnly] - If this component is intended only to be rendered on server (i.e. not upgraded or rehydrated on client)
84 | */
85 |
86 | /**
87 | * @typedef Registry - Object of tag names to Wafer component definitions
88 | * @type {Object.}
89 | */
90 |
91 | export {};
92 |
--------------------------------------------------------------------------------
/src/wafer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Client implementation of Wafer
3 | *
4 | * @module WaferClient
5 | */
6 |
7 | import { WaferMixin } from "./wafer-mixin.js";
8 | import { stamp, apply, bindEvent } from "./dom.js";
9 | import { updateTargets } from "./common.js";
10 |
11 | let isSSR = false;
12 |
13 | export default class WaferClient extends WaferMixin(HTMLElement) {
14 | /**
15 | * Does the current client context support Declarative Shadow DOM
16 | *
17 | * @type {boolean}
18 | */
19 | static get supportsDSD() {
20 | // eslint-disable-next-line no-prototype-builtins
21 | return HTMLTemplateElement.prototype.hasOwnProperty("shadowRoot");
22 | }
23 |
24 | /**
25 | * @param {boolean} ssr
26 | */
27 | static set isSSR(ssr) {
28 | isSSR = ssr;
29 | }
30 |
31 | /**
32 | * Object keyed by a CSS Selector to an object mapping DOM event names
33 | * to the function that should be called when the event occurs. The function
34 | * will be automatically bound to the current element. Alternatively
35 | * the event name can map to a {@link TargetEvent} to enable binding to
36 | * different targets and/or use different event options
37 | *
38 | *
39 | * @type {Object.}
40 | */
41 | get events() {
42 | return {};
43 | }
44 |
45 | /**
46 | *
47 | * @param {import("./types").ClientOpts} opts
48 | *
49 | */
50 | constructor({ shadow = "open" } = {}) {
51 | super();
52 | this.init(shadow);
53 | }
54 |
55 | /**
56 | *
57 | * @param {import("./types").ShadowOpts} shadow
58 | */
59 | init(shadow) {
60 | /**
61 | * If we want to use the ShadowDOM (else use LightDOM)
62 | */
63 | if (shadow) {
64 | /**
65 | * There will be no Shadow root yet, unless the component was rendered
66 | * on the server, and the browser supports DSD
67 | */
68 | if (!this.shadowRoot) {
69 | if (this._needsRehydrating) {
70 | /**
71 | * The component was rendered on the server and hasn't been
72 | * rehydrated yet
73 | **/
74 | if (
75 | !(/** @type {typeof WaferClient} */ (this.constructor).supportsDSD)
76 | ) {
77 | /**
78 | * There's no DSD support in the browser so move any shadowroot
79 | * template element into the Shadow DOM
80 | */
81 | const template = /** @type {HTMLTemplateElement} */ (
82 | this.querySelector("template[shadowroot]")
83 | );
84 | if (template) {
85 | const shadowRoot = this.attachShadow({
86 | mode: shadow,
87 | });
88 | shadowRoot.appendChild(template.content);
89 | template.remove();
90 | }
91 | }
92 | } else {
93 | /**
94 | * The component wasn't rendered on the server, so stamp out template
95 | * and append to Shadow DOM
96 | */
97 | this.attachShadow({ mode: shadow }).appendChild(
98 | stamp(/** @type {typeof WaferClient} */ (this.constructor).template)
99 | );
100 | }
101 | }
102 | }
103 |
104 | this.initialiseProps();
105 | }
106 |
107 | /**
108 | * Called when an element is attached to the DOM
109 | */
110 | connectedCallback() {
111 | if (!this._connectedOnce) {
112 | /**
113 | * If the component is being rendered on the server, but not
114 | * using WaferServer then unreflected attributes should not be removed
115 | */
116 | super._removeUnreflectedAttributes =
117 | !this.hasAttribute("x-ssr") || !isSSR;
118 |
119 | if (!this.hasAttribute("x-ssr") && isSSR) {
120 | this.setAttribute("wafer-ssr", "");
121 | }
122 |
123 | /**
124 | * Initialise the component
125 | */
126 | if (!this.shadowRoot && !this._needsRehydrating) {
127 | /**
128 | * The Light DOM is being used and wasn't rendered on the server so
129 | * stamp out the template and append to element content
130 | */
131 | this.appendChild(
132 | stamp(/** @type {typeof WaferClient} */ (this.constructor).template)
133 | );
134 | }
135 |
136 | this.setupPropValues();
137 |
138 | /**
139 | * Bind declared events
140 | */
141 | for (const selector of Object.keys(this.events)) {
142 | const eventNames = Object.keys(this.events[selector]);
143 |
144 | for (const name of eventNames) {
145 | bindEvent(this, selector, name, this.events[selector][name]);
146 | }
147 | }
148 | }
149 |
150 | /**
151 | * Indicate the initial set up has been completed
152 | */
153 | super._connectedOnce = true;
154 | }
155 |
156 | /**
157 | * Returns true if the component has been rendered on the server
158 | * and has not yet been rehydrated
159 | *
160 | * @type {boolean}
161 | */
162 | get _needsRehydrating() {
163 | return this._firstUpdate && this.hasAttribute("wafer-ssr") && !isSSR;
164 | }
165 |
166 | /**
167 | * Update the targets for property
168 | *
169 | * @param {string} name
170 | */
171 | updateTargets(name) {
172 | return updateTargets(apply, this, {
173 | value: this._props[name],
174 | targets: this.props[name].targets,
175 | });
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/test/client/changed-updated.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../src/wafer.js";
5 |
6 | describe("Wafer update/changed calls", () => {
7 | it(`should call changed once, updated once when one prop changes (not connected to DOM)`, async () => {
8 | class Test extends Wafer {
9 | static props = {
10 | test: {
11 | type: String,
12 | reflect: true,
13 | initial: "foo",
14 | },
15 | };
16 | }
17 | customElements.define(`wafer-test-0`, Test);
18 |
19 | /**
20 | * @type {Test}
21 | */
22 | const el = new Test();
23 |
24 | const spyChanged = sinon.spy(el, "changed");
25 | const spyUpdated = sinon.spy(el, "updated");
26 |
27 | el.test = "bar";
28 |
29 | await el.updateDone();
30 |
31 | expect(el._connectedOnce).to.equal(false);
32 |
33 | expect(spyChanged).to.have.callCount(1);
34 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
35 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
36 | expect(spyChanged.args[0][0].size).equal(1);
37 | expect(spyChanged.args[0][0].has("test")).equal(true);
38 | expect(spyChanged.args[0][0].get("test")).equal(undefined);
39 |
40 | expect(spyUpdated).to.have.callCount(1);
41 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
42 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
43 | expect(spyUpdated.args[0][0].size).equal(1);
44 | expect(spyUpdated.args[0][0].has("test")).equal(true);
45 | expect(spyUpdated.args[0][0].get("test")).equal(undefined);
46 |
47 | expect(el).attr("test").to.equal("bar");
48 | expect(el.test).to.deep.equal("bar");
49 | });
50 |
51 | it(`should call changed once, updated once when prop changes twice in same tick`, async () => {
52 | class Test extends Wafer {
53 | static props = {
54 | test: {
55 | type: String,
56 | reflect: true,
57 | initial: "foo",
58 | },
59 | };
60 | }
61 | customElements.define(`wafer-test-1`, Test);
62 |
63 | /**
64 | * @type {Test}
65 | */
66 | const el = new Test();
67 | document.body.append(el);
68 | await el.updateDone();
69 |
70 | const spyChanged = sinon.spy(el, "changed");
71 | const spyUpdated = sinon.spy(el, "updated");
72 |
73 | el.test = "baz";
74 | el.test = "bar";
75 |
76 | await el.updateDone();
77 |
78 | expect(spyChanged).to.have.callCount(1);
79 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
80 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
81 | expect(spyChanged.args[0][0].size).equal(1);
82 | expect(spyChanged.args[0][0].has("test")).equal(true);
83 | expect(spyChanged.args[0][0].get("test")).equal("foo");
84 |
85 | expect(spyUpdated).to.have.callCount(1);
86 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
87 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
88 | expect(spyUpdated.args[0][0].size).equal(1);
89 | expect(spyUpdated.args[0][0].has("test")).equal(true);
90 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
91 |
92 | expect(el).attr("test").to.equal("bar");
93 | expect(el.test).to.deep.equal("bar");
94 | });
95 |
96 | it(`should call changed twice, updated once when prop changes in response to changed prop`, async () => {
97 | class Test extends Wafer {
98 | static props = {
99 | test: {
100 | type: String,
101 | reflect: true,
102 | initial: "foo",
103 | },
104 | };
105 |
106 | changed(changed) {
107 | if (changed.has("test") && this.test === "baz") {
108 | this.test = "bar";
109 | }
110 | }
111 | }
112 | customElements.define(`wafer-test-2`, Test);
113 |
114 | /**
115 | * @type {Test}
116 | */
117 | const el = new Test();
118 | document.body.append(el);
119 | await el.updateDone();
120 |
121 | const spyChanged = sinon.spy(el, "changed");
122 | const spyUpdated = sinon.spy(el, "updated");
123 |
124 | el.test = "baz";
125 |
126 | await el.updateDone();
127 |
128 | expect(spyChanged).to.have.callCount(2);
129 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
130 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
131 | expect(spyChanged.args[0][0].size).equal(1);
132 | expect(spyChanged.args[0][0].has("test")).equal(true);
133 | expect(spyChanged.args[0][0].get("test")).equal("foo");
134 |
135 | expect(spyChanged.args[1][0].size).equal(1);
136 | expect(spyChanged.args[1][0].has("test")).equal(true);
137 | expect(spyChanged.args[1][0].get("test")).equal("baz");
138 |
139 | expect(spyUpdated).to.have.callCount(1);
140 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
141 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
142 | expect(spyUpdated.args[0][0].size).equal(1);
143 | expect(spyUpdated.args[0][0].has("test")).equal(true);
144 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
145 |
146 | expect(el).attr("test").to.equal("bar");
147 | expect(el.test).to.deep.equal("bar");
148 | });
149 |
150 | it(`should not call changed/updated if prop changes result in original value`, async () => {
151 | class Test extends Wafer {
152 | static props = {
153 | test: {
154 | type: String,
155 | reflect: true,
156 | initial: "foo",
157 | },
158 | };
159 | }
160 | customElements.define(`wafer-test-3`, Test);
161 |
162 | /**
163 | * @type {Test}
164 | */
165 | const el = new Test();
166 | document.body.append(el);
167 | await el.updateDone();
168 |
169 | const spyChanged = sinon.spy(el, "changed");
170 | const spyUpdated = sinon.spy(el, "updated");
171 |
172 | el.test = "foo";
173 | el.test = "bar";
174 | el.test = "baz";
175 | el.test = "foo";
176 |
177 | await el.updateDone();
178 |
179 | expect(spyChanged).to.have.callCount(0);
180 | expect(spyUpdated).to.have.callCount(0);
181 |
182 | expect(el).attr("test").to.equal("foo");
183 | expect(el.test).to.deep.equal("foo");
184 | });
185 |
186 | it(`should call changed/updated twice if prop changes in updated`, async () => {
187 | class Test extends Wafer {
188 | static props = {
189 | test: {
190 | type: String,
191 | reflect: true,
192 | initial: "foo",
193 | },
194 | };
195 |
196 | updated(updated) {
197 | if (updated.has("test") && this.test === "bar") {
198 | this.test = "baz";
199 | }
200 | }
201 | }
202 | customElements.define(`wafer-test-4`, Test);
203 |
204 | /**
205 | * @type {Test}
206 | */
207 | const el = new Test();
208 | document.body.append(el);
209 | await el.updateDone();
210 |
211 | const spyChanged = sinon.spy(el, "changed");
212 | const spyUpdated = sinon.spy(el, "updated");
213 |
214 | el.test = "bar";
215 |
216 | await el.updateDone();
217 |
218 | expect(spyChanged).to.have.callCount(2);
219 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
220 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
221 | expect(spyChanged.args[0][0].size).equal(1);
222 | expect(spyChanged.args[0][0].has("test")).equal(true);
223 | expect(spyChanged.args[0][0].get("test")).equal("foo");
224 |
225 | expect(spyChanged.args[1][0].size).equal(1);
226 | expect(spyChanged.args[1][0].has("test")).equal(true);
227 | expect(spyChanged.args[1][0].get("test")).equal("bar");
228 |
229 | expect(spyUpdated).to.have.callCount(2);
230 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
231 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
232 | expect(spyUpdated.args[0][0].size).equal(1);
233 | expect(spyUpdated.args[0][0].has("test")).equal(true);
234 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
235 |
236 | expect(spyUpdated.args[1][0].size).equal(1);
237 | expect(spyUpdated.args[1][0].has("test")).equal(true);
238 | expect(spyUpdated.args[1][0].get("test")).equal("bar");
239 |
240 | expect(el).attr("test").to.equal("baz");
241 | expect(el.test).to.deep.equal("baz");
242 | });
243 |
244 | it(`should not update if prop name returned from changed`, async () => {
245 | class Test extends Wafer {
246 | static props = {
247 | test: {
248 | type: String,
249 | reflect: true,
250 | initial: "foo",
251 | },
252 | };
253 |
254 | changed(changed) {
255 | return ["test"];
256 | }
257 | }
258 | customElements.define(`wafer-test-5`, Test);
259 |
260 | /**
261 | * @type {Test}
262 | */
263 | const el = new Test();
264 | document.body.append(el);
265 | await el.updateDone();
266 |
267 | const spyChanged = sinon.spy(el, "changed");
268 | const spyUpdated = sinon.spy(el, "updated");
269 |
270 | el.test = "bar";
271 |
272 | await el.updateDone();
273 |
274 | expect(spyChanged).to.have.callCount(1);
275 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
276 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
277 | expect(spyChanged.args[0][0].size).equal(1);
278 | expect(spyChanged.args[0][0].has("test")).equal(true);
279 | expect(spyChanged.args[0][0].get("test")).equal("foo");
280 |
281 | expect(spyUpdated).to.have.callCount(0);
282 |
283 | expect(el).attr("test").to.equal("bar");
284 | expect(el.test).to.deep.equal("bar");
285 | });
286 |
287 | it(`should trigger declared triggers on prop change`, async () => {
288 | class Test extends Wafer {
289 | static props = {
290 | test: {
291 | type: String,
292 | reflect: true,
293 | initial: "foo",
294 | triggers: ["test2"],
295 | },
296 | test2: {
297 | type: String,
298 | initial: "baz",
299 | reflect: true,
300 | },
301 | };
302 | }
303 | customElements.define(`wafer-test-6`, Test);
304 |
305 | /**
306 | * @type {Test}
307 | */
308 | const el = new Test();
309 | document.body.append(el);
310 | await el.updateDone();
311 |
312 | const spyChanged = sinon.spy(el, "changed");
313 | const spyUpdated = sinon.spy(el, "updated");
314 | const spyUpdateTargets = sinon.spy(el, "updateTargets");
315 |
316 | el.test = "bar";
317 |
318 | await el.updateDone();
319 |
320 | expect(spyChanged).to.have.callCount(1);
321 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
322 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
323 | expect(spyChanged.args[0][0].size).equal(1);
324 | expect(spyChanged.args[0][0].has("test")).equal(true);
325 | expect(spyChanged.args[0][0].get("test")).equal("foo");
326 |
327 | expect(spyUpdated).to.have.callCount(1);
328 |
329 | expect(spyUpdated.args[0][0].size).equal(1);
330 | expect(spyUpdated.args[0][0].has("test")).equal(true);
331 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
332 |
333 | expect(spyUpdateTargets).to.have.callCount(2);
334 | expect(spyUpdateTargets.args[0][0]).equal("test");
335 | expect(spyUpdateTargets.args[1][0]).equal("test2");
336 |
337 | expect(el).attr("test").to.equal("bar");
338 | expect(el.test).to.equal("bar");
339 |
340 | expect(el).attr("test2").to.equal("baz");
341 | expect(el.test2).to.equal("baz");
342 | });
343 |
344 | it(`should do nothing on requestUpdate(null)`, async () => {
345 | class Test extends Wafer {
346 | static props = {
347 | test: {
348 | type: String,
349 | reflect: true,
350 | initial: "foo",
351 | },
352 | };
353 | }
354 | customElements.define(`wafer-test-7`, Test);
355 |
356 | /**
357 | * @type {Test}
358 | */
359 | const el = new Test();
360 | document.body.append(el);
361 |
362 | el.test = "bar";
363 | await el.updateDone();
364 |
365 | const spyChanged = sinon.spy(el, "changed");
366 | const spyUpdated = sinon.spy(el, "updated");
367 | const spyUpdateTargets = sinon.spy(el, "updateTargets");
368 |
369 | await el.requestUpdate(null);
370 |
371 | expect(spyChanged).to.have.callCount(0);
372 | expect(spyUpdated).to.have.callCount(0);
373 | expect(spyUpdateTargets).to.have.callCount(0);
374 |
375 | expect(el).attr("test").to.equal("bar");
376 | expect(el.test).to.equal("bar");
377 | });
378 |
379 | it(`should force update on default requestUpdate`, async () => {
380 | class Test extends Wafer {
381 | static props = {
382 | test: {
383 | type: String,
384 | reflect: true,
385 | initial: "foo",
386 | },
387 | test2: {
388 | type: String,
389 | reflect: true,
390 | initial: "baz",
391 | },
392 | };
393 | }
394 | customElements.define(`wafer-test-8`, Test);
395 |
396 | /**
397 | * @type {Test}
398 | */
399 | const el = new Test();
400 | document.body.append(el);
401 |
402 | el.test = "bar";
403 | await el.updateDone();
404 |
405 | const spyChanged = sinon.spy(el, "changed");
406 | const spyUpdated = sinon.spy(el, "updated");
407 | const spyUpdateTargets = sinon.spy(el, "updateTargets");
408 |
409 | await el.requestUpdate([]);
410 | await el.updateDone();
411 |
412 | expect(spyChanged).to.have.callCount(1);
413 | expect(spyChanged.args[0][0].size).equal(2);
414 | expect(spyChanged.args[0][0].has("test")).equal(true);
415 | expect(spyChanged.args[0][0].get("test")).equal("bar");
416 | expect(spyChanged.args[0][0].has("test2")).equal(true);
417 | expect(spyChanged.args[0][0].get("test2")).equal("baz");
418 |
419 | expect(spyUpdated).to.have.callCount(1);
420 | expect(spyUpdated.args[0][0].size).equal(2);
421 | expect(spyUpdated.args[0][0].has("test")).equal(true);
422 | expect(spyUpdated.args[0][0].get("test")).equal("bar");
423 | expect(spyUpdated.args[0][0].has("test2")).equal(true);
424 | expect(spyUpdated.args[0][0].get("test2")).equal("baz");
425 |
426 | expect(spyUpdateTargets).to.have.callCount(2);
427 | expect(spyUpdateTargets.args[0][0]).equal("test");
428 | expect(spyUpdateTargets.args[1][0]).equal("test2");
429 |
430 | expect(el).attr("test").to.equal("bar");
431 | expect(el.test).to.equal("bar");
432 |
433 | expect(el).attr("test2").to.equal("baz");
434 | expect(el.test2).to.equal("baz");
435 | });
436 |
437 | it(`should force update with passed prop only on requestUpdate`, async () => {
438 | class Test extends Wafer {
439 | static props = {
440 | test: {
441 | type: String,
442 | reflect: true,
443 | initial: "foo",
444 | },
445 | test2: {
446 | type: String,
447 | reflect: true,
448 | initial: "baz",
449 | },
450 | };
451 | }
452 | customElements.define(`wafer-test-9`, Test);
453 |
454 | /**
455 | * @type {Test}
456 | */
457 | const el = new Test();
458 | document.body.append(el);
459 |
460 | el.test = "bar";
461 | await el.updateDone();
462 |
463 | const spyChanged = sinon.spy(el, "changed");
464 | const spyUpdated = sinon.spy(el, "updated");
465 | const spyUpdateTargets = sinon.spy(el, "updateTargets");
466 |
467 | await el.requestUpdate(["test"]);
468 | await el.updateDone();
469 |
470 | expect(spyChanged).to.have.callCount(1);
471 | expect(spyChanged.args[0][0].size).equal(1);
472 | expect(spyChanged.args[0][0].has("test")).equal(true);
473 | expect(spyChanged.args[0][0].get("test")).equal("bar");
474 |
475 | expect(spyUpdated).to.have.callCount(1);
476 | expect(spyUpdated.args[0][0].size).equal(1);
477 | expect(spyUpdated.args[0][0].has("test")).equal(true);
478 | expect(spyUpdated.args[0][0].get("test")).equal("bar");
479 |
480 | expect(spyUpdateTargets).to.have.callCount(1);
481 | expect(spyUpdateTargets.args[0][0]).equal("test");
482 |
483 | expect(el).attr("test").to.equal("bar");
484 | expect(el.test).to.equal("bar");
485 |
486 | expect(el).attr("test2").to.equal("baz");
487 | expect(el.test2).to.equal("baz");
488 | });
489 |
490 | it(`updateDone should wait if updated returns promise that updates a property in updated`, async () => {
491 | class Test extends Wafer {
492 | static props = {
493 | test: {
494 | type: String,
495 | reflect: true,
496 | initial: "foo",
497 | },
498 | };
499 |
500 | updated(changed) {
501 | if (changed.has("test") && this.test === "bar") {
502 | return new Promise((resolve) => {
503 | setTimeout(() => {
504 | this.test = "baz";
505 | resolve();
506 | }, 100);
507 | });
508 | }
509 | }
510 | }
511 | customElements.define(`wafer-test-10`, Test);
512 |
513 | /**
514 | * @type {Test}
515 | */
516 | const el = new Test();
517 |
518 | document.body.append(el);
519 | await el.updateDone();
520 |
521 | const spyUpdate = sinon.spy(el, "update");
522 |
523 | el.test = "bar";
524 |
525 | await el.updateDone();
526 |
527 | expect(spyUpdate).to.have.callCount(2);
528 | expect(el.test).to.equal("baz");
529 | });
530 | });
531 |
--------------------------------------------------------------------------------
/test/client/events.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../src/wafer.js";
5 |
6 | describe("Wafer handles events", () => {
7 | it("binds events to element by default in Shadow DOM", async () => {
8 | class Test extends Wafer {
9 | static template = "";
10 | static props = {
11 | count: {
12 | type: Number,
13 | initial: 0,
14 | },
15 | };
16 |
17 | get events() {
18 | return {
19 | $button: {
20 | click: () => {
21 | this.count++;
22 | },
23 | },
24 | };
25 | }
26 | }
27 | customElements.define("wafer-test-0", Test);
28 |
29 | /**
30 | * @type {Test}
31 | */
32 | const el = await fixture("");
33 | const button = el.shadowRoot.querySelector("button");
34 |
35 | expect(el.count).to.equal(0);
36 |
37 | button.click();
38 |
39 | await el.updateDone();
40 |
41 | expect(el.count).to.equal(1);
42 |
43 | button.click();
44 |
45 | await el.updateDone();
46 |
47 | expect(el.count).to.equal(2);
48 | });
49 |
50 | it("binds events to element by default in light DOM", async () => {
51 | class Test extends Wafer {
52 | static template = "";
53 | static props = {
54 | count: {
55 | type: Number,
56 | initial: 0,
57 | },
58 | };
59 |
60 | constructor() {
61 | super({ shadow: false });
62 | }
63 |
64 | get events() {
65 | return {
66 | button: {
67 | click: () => {
68 | this.count++;
69 | },
70 | },
71 | };
72 | }
73 | }
74 | customElements.define("wafer-test-1", Test);
75 |
76 | /**
77 | * @type {Test}
78 | */
79 | const el = await fixture("");
80 | const button = el.querySelector("button");
81 |
82 | expect(el.count).to.equal(0);
83 |
84 | button.click();
85 |
86 | await el.updateDone();
87 |
88 | expect(el.count).to.equal(1);
89 |
90 | button.click();
91 |
92 | await el.updateDone();
93 |
94 | expect(el.count).to.equal(2);
95 | });
96 |
97 | it("can pass event options", async () => {
98 | class Test extends Wafer {
99 | static template = "";
100 | static props = {
101 | count: {
102 | type: Number,
103 | initial: 0,
104 | },
105 | };
106 |
107 | get events() {
108 | return {
109 | $button: {
110 | click: {
111 | fn: () => {
112 | this.count++;
113 | },
114 | opts: {
115 | once: true,
116 | },
117 | },
118 | },
119 | };
120 | }
121 | }
122 | customElements.define("wafer-test-2", Test);
123 |
124 | /**
125 | * @type {Test}
126 | */
127 | const el = await fixture("");
128 | const button = el.shadowRoot.querySelector("button");
129 |
130 | expect(el.count).to.equal(0);
131 |
132 | button.click();
133 |
134 | await el.updateDone();
135 |
136 | expect(el.count).to.equal(1);
137 |
138 | button.click();
139 |
140 | await el.updateDone();
141 |
142 | expect(el.count).to.equal(1);
143 | });
144 | });
145 |
--------------------------------------------------------------------------------
/test/client/helpers.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect, oneEvent } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../src/wafer.js";
5 | import { emit, repeat } from "../../src/dom.js";
6 |
7 | describe("Wafer DOM", () => {
8 | it("emit fires custom events with defaults", async () => {
9 | class Test extends Wafer {
10 | static template = "";
11 | static props = {
12 | count: {
13 | type: Number,
14 | initial: 0,
15 | },
16 | };
17 |
18 | get events() {
19 | return {
20 | $button: {
21 | click: () => {
22 | emit(this, "test-event");
23 | },
24 | },
25 | };
26 | }
27 | }
28 | customElements.define("wafer-test-0", Test);
29 |
30 | /**
31 | * @type {Test}
32 | */
33 | const el = await fixture("");
34 | const button = el.shadowRoot.querySelector("button");
35 |
36 | const listener = oneEvent(el, "test-event");
37 |
38 | button.click();
39 |
40 | const { detail, composed, bubbles } = await listener;
41 |
42 | expect(detail).to.deep.equal({});
43 | expect(composed).to.be.true;
44 | expect(bubbles).to.be.true;
45 | });
46 |
47 | it("emit fires custom events with parameters", async () => {
48 | class Test extends Wafer {
49 | static template = "";
50 | static props = {
51 | count: {
52 | type: Number,
53 | initial: 0,
54 | },
55 | };
56 |
57 | get events() {
58 | return {
59 | $button: {
60 | click: () => {
61 | emit(
62 | this,
63 | "test-event",
64 | { foo: "bar" },
65 | {
66 | bubbles: false,
67 | composed: false,
68 | }
69 | );
70 | },
71 | },
72 | };
73 | }
74 | }
75 | customElements.define("wafer-test-1", Test);
76 |
77 | /**
78 | * @type {Test}
79 | */
80 | const el = await fixture("");
81 | const button = el.shadowRoot.querySelector("button");
82 |
83 | const listener = oneEvent(el, "test-event");
84 |
85 | button.click();
86 |
87 | const { detail, composed, bubbles } = await listener;
88 |
89 | expect(detail).to.deep.equal({ foo: "bar" });
90 | expect(composed).to.be.false;
91 | expect(bubbles).to.be.false;
92 | });
93 |
94 | it("can render elements in container with repeat", async () => {
95 | class Test extends Wafer {
96 | static template = "";
97 | static props = {
98 | items: {
99 | type: Array,
100 | initial: [1, 2, 3],
101 | targets: [
102 | {
103 | selector: "$div",
104 | dom: (targetEl, items, el) => {
105 | return repeat({
106 | container: targetEl,
107 | items,
108 | html: "",
109 | keyFn: (item) => item,
110 | targets: [
111 | {
112 | selector: "self",
113 | text: true,
114 | },
115 | ],
116 | });
117 | },
118 | },
119 | ],
120 | },
121 | };
122 | }
123 | customElements.define("wafer-test-2", Test);
124 |
125 | /**
126 | * @type {Test}
127 | */
128 | const el = await fixture("");
129 |
130 | expect(el).shadowDom.to.equal(`
131 |
132 | 1
133 | 2
134 | 3
135 |
136 | `);
137 | });
138 |
139 | it("can remove elements in container with repeat", async () => {
140 | class Test extends Wafer {
141 | static template = "";
142 | static props = {
143 | items: {
144 | type: Array,
145 | initial: [1, 2, 3],
146 | targets: [
147 | {
148 | selector: "$div",
149 | dom: (targetEl, items, el) => {
150 | return repeat({
151 | container: targetEl,
152 | items,
153 | html: "",
154 | keyFn: (item) => item,
155 | targets: [
156 | {
157 | selector: "self",
158 | text: true,
159 | },
160 | ],
161 | });
162 | },
163 | },
164 | ],
165 | },
166 | };
167 | }
168 | customElements.define("wafer-test-3", Test);
169 |
170 | /**
171 | * @type {Test}
172 | */
173 | const el = await fixture("");
174 |
175 | expect(el).shadowDom.to.equal(`
176 |
177 | 1
178 | 2
179 | 3
180 |
181 | `);
182 |
183 | el.items = [1, 2];
184 |
185 | await el.updateDone();
186 |
187 | expect(el).shadowDom.to.equal(`
188 |
189 | 1
190 | 2
191 |
192 | `);
193 | });
194 |
195 | it("can update elements in container with repeat", async () => {
196 | class Test extends Wafer {
197 | static template = "";
198 | static props = {
199 | items: {
200 | type: Array,
201 | initial: [1, 2, 3],
202 | targets: [
203 | {
204 | selector: "$div",
205 | dom: (targetEl, items, el) => {
206 | return repeat({
207 | container: targetEl,
208 | items,
209 | html: "",
210 | keyFn: (item) => item,
211 | targets: [
212 | {
213 | selector: "self",
214 | text: true,
215 | },
216 | ],
217 | });
218 | },
219 | },
220 | ],
221 | },
222 | };
223 | }
224 | customElements.define("wafer-test-4", Test);
225 |
226 | /**
227 | * @type {Test}
228 | */
229 | const el = await fixture("");
230 |
231 | expect(el).shadowDom.to.equal(`
232 |
233 | 1
234 | 2
235 | 3
236 |
237 | `);
238 |
239 | el.items = [4, 3, 5, 2, 6, 1];
240 |
241 | await el.updateDone();
242 |
243 | expect(el).shadowDom.to.equal(`
244 |
245 | 4
246 | 3
247 | 5
248 | 2
249 | 6
250 | 1
251 |
252 | `);
253 |
254 | el.items = [3, 4, 5, 2, 6, 1];
255 |
256 | await el.updateDone();
257 |
258 | expect(el).shadowDom.to.equal(`
259 |
260 | 3
261 | 4
262 | 5
263 | 2
264 | 6
265 | 1
266 |
267 | `);
268 | });
269 |
270 | it("can add new elements in container with repeat", async () => {
271 | class Test extends Wafer {
272 | static template = "";
273 | static props = {
274 | items: {
275 | type: Array,
276 | initial: [1, 2, 3],
277 | targets: [
278 | {
279 | selector: "$div",
280 | dom: (targetEl, items, el) => {
281 | return repeat({
282 | container: targetEl,
283 | items,
284 | html: "",
285 | keyFn: (item) => item,
286 | targets: [
287 | {
288 | selector: "self",
289 | text: true,
290 | },
291 | ],
292 | });
293 | },
294 | },
295 | ],
296 | },
297 | };
298 | }
299 | customElements.define("wafer-test-5", Test);
300 |
301 | /**
302 | * @type {Test}
303 | */
304 | const el = await fixture("");
305 |
306 | expect(el).shadowDom.to.equal(`
307 |
308 | 1
309 | 2
310 | 3
311 |
312 | `);
313 |
314 | el.items = [1, 2, 3, 4];
315 |
316 | await el.updateDone();
317 |
318 | expect(el).shadowDom.to.equal(`
319 |
320 | 1
321 | 2
322 | 3
323 | 4
324 |
325 | `);
326 | });
327 |
328 | it("can bind events to elements in repeat", async () => {
329 | class Test extends Wafer {
330 | static template = "";
331 | static props = {
332 | count: {
333 | type: Number,
334 | initial: 0,
335 | },
336 | items: {
337 | type: Array,
338 | initial: [1, 2, 3],
339 | targets: [
340 | {
341 | selector: "$div",
342 | dom: (targetEl, items, el) => {
343 | return repeat({
344 | container: targetEl,
345 | items,
346 | html: "
",
347 | keyFn: (item) => item,
348 | targets: [
349 | {
350 | selector: "span",
351 | text: true,
352 | },
353 | ],
354 | events: {
355 | button: {
356 | click: () => el.count++,
357 | },
358 | },
359 | });
360 | },
361 | },
362 | ],
363 | },
364 | };
365 | }
366 | customElements.define("wafer-test-6", Test);
367 |
368 | /**
369 | * @type {Test}
370 | */
371 | const el = await fixture("");
372 |
373 | expect(el).shadowDom.to.equal(`
374 |
375 |
1
376 |
2
377 |
3
378 |
379 | `);
380 |
381 | el.shadowRoot.querySelector("button").click();
382 | el.shadowRoot.querySelector("button").click();
383 |
384 | await el.updateDone();
385 |
386 | expect(el.count).to.equal(2);
387 | });
388 |
389 | it("runs init function only when adding element", async () => {
390 | class Test extends Wafer {
391 | static template = "";
392 | static props = {
393 | items: {
394 | type: Array,
395 | initial: [1, 2, 3],
396 | targets: [
397 | {
398 | selector: "$:scope>div",
399 | dom: (targetEl, items, self) => {
400 | return repeat({
401 | container: targetEl,
402 | items,
403 | html: "
",
404 | keyFn: (item) => item,
405 | targets: [
406 | {
407 | selector: ":scope>span",
408 | text: true,
409 | },
410 | ],
411 | init: (el, item, index) => {
412 | const div = document.createElement("div");
413 | div.textContent = `d${item} (${index})`;
414 | el.append(div);
415 | },
416 | });
417 | },
418 | },
419 | ],
420 | },
421 | };
422 | }
423 | customElements.define("wafer-test-7", Test);
424 |
425 | /**
426 | * @type {Test}
427 | */
428 | const el = await fixture("");
429 |
430 | expect(el).shadowDom.to.equal(`
431 |
436 | `);
437 |
438 | el.items = [4, 3, 5, 2, 6, 1];
439 | await el.updateDone();
440 |
441 | expect(el).shadowDom.to.equal(`
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 | `);
451 | });
452 |
453 | it("can re-add elements in container with repeat", async () => {
454 | class Test extends Wafer {
455 | static template = "";
456 | static props = {
457 | items: {
458 | type: Array,
459 | initial: [1, 2, 3],
460 | targets: [
461 | {
462 | selector: "$div",
463 | dom: (targetEl, items, el) => {
464 | return repeat({
465 | container: targetEl,
466 | items,
467 | html: "",
468 | keyFn: (item) => item,
469 | targets: [
470 | {
471 | selector: "self",
472 | text: true,
473 | },
474 | ],
475 | });
476 | },
477 | },
478 | ],
479 | },
480 | };
481 | }
482 | customElements.define("wafer-test-8", Test);
483 |
484 | /**
485 | * @type {Test}
486 | */
487 | const el = await fixture("");
488 |
489 | expect(el).shadowDom.to.equal(`
490 |
491 | 1
492 | 2
493 | 3
494 |
495 | `);
496 |
497 | el.items = [3];
498 |
499 | await el.updateDone();
500 |
501 | expect(el).shadowDom.to.equal(`
502 |
503 | 3
504 |
505 | `);
506 |
507 | el.items = [1, 2, 3];
508 |
509 | await el.updateDone();
510 |
511 | expect(el).shadowDom.to.equal(`
512 |
513 | 1
514 | 2
515 | 3
516 |
517 | `);
518 | });
519 |
520 | it("can re-add elements in container with repeat (random)", async () => {
521 | const start = [5, 8, 9, 45, 33, 12, 1, 99, 74, 53, 77];
522 | const middle = start.sort((a, b) => b - a).slice(2, 4);
523 | const end = [5, 9, 33, 22, 12, 134, 1, 99, 53, 45, 77, 8];
524 |
525 | class Test extends Wafer {
526 | static template = "";
527 | static props = {
528 | items: {
529 | type: Array,
530 | initial: start,
531 | targets: [
532 | {
533 | selector: "$div",
534 | dom: (targetEl, items, el) => {
535 | return repeat({
536 | container: targetEl,
537 | items,
538 | html: "",
539 | keyFn: (item) => item,
540 | targets: [
541 | {
542 | selector: "self",
543 | text: true,
544 | },
545 | ],
546 | });
547 | },
548 | },
549 | ],
550 | },
551 | };
552 | }
553 | customElements.define("wafer-test-9", Test);
554 |
555 | /**
556 | * @type {Test}
557 | */
558 | const el = await fixture("");
559 |
560 | expect(el).shadowDom.to.equal(`
561 |
562 | ${start
563 | .map((item) => {
564 | return `${item}`;
565 | })
566 | .join("")}
567 |
568 | `);
569 |
570 | el.items = middle;
571 | await el.updateDone();
572 |
573 | expect(el).shadowDom.to.equal(`
574 |
575 | ${middle
576 | .map((item) => {
577 | return `${item}`;
578 | })
579 | .join("")}
580 |
581 | `);
582 |
583 | el.items = end;
584 | await el.updateDone();
585 |
586 | expect(el).shadowDom.to.equal(`
587 |
588 | ${end
589 | .map((item) => {
590 | return `${item}`;
591 | })
592 | .join("")}
593 |
594 | `);
595 | });
596 | });
597 |
--------------------------------------------------------------------------------
/test/client/instantiation/defined-after-created-with-createelement.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/wafer.js";
5 |
6 | class Test extends Wafer {
7 | static props = {
8 | test: {
9 | type: String,
10 | reflect: true,
11 | initial: "foo",
12 | },
13 | };
14 | }
15 |
16 | describe("Wafer sets attributes and properties on element when defined before created", () => {
17 | it(`initialises props`, async () => {
18 | /**
19 | * @type {Test}
20 | */
21 | const el = document.createElement("wafer-test");
22 | customElements.define(`wafer-test`, Test);
23 | await customElements.whenDefined(`wafer-test`);
24 | customElements.upgrade(el);
25 |
26 | const spyChanged = sinon.spy(el, "changed");
27 | const spyUpdated = sinon.spy(el, "updated");
28 |
29 | el.test = "bar";
30 |
31 | await el.updateDone();
32 | document.body.appendChild(el);
33 |
34 | expect(spyChanged).to.have.callCount(1);
35 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
36 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
37 | expect(spyChanged.args[0][0].size).equal(1);
38 | expect(spyChanged.args[0][0].has("test")).equal(true);
39 |
40 | expect(spyUpdated).to.have.callCount(1);
41 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
42 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
43 | expect(spyUpdated.args[0][0].size).equal(1);
44 | expect(spyUpdated.args[0][0].has("test")).equal(true);
45 |
46 | expect(el).attr("test").to.equal("bar");
47 | expect(el.test).to.deep.equal("bar");
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/client/instantiation/defined-after-created-with-innerhtml.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/wafer.js";
5 |
6 | describe("Wafer sets attributes and properties on element when defined before created", () => {
7 | class Test extends Wafer {
8 | static props = {
9 | test: {
10 | type: String,
11 | reflect: true,
12 | initial: "foo",
13 | },
14 | };
15 | }
16 |
17 | it(`initialises props`, async () => {
18 | const div = document.createElement("div");
19 | div.innerHTML = "";
20 |
21 | /**
22 | * @type {Test}
23 | */
24 | const el = div.querySelector("wafer-test");
25 |
26 | customElements.define(`wafer-test`, Test);
27 | await customElements.whenDefined(`wafer-test`);
28 | customElements.upgrade(el);
29 |
30 | const spyChanged = sinon.spy(el, "changed");
31 | const spyUpdated = sinon.spy(el, "updated");
32 |
33 | el.test = "bar";
34 |
35 | await el.updateDone();
36 | document.body.appendChild(el);
37 |
38 | expect(spyChanged).to.have.callCount(1);
39 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
40 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
41 | expect(spyChanged.args[0][0].size).equal(1);
42 | expect(spyChanged.args[0][0].has("test")).equal(true);
43 | expect(spyChanged.args[0][0].get("test")).equal(undefined);
44 |
45 | expect(spyUpdated).to.have.callCount(1);
46 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
47 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
48 | expect(spyUpdated.args[0][0].size).equal(1);
49 | expect(spyUpdated.args[0][0].has("test")).equal(true);
50 | expect(spyUpdated.args[0][0].get("test")).equal(undefined);
51 |
52 | expect(el).attr("test").to.equal("bar");
53 | expect(el.test).to.deep.equal("bar");
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/test/client/instantiation/defined-after-created-with-new.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/wafer.js";
5 |
6 | describe("Wafer sets attributes and properties on element when defined before created", () => {
7 | it(`initialises props`, async () => {
8 | class Test extends Wafer {
9 | static props = {
10 | test: {
11 | type: String,
12 | reflect: true,
13 | initial: "foo",
14 | },
15 | };
16 | }
17 | customElements.define(`wafer-test`, Test);
18 |
19 | /**
20 | * @type {Test}
21 | */
22 | const el = new Test();
23 |
24 | const spyChanged = sinon.spy(el, "changed");
25 | const spyUpdated = sinon.spy(el, "updated");
26 |
27 | el.test = "bar";
28 |
29 | await el.updateDone();
30 | document.body.appendChild(el);
31 |
32 | expect(spyChanged).to.have.callCount(1);
33 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
34 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
35 | expect(spyChanged.args[0][0].size).equal(1);
36 | expect(spyChanged.args[0][0].has("test")).equal(true);
37 | expect(spyChanged.args[0][0].get("test")).equal(undefined);
38 |
39 | expect(spyUpdated).to.have.callCount(1);
40 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
41 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
42 | expect(spyUpdated.args[0][0].size).equal(1);
43 | expect(spyUpdated.args[0][0].has("test")).equal(true);
44 | expect(spyUpdated.args[0][0].get("test")).equal(undefined);
45 |
46 | expect(el).attr("test").to.equal("bar");
47 | expect(el.test).to.deep.equal("bar");
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/client/instantiation/defined-after-dom-all-types.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/wafer.js";
5 | import configs from "../../configs.js";
6 |
7 | describe("Wafer sets attributes and properties on element when defined after DOM", () => {
8 | for (const [configIndex, config] of configs.entries()) {
9 | for (const [testIndex, test] of config.tests.entries()) {
10 | let itFunc = it;
11 | if (test.only) {
12 | itFunc = it.only;
13 | }
14 |
15 | itFunc(
16 | `with ${test.html(`${configIndex}-${testIndex}`).trim()} (${
17 | config.description
18 | }, ${test.description})`,
19 | async () => {
20 | class Test extends Wafer {
21 | static props = config.props;
22 | }
23 |
24 | const attrName = config.props.test.attributeName || "test";
25 |
26 | /**
27 | * @type {Test}
28 | */
29 | const el = await fixture(test.html(`${configIndex}-${testIndex}`));
30 | customElements.define(`wafer-test-${configIndex}-${testIndex}`, Test);
31 |
32 | const spyChanged = sinon.spy(el, "changed");
33 | const spyUpdated = sinon.spy(el, "updated");
34 |
35 | await customElements.whenDefined(
36 | `wafer-test-${configIndex}-${testIndex}`
37 | );
38 |
39 | await el.updateDone();
40 |
41 | expect(spyChanged).to.have.callCount(test.expected.changed.length);
42 | for (const expected of test.expected.changed) {
43 | expect(spyChanged).calledWith(expected.value);
44 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
45 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
46 | expect(spyChanged.args[0][0].size).equal(expected.value.size);
47 |
48 | for (const [key, value] of expected.value) {
49 | expect(spyChanged.args[0][0].has(key)).equal(true);
50 | expect(spyChanged.args[0][0].get(key)).equal(value);
51 | }
52 | }
53 |
54 | expect(spyUpdated).to.have.callCount(test.expected.updated.length);
55 | for (const expected of test.expected.updated) {
56 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
57 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
58 | expect(spyUpdated.args[0][0].size).equal(expected.value.size);
59 |
60 | for (const [key, value] of expected.value) {
61 | expect(spyUpdated.args[0][0].has(key)).equal(true);
62 | expect(spyUpdated.args[0][0].get(key)).equal(value);
63 | }
64 | }
65 |
66 | if (test.expected.attribute !== null) {
67 | expect(el).attr(attrName).to.equal(test.expected.attribute);
68 | } else {
69 | expect(el).not.to.have.attr(attrName);
70 | }
71 | expect(el.test).to.deep.equal(test.expected.prop);
72 | }
73 | );
74 | }
75 | }
76 | });
77 |
--------------------------------------------------------------------------------
/test/client/instantiation/defined-before-created-with-createelement.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/wafer.js";
5 |
6 | describe("Wafer sets attributes and properties on element when defined before created", () => {
7 | class Test extends Wafer {
8 | static props = {
9 | test: {
10 | type: String,
11 | reflect: true,
12 | initial: "foo",
13 | },
14 | };
15 | }
16 | customElements.define(`wafer-test`, Test);
17 |
18 | it(`initialises props`, async () => {
19 | /**
20 | * @type {Test}
21 | */
22 | const el = document.createElement("wafer-test");
23 |
24 | const spyChanged = sinon.spy(el, "changed");
25 | const spyUpdated = sinon.spy(el, "updated");
26 |
27 | el.test = "bar";
28 |
29 | await el.updateDone();
30 | document.body.appendChild(el);
31 |
32 | expect(spyChanged).to.have.callCount(1);
33 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
34 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
35 | expect(spyChanged.args[0][0].size).equal(1);
36 | expect(spyChanged.args[0][0].has("test")).equal(true);
37 |
38 | expect(spyUpdated).to.have.callCount(1);
39 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
40 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
41 | expect(spyUpdated.args[0][0].size).equal(1);
42 | expect(spyUpdated.args[0][0].has("test")).equal(true);
43 |
44 | expect(el).attr("test").to.equal("bar");
45 | expect(el.test).to.deep.equal("bar");
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/test/client/instantiation/defined-before-created-with-innerhtml.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/wafer.js";
5 |
6 | describe("Wafer sets attributes and properties on element when defined before created", () => {
7 | class Test extends Wafer {
8 | static props = {
9 | test: {
10 | type: String,
11 | reflect: true,
12 | initial: "foo",
13 | },
14 | };
15 | }
16 | customElements.define(`wafer-test`, Test);
17 |
18 | it(`initialises props`, async () => {
19 | const div = document.createElement("div");
20 | div.innerHTML = "";
21 |
22 | /**
23 | * @type {Test}
24 | */
25 | const el = div.querySelector("wafer-test");
26 |
27 | const spyChanged = sinon.spy(el, "changed");
28 | const spyUpdated = sinon.spy(el, "updated");
29 |
30 | el.test = "bar";
31 |
32 | await el.updateDone();
33 | document.body.appendChild(el);
34 |
35 | expect(spyChanged).to.have.callCount(1);
36 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
37 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
38 | expect(spyChanged.args[0][0].size).equal(1);
39 | expect(spyChanged.args[0][0].has("test")).equal(true);
40 | expect(spyChanged.args[0][0].get("test")).equal(undefined);
41 |
42 | expect(spyUpdated).to.have.callCount(1);
43 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
44 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
45 | expect(spyUpdated.args[0][0].size).equal(1);
46 | expect(spyUpdated.args[0][0].has("test")).equal(true);
47 | expect(spyUpdated.args[0][0].get("test")).equal(undefined);
48 |
49 | expect(el).attr("test").to.equal("bar");
50 | expect(el.test).to.deep.equal("bar");
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/test/client/instantiation/defined-before-created-with-new.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/wafer.js";
5 |
6 | describe("Wafer sets attributes and properties on element when defined before created", () => {
7 | class Test extends Wafer {
8 | static props = {
9 | test: {
10 | type: String,
11 | reflect: true,
12 | initial: "foo",
13 | },
14 | };
15 | }
16 | customElements.define(`wafer-test`, Test);
17 |
18 | it(`initialises props`, async () => {
19 | /**
20 | * @type {Test}
21 | */
22 | const el = new Test();
23 |
24 | const spyChanged = sinon.spy(el, "changed");
25 | const spyUpdated = sinon.spy(el, "updated");
26 |
27 | el.test = "bar";
28 |
29 | await el.updateDone();
30 | document.body.appendChild(el);
31 |
32 | expect(spyChanged).to.have.callCount(1);
33 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
34 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
35 | expect(spyChanged.args[0][0].size).equal(1);
36 | expect(spyChanged.args[0][0].has("test")).equal(true);
37 | expect(spyChanged.args[0][0].get("test")).equal(undefined);
38 |
39 | expect(spyUpdated).to.have.callCount(1);
40 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
41 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
42 | expect(spyUpdated.args[0][0].size).equal(1);
43 | expect(spyUpdated.args[0][0].has("test")).equal(true);
44 | expect(spyUpdated.args[0][0].get("test")).equal(undefined);
45 |
46 | expect(el).attr("test").to.equal("bar");
47 | expect(el.test).to.deep.equal("bar");
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/client/instantiation/defined-before-dom-all-types.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/wafer.js";
5 | import configs from "../../configs.js";
6 |
7 | describe("Wafer sets attributes and properties on element when defined before DOM", () => {
8 | for (const [index, config] of configs.entries()) {
9 | class Test extends Wafer {
10 | static props = config.props;
11 | }
12 | customElements.define(`wafer-test-${index}`, Test);
13 |
14 | const attrName = config.props.test.attributeName || "test";
15 |
16 | for (const test of config.tests) {
17 | let itFunc = it;
18 | if (test.only) {
19 | itFunc = it.only;
20 | }
21 |
22 | itFunc(
23 | `with ${test.html(index).trim()} (${config.description}, ${
24 | test.description
25 | })`,
26 | async () => {
27 | /**
28 | * @type {Test}
29 | */
30 | const el = await fixture(test.html(index));
31 |
32 | // can't spy on changed/updated as they will have already been
33 | // called before we can spy on them
34 |
35 | await el.updateDone();
36 |
37 | if (test.expected.attribute !== null) {
38 | expect(el).attr(attrName).to.equal(test.expected.attribute);
39 | } else {
40 | expect(el).not.to.have.attr(attrName);
41 | }
42 | expect(el.test).to.deep.equal(test.expected.prop);
43 | }
44 | );
45 | }
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/test/client/instantiation/general.test.js:
--------------------------------------------------------------------------------
1 | import { fixture, expect } from "@open-wc/testing";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/wafer.js";
5 |
6 | describe("Wafer behaviour on instantiation", () => {
7 | it(`should update even if not connected`, async () => {
8 | class Test extends Wafer {
9 | static props = {
10 | test: {
11 | type: String,
12 | reflect: true,
13 | initial: "foo",
14 | },
15 | };
16 | }
17 | customElements.define(`wafer-test-0`, Test);
18 |
19 | /**
20 | * @type {Test}
21 | */
22 | const el = new Test();
23 |
24 | const spyChanged = sinon.spy(el, "changed");
25 | const spyUpdated = sinon.spy(el, "updated");
26 |
27 | el.test = "bar";
28 |
29 | await el.updateDone();
30 |
31 | expect(el._connectedOnce).to.equal(false);
32 |
33 | expect(spyChanged).to.have.callCount(1);
34 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
35 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
36 | expect(spyChanged.args[0][0].size).equal(1);
37 | expect(spyChanged.args[0][0].has("test")).equal(true);
38 | expect(spyChanged.args[0][0].get("test")).equal(undefined);
39 |
40 | expect(spyUpdated).to.have.callCount(1);
41 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
42 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
43 | expect(spyUpdated.args[0][0].size).equal(1);
44 | expect(spyUpdated.args[0][0].has("test")).equal(true);
45 | expect(spyUpdated.args[0][0].get("test")).equal(undefined);
46 |
47 | expect(el).attr("test").to.equal("bar");
48 | expect(el.test).to.deep.equal("bar");
49 | });
50 |
51 | it(`should update reflected property when attribute changes`, async () => {
52 | class Test extends Wafer {
53 | static props = {
54 | test: {
55 | type: String,
56 | reflect: true,
57 | initial: "foo",
58 | },
59 | };
60 | }
61 | customElements.define(`wafer-test-1`, Test);
62 |
63 | /**
64 | * @type {Test}
65 | */
66 | const el = await fixture("");
67 |
68 | const spyChanged = sinon.spy(el, "changed");
69 | const spyUpdated = sinon.spy(el, "updated");
70 |
71 | el.setAttribute("test", "bar");
72 |
73 | await el.updateDone();
74 |
75 | expect(spyChanged).to.have.callCount(1);
76 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
77 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
78 | expect(spyChanged.args[0][0].size).equal(1);
79 | expect(spyChanged.args[0][0].has("test")).equal(true);
80 | expect(spyChanged.args[0][0].get("test")).equal("foo");
81 |
82 | expect(spyUpdated).to.have.callCount(1);
83 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
84 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
85 | expect(spyUpdated.args[0][0].size).equal(1);
86 | expect(spyUpdated.args[0][0].has("test")).equal(true);
87 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
88 |
89 | expect(el).attr("test").to.equal("bar");
90 | expect(el.test).to.deep.equal("bar");
91 | });
92 |
93 | it(`should update un-reflected property when attribute changes`, async () => {
94 | class Test extends Wafer {
95 | static props = {
96 | test: {
97 | type: String,
98 | initial: "foo",
99 | },
100 | };
101 | }
102 | customElements.define(`wafer-test-2`, Test);
103 |
104 | /**
105 | * @type {Test}
106 | */
107 | const el = await fixture("");
108 |
109 | const spyChanged = sinon.spy(el, "changed");
110 | const spyUpdated = sinon.spy(el, "updated");
111 |
112 | el.setAttribute("test", "bar");
113 |
114 | await el.updateDone();
115 |
116 | expect(spyChanged).to.have.callCount(1);
117 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
118 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
119 | expect(spyChanged.args[0][0].size).equal(1);
120 | expect(spyChanged.args[0][0].has("test")).equal(true);
121 | expect(spyChanged.args[0][0].get("test")).equal("foo");
122 |
123 | expect(spyUpdated).to.have.callCount(1);
124 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
125 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
126 | expect(spyUpdated.args[0][0].size).equal(1);
127 | expect(spyUpdated.args[0][0].has("test")).equal(true);
128 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
129 |
130 | expect(el).attr("test").to.equal("bar");
131 | expect(el.test).to.deep.equal("bar");
132 | });
133 |
134 | it(`should not reflect un-reflected property to attribute`, async () => {
135 | class Test extends Wafer {
136 | static props = {
137 | test: {
138 | type: String,
139 | initial: "foo",
140 | },
141 | };
142 | }
143 | customElements.define(`wafer-test-3`, Test);
144 |
145 | /**
146 | * @type {Test}
147 | */
148 | const el = await fixture("");
149 |
150 | const spyChanged = sinon.spy(el, "changed");
151 | const spyUpdated = sinon.spy(el, "updated");
152 |
153 | el.test = "bar";
154 |
155 | await el.updateDone();
156 |
157 | expect(spyChanged).to.have.callCount(1);
158 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
159 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
160 | expect(spyChanged.args[0][0].size).equal(1);
161 | expect(spyChanged.args[0][0].has("test")).equal(true);
162 | expect(spyChanged.args[0][0].get("test")).equal("foo");
163 |
164 | expect(spyUpdated).to.have.callCount(1);
165 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
166 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
167 | expect(spyUpdated.args[0][0].size).equal(1);
168 | expect(spyUpdated.args[0][0].has("test")).equal(true);
169 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
170 |
171 | expect(el).not.to.have.attr("test");
172 | expect(el.test).to.deep.equal("bar");
173 | });
174 |
175 | it(`should not call _setFromAttribute when attribute is set to existing value when connected`, async () => {
176 | class Test extends Wafer {
177 | static props = {
178 | test: {
179 | type: String,
180 | initial: "foo",
181 | },
182 | };
183 | }
184 | customElements.define(`wafer-test-4`, Test);
185 |
186 | /**
187 | * @type {Test}
188 | */
189 | const el = await fixture("");
190 |
191 | const spySetFromAttribute = sinon.spy(el, "_setFromAttribute");
192 |
193 | el.setAttribute("test", "bar");
194 | await el.updateDone();
195 |
196 | expect(spySetFromAttribute).to.have.callCount(1);
197 | spySetFromAttribute.resetHistory();
198 |
199 | el.setAttribute("test", "bar");
200 | await el.updateDone();
201 |
202 | expect(spySetFromAttribute).to.have.callCount(0);
203 | });
204 |
205 | it(`should not call _setFromAttribute when not connected`, async () => {
206 | class Test extends Wafer {
207 | static props = {
208 | test: {
209 | type: String,
210 | initial: "foo",
211 | },
212 | };
213 | }
214 | customElements.define(`wafer-test-5`, Test);
215 |
216 | /**
217 | * @type {Test}
218 | */
219 | const el = new Test();
220 |
221 | const spySetFromAttribute = sinon.spy(el, "_setFromAttribute");
222 |
223 | el.setAttribute("test", "bar");
224 | await el.updateDone();
225 |
226 | expect(spySetFromAttribute).to.have.callCount(0);
227 | spySetFromAttribute.resetHistory();
228 |
229 | el.setAttribute("test", "bar");
230 | await el.updateDone();
231 |
232 | expect(spySetFromAttribute).to.have.callCount(0);
233 | });
234 |
235 | it("should use initial value set before upgraded", async () => {
236 | class Test extends Wafer {
237 | static props = {
238 | test: {
239 | type: String,
240 | initial: "foo",
241 | },
242 | };
243 | }
244 |
245 | /**
246 | * @type {Test}
247 | */
248 | const el = await fixture("");
249 | el.test = "bar";
250 |
251 | customElements.define(`wafer-test-6`, Test);
252 |
253 | await el.updateDone();
254 |
255 | expect(el._initials.test).equal("bar");
256 |
257 | expect(el.test).to.equal("bar");
258 | });
259 |
260 | it("should not call changed/updated when prop is changed to a new value then back to the old one", async () => {
261 | class Test extends Wafer {
262 | static props = {
263 | test: {
264 | type: String,
265 | initial: "foo",
266 | },
267 | };
268 | }
269 |
270 | customElements.define(`wafer-test-7`, Test);
271 |
272 | /**
273 | * @type {Test}
274 | */
275 | const el = await fixture("");
276 |
277 | const spyChanged = sinon.spy(el, "changed");
278 | const spyUpdated = sinon.spy(el, "updated");
279 |
280 | el.test = "bar";
281 | el.test = "foo";
282 |
283 | await el.updateDone();
284 |
285 | expect(spyChanged).to.have.callCount(0);
286 | expect(spyUpdated).to.have.callCount(0);
287 |
288 | expect(el.test).to.equal("foo");
289 | });
290 |
291 | it("should not call changed/updated when prop is changed to the same value twice", async () => {
292 | class Test extends Wafer {
293 | static props = {
294 | test: {
295 | type: String,
296 | initial: "foo",
297 | },
298 | };
299 | }
300 |
301 | customElements.define(`wafer-test-8`, Test);
302 |
303 | /**
304 | * @type {Test}
305 | */
306 | const el = await fixture("");
307 |
308 | const spyChanged = sinon.spy(el, "changed");
309 | const spyUpdated = sinon.spy(el, "updated");
310 |
311 | el.test = "bar";
312 | el.test = "bar";
313 |
314 | await el.updateDone();
315 |
316 | expect(spyChanged).to.have.callCount(1);
317 | expect(spyUpdated).to.have.callCount(1);
318 |
319 | expect(el.test).to.equal("bar");
320 | });
321 |
322 | it("should set prop to null if attribute removed", async () => {
323 | class Test extends Wafer {
324 | static props = {
325 | test: {
326 | type: String,
327 | initial: "foo",
328 | reflect: true,
329 | },
330 | };
331 | }
332 |
333 | customElements.define(`wafer-test-9`, Test);
334 |
335 | /**
336 | * @type {Test}
337 | */
338 | const el = await fixture("");
339 |
340 | const spyChanged = sinon.spy(el, "changed");
341 | const spyUpdated = sinon.spy(el, "updated");
342 |
343 | el.removeAttribute("test");
344 |
345 | await el.updateDone();
346 |
347 | expect(spyChanged).to.have.callCount(1);
348 | expect(spyUpdated).to.have.callCount(1);
349 |
350 | expect(el.test).to.equal(null);
351 | });
352 |
353 | it("should remove attribute when non boolean prop set to null", async () => {
354 | class Test extends Wafer {
355 | static props = {
356 | test: {
357 | type: String,
358 | initial: "foo",
359 | reflect: true,
360 | },
361 | };
362 | }
363 |
364 | customElements.define(`wafer-test-10`, Test);
365 |
366 | /**
367 | * @type {Test}
368 | */
369 | const el = await fixture("");
370 |
371 | const spyChanged = sinon.spy(el, "changed");
372 | const spyUpdated = sinon.spy(el, "updated");
373 |
374 | expect(el).attr("test").to.equal("foo");
375 |
376 | el.test = null;
377 |
378 | await el.updateDone();
379 |
380 | expect(spyChanged).to.have.callCount(1);
381 | expect(spyUpdated).to.have.callCount(1);
382 |
383 | expect(el.test).to.equal(null);
384 | expect(el).not.to.have.attr("test");
385 | });
386 |
387 | it("should remove attribute when non boolean prop set to undefined", async () => {
388 | class Test extends Wafer {
389 | static props = {
390 | test: {
391 | type: String,
392 | initial: "foo",
393 | reflect: true,
394 | },
395 | };
396 | }
397 |
398 | customElements.define(`wafer-test-10a`, Test);
399 |
400 | /**
401 | * @type {Test}
402 | */
403 | const el = await fixture("");
404 |
405 | const spyChanged = sinon.spy(el, "changed");
406 | const spyUpdated = sinon.spy(el, "updated");
407 |
408 | expect(el).attr("test").to.equal("foo");
409 |
410 | el.test = undefined;
411 |
412 | await el.updateDone();
413 |
414 | expect(spyChanged).to.have.callCount(1);
415 | expect(spyUpdated).to.have.callCount(1);
416 |
417 | expect(el.test).to.equal(undefined);
418 | expect(el).not.to.have.attr("test");
419 | });
420 |
421 | it("should remove attribute when boolean prop set to null", async () => {
422 | class Test extends Wafer {
423 | static props = {
424 | test: {
425 | type: Boolean,
426 | initial: true,
427 | reflect: true,
428 | },
429 | };
430 | }
431 |
432 | customElements.define(`wafer-test-11`, Test);
433 |
434 | /**
435 | * @type {Test}
436 | */
437 | const el = await fixture("");
438 |
439 | const spyChanged = sinon.spy(el, "changed");
440 | const spyUpdated = sinon.spy(el, "updated");
441 |
442 | expect(el).attr("test").to.equal("");
443 |
444 | el.test = null;
445 |
446 | await el.updateDone();
447 |
448 | expect(spyChanged).to.have.callCount(1);
449 | expect(spyUpdated).to.have.callCount(1);
450 |
451 | expect(el.test).to.equal(null);
452 | expect(el).not.to.have.attr("test");
453 | });
454 |
455 | it("should remove attribute when boolean prop set to undefined", async () => {
456 | class Test extends Wafer {
457 | static props = {
458 | test: {
459 | type: Boolean,
460 | initial: true,
461 | reflect: true,
462 | },
463 | };
464 | }
465 |
466 | customElements.define(`wafer-test-11a`, Test);
467 |
468 | /**
469 | * @type {Test}
470 | */
471 | const el = await fixture("");
472 |
473 | const spyChanged = sinon.spy(el, "changed");
474 | const spyUpdated = sinon.spy(el, "updated");
475 |
476 | expect(el).attr("test").to.equal("");
477 |
478 | el.test = undefined;
479 |
480 | await el.updateDone();
481 |
482 | expect(spyChanged).to.have.callCount(1);
483 | expect(spyUpdated).to.have.callCount(1);
484 |
485 | expect(el.test).to.equal(undefined);
486 | expect(el).not.to.have.attr("test");
487 | });
488 |
489 | it("should remove attribute when boolean prop set to false", async () => {
490 | class Test extends Wafer {
491 | static props = {
492 | test: {
493 | type: Boolean,
494 | initial: true,
495 | reflect: true,
496 | },
497 | };
498 | }
499 |
500 | customElements.define(`wafer-test-12`, Test);
501 |
502 | /**
503 | * @type {Test}
504 | */
505 | const el = await fixture("");
506 |
507 | const spyChanged = sinon.spy(el, "changed");
508 | const spyUpdated = sinon.spy(el, "updated");
509 |
510 | expect(el).attr("test").to.equal("");
511 |
512 | el.test = false;
513 |
514 | await el.updateDone();
515 |
516 | expect(spyChanged).to.have.callCount(1);
517 | expect(spyUpdated).to.have.callCount(1);
518 |
519 | expect(el.test).to.equal(false);
520 | expect(el).not.to.have.attr("test");
521 | });
522 |
523 | it(`should reflect un-reflected property to attribute when SSRd outside Wafer SSR (otherwise prop info lost on rehydration)`, async () => {
524 | class Test extends Wafer {
525 | static props = {
526 | test: {
527 | type: String,
528 | initial: "foo",
529 | },
530 | };
531 | }
532 |
533 | Wafer.isSSR = true;
534 |
535 | customElements.define(`wafer-test-13`, Test);
536 |
537 | /**
538 | * @type {Test}
539 | */
540 | const el = await fixture(
541 | ""
542 | );
543 |
544 | await el.updateDone();
545 |
546 | expect(el).attr("test").to.equal("bar");
547 | expect(el.test).to.deep.equal("bar");
548 | expect(el).attr("x-ssr").to.equal("");
549 | });
550 | });
551 |
--------------------------------------------------------------------------------
/test/server/element.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from "../testing.js";
2 |
3 | import { ServerElement, parse } from "../../src/server/element.js";
4 |
5 | describe("Wafer element", () => {
6 | it("can set content using innerHTML", async () => {
7 | const el = new ServerElement("div");
8 |
9 | el.innerHTML = `Hi!!
Bye!!
`;
10 |
11 | const children = el.childNodes;
12 |
13 | expect(children.length).to.equal(2);
14 |
15 | expect(children[0].tagName).to.equal("H1");
16 | expect(children[0].textContent).to.equal("Hi!!");
17 |
18 | expect(children[1].tagName).to.equal("H2");
19 | expect(children[1].textContent).to.equal("Bye!!");
20 | });
21 |
22 | it("can get firstChild", async () => {
23 | const el = new ServerElement("div");
24 |
25 | el.innerHTML = `Hi!!
Bye!!
`;
26 |
27 | const firstChild = el.firstChild;
28 |
29 | expect(firstChild.tagName).to.equal("H1");
30 | expect(firstChild.textContent).to.equal("Hi!!");
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/test/server/helpers.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from "../testing.js";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../src/server/wafer.js";
5 | import { repeat } from "../../src/server/dom.js";
6 | import { ServerElement, parse } from "../../src/server/element.js";
7 |
8 | describe("Wafer DOM", () => {
9 | it("can render elements in container with repeat", async () => {
10 | class Test extends Wafer {
11 | static template = "";
12 | static props = {
13 | items: {
14 | type: Array,
15 | initial: [1, 2, 3],
16 | targets: [
17 | {
18 | selector: "$div",
19 | dom: async (targetEl, items, el) => {
20 | await repeat({
21 | container: targetEl,
22 | items,
23 | html: "",
24 | keyFn: (item) => item,
25 | targets: [
26 | {
27 | selector: "self",
28 | text: true,
29 | },
30 | ],
31 | registry: el._registry,
32 | });
33 | },
34 | },
35 | ],
36 | },
37 | };
38 | }
39 |
40 | const html = await parse(
41 | `
42 |
43 | `,
44 | { "wafer-test": { def: Test, serverOnly: false } }
45 | );
46 |
47 | expect(html.toString()).html.to.equal(
48 | `
49 |
50 |
51 | 1
52 | 2
53 | 3
54 |
55 |
56 | `
57 | );
58 | });
59 |
60 | it("can remove elements in container with repeat", async () => {
61 | class Test extends Wafer {
62 | static template = "";
63 | static props = {
64 | items: {
65 | type: Array,
66 | initial: [1, 2, 3],
67 | targets: [
68 | {
69 | selector: "$div",
70 | dom: async (targetEl, items, el) => {
71 | await repeat({
72 | container: targetEl,
73 | items,
74 | html: "",
75 | keyFn: (item) => item,
76 | targets: [
77 | {
78 | selector: "self",
79 | text: true,
80 | },
81 | ],
82 | });
83 | },
84 | },
85 | ],
86 | },
87 | };
88 | }
89 |
90 | const html = await parse(
91 | `
92 |
93 | `,
94 | { "wafer-test": { def: Test } }
95 | );
96 |
97 | const el = html.querySelector("wafer-test");
98 |
99 | expect(el.toString()).html.to.equal(
100 | `
101 |
102 |
103 | 1
104 | 2
105 | 3
106 |
107 |
108 | `
109 | );
110 |
111 | el.items = [1, 2];
112 |
113 | await el.updateDone();
114 |
115 | expect(el.toString()).html.to.equal(
116 | `
117 |
118 |
119 | 1
120 | 2
121 |
122 |
123 | `
124 | );
125 | });
126 |
127 | it("can update elements in container with repeat", async () => {
128 | class Test extends Wafer {
129 | static template = "";
130 | static props = {
131 | items: {
132 | type: Array,
133 | initial: [1, 2, 3],
134 | targets: [
135 | {
136 | selector: "$div",
137 | dom: async (targetEl, items, el) => {
138 | await repeat({
139 | container: targetEl,
140 | items,
141 | html: "",
142 | keyFn: (item) => item,
143 | targets: [
144 | {
145 | selector: "self",
146 | text: true,
147 | },
148 | ],
149 | });
150 | },
151 | },
152 | ],
153 | },
154 | };
155 | }
156 |
157 | const html = await parse(
158 | `
159 |
160 | `,
161 | { "wafer-test": { def: Test } }
162 | );
163 |
164 | const el = html.querySelector("wafer-test");
165 |
166 | expect(el.toString()).html.to.equal(
167 | `
168 |
169 |
170 | 1
171 | 2
172 | 3
173 |
174 |
175 | `
176 | );
177 |
178 | el.items = [4, 3, 5, 2, 6, 1];
179 |
180 | await el.updateDone();
181 |
182 | expect(el.toString()).html.to.equal(
183 | `
184 |
185 |
186 | 4
187 | 3
188 | 5
189 | 2
190 | 6
191 | 1
192 |
193 |
194 | `
195 | );
196 | });
197 |
198 | it("can add new elements in container with repeat", async () => {
199 | class Test extends Wafer {
200 | static template = "";
201 | static props = {
202 | items: {
203 | type: Array,
204 | initial: [1, 2, 3],
205 | targets: [
206 | {
207 | selector: "$div",
208 | dom: async (targetEl, items, el) => {
209 | await repeat({
210 | container: targetEl,
211 | items,
212 | html: "",
213 | keyFn: (item) => item,
214 | targets: [
215 | {
216 | selector: "self",
217 | text: true,
218 | },
219 | ],
220 | });
221 | },
222 | },
223 | ],
224 | },
225 | };
226 | }
227 |
228 | const html = await parse(
229 | `
230 |
231 | `,
232 | { "wafer-test": { def: Test } }
233 | );
234 |
235 | const el = html.querySelector("wafer-test");
236 |
237 | expect(el.toString()).html.to.equal(
238 | `
239 |
240 |
241 | 1
242 | 2
243 | 3
244 |
245 |
246 | `
247 | );
248 |
249 | el.items = [1, 2, 3, 4];
250 |
251 | await el.updateDone();
252 |
253 | expect(el.toString()).html.to.equal(
254 | `
255 |
256 |
257 | 1
258 | 2
259 | 3
260 | 4
261 |
262 |
263 | `
264 | );
265 | });
266 |
267 | it("runs init function only when adding element", async () => {
268 | class Test extends Wafer {
269 | static template = "";
270 | static props = {
271 | items: {
272 | type: Array,
273 | initial: [1, 2, 3],
274 | targets: [
275 | {
276 | selector: "$:scope>div",
277 | dom: async (targetEl, items, self) => {
278 | await repeat({
279 | container: targetEl,
280 | items,
281 | html: "
",
282 | keyFn: (item) => item,
283 | targets: [
284 | {
285 | selector: ":scope>span",
286 | text: true,
287 | },
288 | ],
289 | init: async (el, item, index) => {
290 | const div = new ServerElement("div");
291 | div.textContent = `d${item} (${index})`;
292 | el.appendChild(div);
293 | },
294 | });
295 | },
296 | },
297 | ],
298 | },
299 | };
300 | }
301 |
302 | const html = await parse(
303 | `
304 |
305 | `,
306 | { "wafer-test": { def: Test } }
307 | );
308 |
309 | const el = html.querySelector("wafer-test");
310 |
311 | expect(el.toString()).html.to.equal(
312 | `
313 |
314 |
319 |
320 | `
321 | );
322 |
323 | el.items = [4, 3, 5, 2, 6, 1];
324 | await el.updateDone();
325 |
326 | expect(el.toString()).html.to.equal(`
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 | `);
340 | });
341 |
342 | it("shouldn't choke on leaading whitespace in repeat html", async () => {
343 | class Test extends Wafer {
344 | static template = "";
345 | static props = {
346 | items: {
347 | type: Array,
348 | initial: [1, 2, 3],
349 | targets: [
350 | {
351 | selector: "$div",
352 | dom: async (container, items) => {
353 | await repeat({
354 | container,
355 | items,
356 | html: `
357 |
358 | `,
359 | keyFn: (item) => item,
360 | targets: [
361 | {
362 | selector: "self",
363 | text: true,
364 | },
365 | ],
366 | });
367 | },
368 | },
369 | ],
370 | },
371 | };
372 | }
373 |
374 | const html = await parse(
375 | `
376 |
377 | `,
378 | { "wafer-test": { def: Test } }
379 | );
380 |
381 | const el = html.querySelector("wafer-test");
382 |
383 | expect(el.toString()).html.to.equal(
384 | `
385 |
386 |
387 | 1
388 | 2
389 | 3
390 |
391 |
392 | `
393 | );
394 | });
395 | });
396 |
--------------------------------------------------------------------------------
/test/server/instantiation/as-object.js:
--------------------------------------------------------------------------------
1 | import { expect } from "../../testing.js";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/server/wafer.js";
5 | import configs from "../../configs.js";
6 |
7 | describe("Wafer sets attributes and properties on element when defined as object", () => {
8 | for (const [configIndex, config] of configs.entries()) {
9 | class Test extends Wafer {
10 | static props = config.props;
11 | }
12 |
13 | const attrName = config.props.test.attributeName || "test";
14 |
15 | for (const [testIndex, test] of config.tests.entries()) {
16 | let itFunc = it;
17 | if (test.only) {
18 | itFunc = it.only;
19 | }
20 |
21 | itFunc(
22 | `with ${test.html(`${configIndex}-${testIndex}`).trim()} (${
23 | config.description
24 | }, ${test.description})`,
25 | async () => {
26 | /**
27 | * @type {Test}
28 | */
29 | const el = new Test(
30 | {
31 | tagName: `wafer-test-${configIndex}-${testIndex}`,
32 | attrs: test.attrs,
33 | },
34 | {
35 | [`wafer-test-${configIndex}-${testIndex}`]: { def: Test },
36 | }
37 | );
38 | await el.construct();
39 | await el.connectedCallback();
40 |
41 | // ssr elements will always have attributes set if prop has a value
42 | if (test.expected.prop !== undefined) {
43 | const attrValue =
44 | config.props.test.type === Boolean
45 | ? test.expected.prop
46 | ? ""
47 | : undefined
48 | : config.props.test.type === Object ||
49 | config.props.test.type === Array
50 | ? JSON.stringify(test.expected.prop)
51 | : `${test.expected.prop}`;
52 |
53 | expect(el.getAttribute(attrName)).to.equal(attrValue);
54 | } else {
55 | expect(el.getAttribute(attrName)).to.equal(undefined);
56 | }
57 |
58 | expect(el.test).to.deep.equal(test.expected.prop);
59 | }
60 | );
61 | }
62 | }
63 | });
64 |
--------------------------------------------------------------------------------
/test/server/instantiation/as-string.js:
--------------------------------------------------------------------------------
1 | import { expect } from "../../testing.js";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/server/wafer.js";
5 | import { parse } from "../../../src/server/element.js";
6 | import configs from "../../configs.js";
7 |
8 | describe("Wafer sets attributes and properties on element when defined as string", () => {
9 | for (const [configIndex, config] of configs.entries()) {
10 | for (const [testIndex, test] of config.tests.entries()) {
11 | let itFunc = it;
12 | if (test.only) {
13 | itFunc = it.only;
14 | }
15 |
16 | itFunc(
17 | `with ${test.html(`${configIndex}-${testIndex}`).trim()} (${
18 | config.description
19 | }, ${test.description})`,
20 | async () => {
21 | class Test extends Wafer {
22 | static props = config.props;
23 | }
24 |
25 | const attrName = config.props.test.attributeName || "test";
26 |
27 | const html = await parse(test.html(`${configIndex}-${testIndex}`), {
28 | [`wafer-test-${configIndex}-${testIndex}`]: { def: Test },
29 | });
30 |
31 | const el = html.querySelector(
32 | `wafer-test-${configIndex}-${testIndex}`
33 | );
34 |
35 | // ssr elements will always have attributes set if prop has a value
36 | if (test.expected.prop !== undefined) {
37 | const attrValue =
38 | config.props.test.type === Boolean
39 | ? test.expected.prop
40 | ? ""
41 | : undefined
42 | : config.props.test.type === Object ||
43 | config.props.test.type === Array
44 | ? JSON.stringify(test.expected.prop)
45 | : `${test.expected.prop}`;
46 |
47 | expect(el.getAttribute(attrName)).to.equal(attrValue);
48 | } else {
49 | expect(el.getAttribute(attrName)).to.equal(undefined);
50 | }
51 |
52 | expect(el.test).to.deep.equal(test.expected.prop);
53 | }
54 | );
55 | }
56 | }
57 | });
58 |
--------------------------------------------------------------------------------
/test/server/instantiation/general.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from "../../testing.js";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../../src/server/wafer.js";
5 |
6 | describe("WaferServer behaviour on instantiation", () => {
7 | it(`should update even if not connected`, async () => {
8 | class Test extends Wafer {
9 | static props = {
10 | test: {
11 | type: String,
12 | reflect: true,
13 | initial: "foo",
14 | },
15 | };
16 | }
17 |
18 | /**
19 | * @type {Test}
20 | */
21 | const el = new Test({ tagName: "wafer-test-0" });
22 | await el.construct();
23 |
24 | const spyChanged = sinon.spy(el, "changed");
25 | const spyUpdated = sinon.spy(el, "updated");
26 |
27 | el.test = "bar";
28 |
29 | await el.updateDone();
30 |
31 | expect(spyChanged).to.have.callCount(1);
32 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
33 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
34 | expect(spyChanged.args[0][0].size).equal(1);
35 | expect(spyChanged.args[0][0].has("test")).equal(true);
36 | expect(spyChanged.args[0][0].get("test")).equal(undefined);
37 |
38 | expect(spyUpdated).to.have.callCount(1);
39 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
40 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
41 | expect(spyUpdated.args[0][0].size).equal(1);
42 | expect(spyUpdated.args[0][0].has("test")).equal(true);
43 | expect(spyUpdated.args[0][0].get("test")).equal(undefined);
44 |
45 | expect(el).attr("test").to.equal("bar");
46 | expect(el.test).to.deep.equal("bar");
47 |
48 | expect(el.toString()).html.to.equal(
49 | `
50 |
51 |
52 |
53 | `
54 | );
55 | });
56 |
57 | it(`should update reflected property when attribute changes`, async () => {
58 | class Test extends Wafer {
59 | static props = {
60 | test: {
61 | type: String,
62 | reflect: true,
63 | initial: "foo",
64 | },
65 | };
66 | }
67 |
68 | /**
69 | * @type {Test}
70 | */
71 | const el = new Test({ tagName: "wafer-test-1" });
72 | await el.construct();
73 | await el.connectedCallback();
74 |
75 | const spyChanged = sinon.spy(el, "changed");
76 | const spyUpdated = sinon.spy(el, "updated");
77 |
78 | el.setAttribute("test", "bar");
79 |
80 | await el.updateDone();
81 |
82 | expect(spyChanged).to.have.callCount(1);
83 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
84 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
85 | expect(spyChanged.args[0][0].size).equal(1);
86 | expect(spyChanged.args[0][0].has("test")).equal(true);
87 | expect(spyChanged.args[0][0].get("test")).equal("foo");
88 |
89 | expect(spyUpdated).to.have.callCount(1);
90 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
91 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
92 | expect(spyUpdated.args[0][0].size).equal(1);
93 | expect(spyUpdated.args[0][0].has("test")).equal(true);
94 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
95 |
96 | expect(el).attr("test").to.equal("bar");
97 | expect(el.test).to.deep.equal("bar");
98 | });
99 |
100 | it(`should update un-reflected property when attribute changes`, async () => {
101 | class Test extends Wafer {
102 | static props = {
103 | test: {
104 | type: String,
105 | initial: "foo",
106 | },
107 | };
108 | }
109 |
110 | /**
111 | * @type {Test}
112 | */
113 | const el = new Test({ tagName: "wafer-test-2" });
114 | await el.construct();
115 | await el.connectedCallback();
116 |
117 | const spyChanged = sinon.spy(el, "changed");
118 | const spyUpdated = sinon.spy(el, "updated");
119 |
120 | el.setAttribute("test", "bar");
121 |
122 | await el.updateDone();
123 |
124 | expect(spyChanged).to.have.callCount(1);
125 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
126 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
127 | expect(spyChanged.args[0][0].size).equal(1);
128 | expect(spyChanged.args[0][0].has("test")).equal(true);
129 | expect(spyChanged.args[0][0].get("test")).equal("foo");
130 |
131 | expect(spyUpdated).to.have.callCount(1);
132 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
133 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
134 | expect(spyUpdated.args[0][0].size).equal(1);
135 | expect(spyUpdated.args[0][0].has("test")).equal(true);
136 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
137 |
138 | expect(el).attr("test").to.equal("bar");
139 | expect(el.test).to.deep.equal("bar");
140 | });
141 |
142 | it(`should reflect un-reflected property to attribute (server always reflect attributes as they are the source of truth in ssr)`, async () => {
143 | class Test extends Wafer {
144 | static props = {
145 | test: {
146 | type: String,
147 | initial: "foo",
148 | },
149 | };
150 | }
151 |
152 | /**
153 | * @type {Test}
154 | */
155 | const el = new Test({ tagName: "wafer-test-3" });
156 | await el.construct();
157 | await el.connectedCallback();
158 |
159 | const spyChanged = sinon.spy(el, "changed");
160 | const spyUpdated = sinon.spy(el, "updated");
161 |
162 | el.test = "bar";
163 |
164 | await el.updateDone();
165 |
166 | expect(spyChanged).to.have.callCount(1);
167 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
168 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
169 | expect(spyChanged.args[0][0].size).equal(1);
170 | expect(spyChanged.args[0][0].has("test")).equal(true);
171 | expect(spyChanged.args[0][0].get("test")).equal("foo");
172 |
173 | expect(spyUpdated).to.have.callCount(1);
174 | // can't check for equal maps, since a key set to undefined is the same as a key that's not set (with chai)
175 | // expect(spyUpdated).calledWith(new Map([['test', undefined]])) === expect(spyUpdated).calledWith(new Map([['blah', undefined]]))
176 | expect(spyUpdated.args[0][0].size).equal(1);
177 | expect(spyUpdated.args[0][0].has("test")).equal(true);
178 | expect(spyUpdated.args[0][0].get("test")).equal("foo");
179 |
180 | expect(el).attr("test").to.equal("bar");
181 | expect(el.test).to.deep.equal("bar");
182 | });
183 |
184 | it(`should not call _setFromAttribute when attribute is set to existing value when connected`, async () => {
185 | class Test extends Wafer {
186 | static props = {
187 | test: {
188 | type: String,
189 | initial: "foo",
190 | },
191 | };
192 | }
193 |
194 | /**
195 | * @type {Test}
196 | */
197 | const el = new Test({ tagName: "wafer-test-4" });
198 | await el.construct();
199 | await el.connectedCallback();
200 |
201 | const spySetFromAttribute = sinon.spy(el, "_setFromAttribute");
202 |
203 | el.setAttribute("test", "bar");
204 |
205 | await el.updateDone();
206 |
207 | expect(spySetFromAttribute).to.have.callCount(1);
208 | spySetFromAttribute.resetHistory();
209 |
210 | el.setAttribute("test", "bar");
211 | await el.updateDone();
212 |
213 | expect(spySetFromAttribute).to.have.callCount(0);
214 | });
215 |
216 | it(`should not call _setFromAttribute when not connected`, async () => {
217 | class Test extends Wafer {
218 | static props = {
219 | test: {
220 | type: String,
221 | initial: "foo",
222 | },
223 | };
224 | }
225 |
226 | /**
227 | * @type {Test}
228 | */
229 | const el = new Test({ tagName: "wafer-test-5" });
230 | await el.construct();
231 |
232 | const spySetFromAttribute = sinon.spy(el, "_setFromAttribute");
233 |
234 | el.setAttribute("test", "bar");
235 |
236 | await el.updateDone();
237 |
238 | expect(spySetFromAttribute).to.have.callCount(0);
239 | spySetFromAttribute.resetHistory();
240 |
241 | el.setAttribute("test", "bar");
242 | await el.updateDone();
243 |
244 | expect(spySetFromAttribute).to.have.callCount(0);
245 | });
246 |
247 | it("should use initial value set before upgraded", async () => {
248 | class Test extends Wafer {
249 | static props = {
250 | test: {
251 | type: String,
252 | initial: "foo",
253 | },
254 | };
255 | }
256 |
257 | /**
258 | * @type {Test}
259 | */
260 | const el = new Test({ tagName: "wafer-test-6" });
261 | el.test = "bar";
262 |
263 | await el.construct();
264 | await el.connectedCallback();
265 |
266 | expect(el._initials.test).equal("bar");
267 | expect(el.test).to.equal("bar");
268 | });
269 |
270 | it("should not call changed/updated when prop is changed to a new value then back to the old one", async () => {
271 | class Test extends Wafer {
272 | static props = {
273 | test: {
274 | type: String,
275 | initial: "foo",
276 | },
277 | };
278 | }
279 |
280 | /**
281 | * @type {Test}
282 | */
283 | const el = new Test({ tagName: "wafer-test-7" });
284 | await el.construct();
285 | await el.connectedCallback();
286 |
287 | const spyChanged = sinon.spy(el, "changed");
288 | const spyUpdated = sinon.spy(el, "updated");
289 |
290 | el.test = "bar";
291 | el.test = "foo";
292 |
293 | await el.updateDone();
294 |
295 | expect(spyChanged).to.have.callCount(0);
296 | expect(spyUpdated).to.have.callCount(0);
297 |
298 | expect(el.test).to.equal("foo");
299 | });
300 |
301 | it("should not call changed/updated when prop is changed to the same value twice", async () => {
302 | class Test extends Wafer {
303 | static props = {
304 | test: {
305 | type: String,
306 | initial: "foo",
307 | },
308 | };
309 | }
310 |
311 | /**
312 | * @type {Test}
313 | */
314 | const el = new Test({ tagName: "wafer-test-8" });
315 | await el.construct();
316 | await el.connectedCallback();
317 |
318 | const spyChanged = sinon.spy(el, "changed");
319 | const spyUpdated = sinon.spy(el, "updated");
320 |
321 | el.test = "bar";
322 | el.test = "bar";
323 |
324 | await el.updateDone();
325 |
326 | expect(spyChanged).to.have.callCount(1);
327 | expect(spyUpdated).to.have.callCount(1);
328 |
329 | expect(el.test).to.equal("bar");
330 | });
331 |
332 | it("should set prop to null if attribute removed", async () => {
333 | class Test extends Wafer {
334 | static props = {
335 | test: {
336 | type: String,
337 | initial: "foo",
338 | reflect: true,
339 | },
340 | };
341 | }
342 |
343 | /**
344 | * @type {Test}
345 | */
346 | const el = new Test({ tagName: "wafer-test-9" });
347 | await el.construct();
348 | await el.connectedCallback();
349 |
350 | const spyChanged = sinon.spy(el, "changed");
351 | const spyUpdated = sinon.spy(el, "updated");
352 |
353 | el.removeAttribute("test");
354 |
355 | await el.updateDone();
356 |
357 | expect(spyChanged).to.have.callCount(1);
358 | expect(spyUpdated).to.have.callCount(1);
359 |
360 | expect(el.test).to.equal(null);
361 | });
362 |
363 | it("should remove attribute when non boolean prop set to null", async () => {
364 | class Test extends Wafer {
365 | static props = {
366 | test: {
367 | type: String,
368 | initial: "foo",
369 | reflect: true,
370 | },
371 | };
372 | }
373 |
374 | /**
375 | * @type {Test}
376 | */
377 | const el = new Test({ tagName: "wafer-test-10" });
378 | await el.construct();
379 | await el.connectedCallback();
380 |
381 | const spyChanged = sinon.spy(el, "changed");
382 | const spyUpdated = sinon.spy(el, "updated");
383 |
384 | expect(el).attr("test").to.equal("foo");
385 |
386 | el.test = null;
387 |
388 | await el.updateDone();
389 |
390 | expect(spyChanged).to.have.callCount(1);
391 | expect(spyUpdated).to.have.callCount(1);
392 |
393 | expect(el.test).to.equal(null);
394 | expect(el.getAttribute("test")).to.equal(undefined);
395 | });
396 |
397 | it("should remove attribute when non boolean prop set to undefined", async () => {
398 | class Test extends Wafer {
399 | static props = {
400 | test: {
401 | type: String,
402 | initial: "foo",
403 | reflect: true,
404 | },
405 | };
406 | }
407 |
408 | /**
409 | * @type {Test}
410 | */
411 | const el = new Test({ tagName: "wafer-test-10" });
412 | await el.construct();
413 | await el.connectedCallback();
414 |
415 | const spyChanged = sinon.spy(el, "changed");
416 | const spyUpdated = sinon.spy(el, "updated");
417 |
418 | expect(el).attr("test").to.equal("foo");
419 |
420 | el.test = undefined;
421 |
422 | await el.updateDone();
423 |
424 | expect(spyChanged).to.have.callCount(1);
425 | expect(spyUpdated).to.have.callCount(1);
426 |
427 | expect(el.test).to.equal(undefined);
428 | expect(el.getAttribute("test")).to.equal(undefined);
429 | });
430 |
431 | it("should remove attribute when boolean prop set to null", async () => {
432 | class Test extends Wafer {
433 | static props = {
434 | test: {
435 | type: Boolean,
436 | initial: true,
437 | reflect: true,
438 | },
439 | };
440 | }
441 |
442 | /**
443 | * @type {Test}
444 | */
445 | const el = new Test({ tagName: "wafer-test-11" });
446 | await el.construct();
447 | await el.connectedCallback();
448 |
449 | const spyChanged = sinon.spy(el, "changed");
450 | const spyUpdated = sinon.spy(el, "updated");
451 |
452 | expect(el.getAttribute("test")).to.equal("");
453 |
454 | el.test = null;
455 |
456 | await el.updateDone();
457 |
458 | expect(spyChanged).to.have.callCount(1);
459 | expect(spyUpdated).to.have.callCount(1);
460 |
461 | expect(el.test).to.equal(null);
462 | expect(el.getAttribute("test")).to.equal(undefined);
463 | });
464 |
465 | it("should remove attribute when boolean prop set to undefined", async () => {
466 | class Test extends Wafer {
467 | static props = {
468 | test: {
469 | type: Boolean,
470 | initial: true,
471 | reflect: true,
472 | },
473 | };
474 | }
475 |
476 | /**
477 | * @type {Test}
478 | */
479 | const el = new Test({ tagName: "wafer-test-11" });
480 | await el.construct();
481 | await el.connectedCallback();
482 |
483 | const spyChanged = sinon.spy(el, "changed");
484 | const spyUpdated = sinon.spy(el, "updated");
485 |
486 | expect(el.getAttribute("test")).to.equal("");
487 |
488 | el.test = undefined;
489 |
490 | await el.updateDone();
491 |
492 | expect(spyChanged).to.have.callCount(1);
493 | expect(spyUpdated).to.have.callCount(1);
494 |
495 | expect(el.test).to.equal(undefined);
496 | expect(el.getAttribute("test")).to.equal(undefined);
497 | });
498 |
499 | it("should remove attribute when boolean prop set to false", async () => {
500 | class Test extends Wafer {
501 | static props = {
502 | test: {
503 | type: Boolean,
504 | initial: true,
505 | reflect: true,
506 | },
507 | };
508 | }
509 |
510 | /**
511 | * @type {Test}
512 | */
513 | const el = new Test({ tagName: "wafer-test-12" });
514 | await el.construct();
515 | await el.connectedCallback();
516 |
517 | const spyChanged = sinon.spy(el, "changed");
518 | const spyUpdated = sinon.spy(el, "updated");
519 |
520 | expect(el.getAttribute("test")).to.equal("");
521 |
522 | el.test = false;
523 |
524 | await el.updateDone();
525 |
526 | expect(spyChanged).to.have.callCount(1);
527 | expect(spyUpdated).to.have.callCount(1);
528 |
529 | expect(el.test).to.equal(false);
530 | expect(el.getAttribute("test")).to.equal(undefined);
531 | });
532 |
533 | it("should use initial value set before connected", async () => {
534 | class Test extends Wafer {
535 | static props = {
536 | test: {
537 | type: String,
538 | initial: "foo",
539 | },
540 | };
541 | }
542 |
543 | /**
544 | * @type {Test}
545 | */
546 | const el = new Test({ tagName: "wafer-test-13" });
547 | await el.construct();
548 |
549 | el.test = "bar";
550 |
551 | await el.connectedCallback();
552 |
553 | expect(el).attr("test").to.equal("bar");
554 | expect(el.test).to.equal("bar");
555 | });
556 |
557 | it("should set boolean prop to false on attribute removal", async () => {
558 | class Test extends Wafer {
559 | static props = {
560 | test: {
561 | type: Boolean,
562 | initial: true,
563 | reflect: true,
564 | },
565 | };
566 | }
567 |
568 | /**
569 | * @type {Test}
570 | */
571 | const el = new Test({ tagName: "wafer-test" });
572 | await el.construct();
573 | await el.connectedCallback();
574 |
575 | expect(el.getAttribute("test")).to.equal("");
576 | expect(el.test).to.equal(true);
577 |
578 | el.removeAttribute("test");
579 | await el.updateDone();
580 |
581 | expect(el.getAttribute("test")).to.equal(undefined);
582 | expect(el.test).to.equal(false);
583 | });
584 | });
585 |
--------------------------------------------------------------------------------
/test/server/rendering.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from "../testing.js";
2 | import sinon from "sinon";
3 |
4 | import Wafer from "../../src/server/wafer.js";
5 | import { ServerElement, parse } from "../../src/server/element.js";
6 |
7 | describe("Wafer renders expected content", () => {
8 | it("renders in shadow DOM", async () => {
9 | class Test extends Wafer {
10 | static template = "Test
";
11 | }
12 |
13 | /**
14 | * @type {Test}
15 | */
16 | const el = new Test({ tagName: "wafer-test" });
17 | await el.construct();
18 | await el.connectedCallback();
19 |
20 | expect(el.toString()).html.to.equal(
21 | `
22 |
23 | Test
24 |
25 | `
26 | );
27 |
28 | expect(el.shadowRoot.toString()).html.to.equal(`
29 |
30 | Test
31 |
32 | `);
33 | });
34 |
35 | it("renders in light DOM", async () => {
36 | class Test extends Wafer {
37 | static template = "Test
";
38 |
39 | constructor({ tagName }) {
40 | super({ tagName, shadow: false });
41 | }
42 | }
43 |
44 | /**
45 | * @type {Test}
46 | */
47 | const el = new Test({ tagName: "wafer-test" });
48 | await el.construct();
49 | await el.connectedCallback();
50 |
51 | expect(el.toString()).html.to.equal(`
52 |
53 | Test
54 | `);
55 |
56 | expect(el.shadowRoot).to.be.null;
57 | });
58 |
59 | it("renders slots", async () => {
60 | class Test extends Wafer {
61 | static template = "Hi
";
62 | }
63 |
64 | const html = await parse(
65 | `
66 |
67 | Chris
68 |
69 | `,
70 | { "wafer-test": { def: Test } }
71 | );
72 |
73 | expect(html.toString()).html.to.equal(
74 | `
75 | Chris
76 |
77 | Hi
78 |
79 | `
80 | );
81 | });
82 |
83 | it("updates DOM text in shadow DOM", async () => {
84 | class Test extends Wafer {
85 | static template = 'Hi
';
86 | static props = {
87 | name: {
88 | type: String,
89 | reflect: true,
90 | targets: [
91 | {
92 | selector: "$#name",
93 | text: true,
94 | },
95 | ],
96 | },
97 | };
98 | }
99 |
100 | const html = await parse(
101 | `
102 |
103 | `,
104 | { "wafer-test": { def: Test } }
105 | );
106 |
107 | expect(html.toString()).html.to.equal(
108 | `
109 |
110 | Hi Chris
111 |
112 | `
113 | );
114 | });
115 |
116 | it("updates DOM text in light DOM", async () => {
117 | class Test extends Wafer {
118 | static template = "";
119 | static props = {
120 | name: {
121 | type: String,
122 | reflect: true,
123 | targets: [
124 | {
125 | selector: "#name",
126 | text: true,
127 | },
128 | ],
129 | },
130 | };
131 | }
132 |
133 | const html = await parse(
134 | `
135 |
136 |
137 |
138 | `,
139 | { "wafer-test": { def: Test } }
140 | );
141 |
142 | expect(html.toString()).html.to.equal(
143 | `
144 | Chris
145 |
146 |
147 | `
148 | );
149 | });
150 |
151 | it("updates DOM property in shadow DOM", async () => {
152 | class Test extends Wafer {
153 | static template = '';
154 | static props = {
155 | on: {
156 | type: Boolean,
157 | reflect: true,
158 | targets: [
159 | {
160 | selector: "$input",
161 | property: "checked",
162 | },
163 | ],
164 | },
165 | };
166 | }
167 |
168 | /**
169 | * @type {Test}
170 | */
171 | const el = new Test({ tagName: "wafer-test", attrs: { on: "" } });
172 | await el.construct();
173 | await el.connectedCallback();
174 |
175 | expect(el.toString()).html.to.equal(
176 | `
177 |
178 |
179 |
180 | `
181 | );
182 |
183 | expect(el.shadowRoot.querySelector("input")).property("checked").to.be.true;
184 | });
185 |
186 | it("updates DOM property in light DOM", async () => {
187 | class Test extends Wafer {
188 | static template = '';
189 | static props = {
190 | on: {
191 | type: Boolean,
192 | reflect: true,
193 | targets: [
194 | {
195 | selector: "input",
196 | property: "checked",
197 | },
198 | ],
199 | },
200 | };
201 |
202 | constructor({ tagName, attrs }) {
203 | super({ tagName, attrs, shadow: false });
204 | }
205 | }
206 |
207 | /**
208 | * @type {Test}
209 | */
210 | const el = new Test({ tagName: "wafer-test", attrs: { on: "" } });
211 | await el.construct();
212 | await el.connectedCallback();
213 |
214 | expect(el.toString()).html.to.equal(
215 | `
216 |
217 | `
218 | );
219 |
220 | expect(el.querySelector("input")).property("checked").to.be.true;
221 | });
222 |
223 | it("updates DOM attribute in shadow DOM", async () => {
224 | class Test extends Wafer {
225 | static template = '';
226 | static props = {
227 | on: {
228 | type: Boolean,
229 | reflect: true,
230 | targets: [
231 | {
232 | selector: "$input",
233 | attribute: "checked",
234 | },
235 | ],
236 | },
237 | };
238 | }
239 |
240 | const html = await parse(
241 | `
242 |
243 | `,
244 | { "wafer-test": { def: Test } }
245 | );
246 |
247 | expect(html.toString()).html.to.equal(
248 | `
249 |
250 |
251 |
252 | `
253 | );
254 | });
255 |
256 | it("updates DOM property in light DOM", async () => {
257 | class Test extends Wafer {
258 | static template = '';
259 | static props = {
260 | on: {
261 | type: Boolean,
262 | reflect: true,
263 | targets: [
264 | {
265 | selector: "input",
266 | attribute: "checked",
267 | },
268 | ],
269 | },
270 | };
271 |
272 | constructor({ tagName }) {
273 | super({ tagName, shadow: false });
274 | }
275 | }
276 |
277 | const html = await parse(
278 | `
279 |
280 | `,
281 | { "wafer-test": { def: Test } }
282 | );
283 |
284 | expect(html.toString()).html.to.equal(
285 | `
286 |
287 |
288 | `
289 | );
290 | });
291 |
292 | it("uses custom value in shadow DOM", async () => {
293 | class Test extends Wafer {
294 | static template = "test
";
295 | static props = {
296 | test: {
297 | type: String,
298 | reflect: true,
299 | targets: [
300 | {
301 | selector: "$div",
302 | use: (value) => `bar ${value}`,
303 | attribute: "class",
304 | },
305 | ],
306 | },
307 | };
308 | }
309 |
310 | const html = await parse(
311 | `
312 |
313 | `,
314 | { "wafer-test": { def: Test } }
315 | );
316 |
317 | expect(html.toString()).html.to.equal(
318 | `
319 |
320 |
321 | test
322 |
323 | `
324 | );
325 | });
326 |
327 | it("uses custom value in light DOM", async () => {
328 | class Test extends Wafer {
329 | static template = "test
";
330 | static props = {
331 | test: {
332 | type: String,
333 | reflect: true,
334 | targets: [
335 | {
336 | selector: "div",
337 | use: (value) => `bar ${value}`,
338 | attribute: "class",
339 | },
340 | ],
341 | },
342 | };
343 |
344 | constructor({ tagName }) {
345 | super({ tagName, shadow: false });
346 | }
347 | }
348 |
349 | const html = await parse(
350 | `
351 |
352 | `,
353 | { "wafer-test": { def: Test } }
354 | );
355 |
356 | expect(html.toString()).html.to.equal(
357 | `
358 |
359 | test
360 | `
361 | );
362 | });
363 |
364 | it("updates DOM in shadow DOM", async () => {
365 | class Test extends Wafer {
366 | static template = "test
";
367 | static props = {
368 | test: {
369 | type: String,
370 | reflect: true,
371 | targets: [
372 | {
373 | selector: "$div",
374 | dom: (el, value) => {
375 | const child = new ServerElement("h1");
376 | child.textContent = value;
377 | el.innerHTML = "";
378 | el.appendChild(child);
379 | },
380 | },
381 | ],
382 | },
383 | };
384 | }
385 |
386 | const html = await parse(
387 | `
388 |
389 | `,
390 | { "wafer-test": { def: Test } }
391 | );
392 |
393 | expect(html.toString()).html.to.equal(
394 | `
395 |
396 |
397 | foo
398 |
399 | `
400 | );
401 | });
402 |
403 | it("updates DOM in light DOM", async () => {
404 | class Test extends Wafer {
405 | static template = "test
";
406 | static props = {
407 | test: {
408 | type: String,
409 | reflect: true,
410 | targets: [
411 | {
412 | selector: "div",
413 | dom: (el, value) => {
414 | const child = new ServerElement("h1");
415 | child.textContent = value;
416 | el.innerHTML = "";
417 | el.appendChild(child);
418 | },
419 | },
420 | ],
421 | },
422 | };
423 |
424 | constructor({ tagName }) {
425 | super({ tagName, shadow: false });
426 | }
427 | }
428 |
429 | const html = await parse(
430 | `
431 |
432 | `,
433 | { "wafer-test": { def: Test } }
434 | );
435 |
436 | expect(html.toString()).html.to.equal(
437 | `
438 |
439 | foo
440 | `
441 | );
442 | });
443 |
444 | it("can use a function for selector", async () => {
445 | class Test extends Wafer {
446 | static template = `
447 | 1
448 | 2
449 | 3
450 | `;
451 | static props = {
452 | active: {
453 | type: Number,
454 | reflect: true,
455 | targets: [
456 | {
457 | selector: (value) => `$#item-${value}`,
458 | attribute: "active",
459 | },
460 | ],
461 | },
462 | };
463 | }
464 |
465 | const html = await parse(
466 | `
467 |
468 | `,
469 | { "wafer-test": { def: Test } }
470 | );
471 |
472 | expect(html.toString()).html.to.equal(
473 | `
474 |
475 |
476 | 1
477 | 2
478 | 3
479 |
480 | `
481 | );
482 | });
483 |
484 | it("can use self as selector", async () => {
485 | class Test extends Wafer {
486 | static props = {
487 | active: {
488 | type: Number,
489 | reflect: true,
490 | targets: [
491 | {
492 | selector: "self",
493 | attribute: "test",
494 | },
495 | ],
496 | },
497 | };
498 | }
499 |
500 | const html = await parse(
501 | `
502 |
503 | `,
504 | { "wafer-test": { def: Test } }
505 | );
506 |
507 | expect(html.toString()).html.to.equal(
508 | `
509 |
510 |
511 |
512 | `
513 | );
514 | });
515 |
516 | it("will remove attribute if selecting attribute with null", async () => {
517 | class Test extends Wafer {
518 | static template = '';
519 | static props = {
520 | active: {
521 | type: Number,
522 | reflect: true,
523 | targets: [
524 | {
525 | selector: "$div",
526 | attribute: "test",
527 | },
528 | ],
529 | },
530 | };
531 | }
532 |
533 | const html = await parse(
534 | `
535 |
536 | `,
537 | { "wafer-test": { def: Test } }
538 | );
539 |
540 | expect(html.toString()).html.to.equal(
541 | `
542 |
543 |
544 |
545 |
546 |
547 | `
548 | );
549 |
550 | const el = html.querySelector("wafer-test");
551 | el.active = null;
552 | await el.updateDone();
553 |
554 | expect(html.toString()).html.to.equal(
555 | `
556 |
557 |
558 |
559 |
560 |
561 | `
562 | );
563 | });
564 |
565 | it("will update elements outside component", async () => {
566 | class Test extends Wafer {
567 | static props = {
568 | title: {
569 | type: String,
570 | targets: [
571 | {
572 | selector: "@title",
573 | text: true,
574 | },
575 | ],
576 | },
577 | };
578 | }
579 |
580 | const html = await parse(
581 | `
582 |
583 |
584 | `,
585 | { "wafer-test": { def: Test } }
586 | );
587 |
588 | const el = html.querySelector("wafer-test");
589 | const titleEl = html.querySelector("title");
590 |
591 | el.title = "Yo!!";
592 | await el.updateDone();
593 |
594 | expect(titleEl).to.have.text("Yo!!");
595 | });
596 |
597 | it("should remove attributes that are not reflected if not being client side rendered (serverOnly)", async () => {
598 | class Test extends Wafer {
599 | static props = {
600 | title: {
601 | type: String,
602 | initial: "bar",
603 | },
604 | };
605 | }
606 |
607 | const html = await parse(
608 | `
609 |
610 | `,
611 | { "wafer-test": { def: Test, serverOnly: true } }
612 | );
613 |
614 | // title attribute removed since not reflected and not needed for client side hydration
615 | // also no wafer-ssr attribute present as not needed
616 | expect(html.toString()).html.to.equal(
617 | `
618 |
619 |
620 |
621 | `
622 | );
623 |
624 | const el = html.querySelector("wafer-test");
625 | expect(el.title).to.equal("foo");
626 | });
627 |
628 | it("should escape attribute values and textContent", async () => {
629 | class Test extends Wafer {
630 | static props = {
631 | title: {
632 | type: String,
633 | initial: "fo>o",
634 | targets: [
635 | {
636 | selector: "$span",
637 | text: true,
638 | attribute: "test2",
639 | },
640 | ],
641 | },
642 | };
643 |
644 | static get template() {
645 | return "";
646 | }
647 | }
648 |
649 | const html = await parse(
650 | `
651 |
652 | `,
653 | { "wafer-test": { def: Test } }
654 | );
655 |
656 | expect(html.toString()).html.to.equal(
657 | `
658 |
659 | fo>o
660 |
661 | `
662 | );
663 | });
664 | });
665 |
--------------------------------------------------------------------------------
/test/testing.js:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai';
2 | import sinonChai from 'sinon-chai';
3 | import chaiHTML from 'chai-html';
4 | import chaiDOM from 'chai-dom';
5 |
6 | chai.use(sinonChai);
7 | chai.use(chaiHTML);
8 | chai.use(chaiDOM);
9 |
10 | export { expect };
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "checkJs": true,
5 | "target": "esnext",
6 | "module": "esnext",
7 | "strict": true,
8 | "moduleResolution": "node",
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "declaration": true,
13 | "outDir": "lib"
14 | },
15 | "include": ["src/**/*.js"],
16 | "exclude": ["node_modules"],
17 | "typedocOptions": {
18 | "entryPoints": ["src/"],
19 | "excludeExternals": true,
20 | "out": "../wafer-docs/api"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/typedoc-theme/partials/header.hbs:
--------------------------------------------------------------------------------
1 |
125 |
126 |
127 |
128 |
154 |
155 |
156 |
158 |
159 |
161 |
162 |
163 |
164 |
165 | - Preparing search index...
166 | - The search index is not available
167 |
168 |
169 |
{{project.name}}
170 |
171 |
172 |
201 |
202 |
203 |
204 |
205 |
206 | {{#if model.parent}} {{! Don't show breadcrumbs on main project page, it is the root page. !}}
207 |
208 | {{#with model}}{{> breadcrumb}}{{/with}}
209 |
210 | {{/if}}
211 |
{{#compact}}
212 | {{#ifCond model.kindString "!==" "Project" }}
213 | {{model.kindString}}
214 | {{/ifCond}}
215 | {{model.name}}
216 | {{#if model.typeParameters}}
217 | <
218 | {{#each model.typeParameters}}
219 | {{#if @index}}, {{/if}}
220 | {{name}}
221 | {{/each}}
222 | >
223 | {{/if}}
224 | {{/compact}}
225 |
226 |
227 |
228 |
--------------------------------------------------------------------------------
/web-test-runner.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | coverageConfig: {
3 | reporters: ["html", "text"],
4 | report: true,
5 | reportDir: "coverage",
6 |
7 | threshold: {
8 | statements: 70,
9 | branches: 40,
10 | functions: 70,
11 | lines: 70,
12 | },
13 | },
14 | };
15 |
--------------------------------------------------------------------------------