├── .github
└── workflows
│ └── publish-docs.yml
├── .gitignore
├── LICENSE
├── README.md
├── guides
├── guides.json
└── slots.md
├── index.js
├── jsdoc.config.json
├── package-lock.json
├── package.json
└── readme_files
├── docstyle.css
└── svawc logo.png
/.github/workflows/publish-docs.yml:
--------------------------------------------------------------------------------
1 | name: GitHub pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v2
14 |
15 | - name: Install node
16 | uses: actions/setup-node@v2
17 | with:
18 | node-version: 'lts/*'
19 | cache: 'npm'
20 |
21 | - name: Install deps
22 | run: npm ci
23 |
24 | - name: Build docs
25 | run: npm run doc
26 |
27 | - name: Deploy
28 | uses: peaceiris/actions-gh-pages@v3
29 | with:
30 | github_token: ${{ secrets.GITHUB_TOKEN }}
31 | publish_dir: ./doc
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | doc
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Immers Space
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 | # SVAWC
4 |
5 | **Sv**elte **A**-Frame **W**eb **C**omponents
6 |
7 | SVAWC brings modern reactive development and HTML templating to A-Frame component development without compromising on speed, usability, or bundle size.
8 |
9 | ## How it works
10 |
11 | 1. Write reactive template code using Svelte
12 | 1. Svelte compiles that down to efficient `createElement`, `setAttibute`, et c. calls (no virtual DOM or unecessary entity recreation)
13 | 2. SVAWC packages it into Web Components for distribution
14 | 3. Link the packaged script and then use the Web Component in any A-Frame scene, works with bundled apps and vanilla JS & HTML
15 |
16 | ## What it looks like
17 |
18 | **Svelte reactive template source:**
19 |
20 | ```Svelte
21 |
22 |
33 |
34 |
41 |
48 |
49 | {#each limbs as side (side)}
50 |
58 | {/each}
59 |
60 | {#each limbs as side (side)}
61 |
69 | {/each}
70 | ```
71 |
72 | The above is just standard Svelte code.
73 | [Check out their guide](https://svelte.dev/tutorial/basics) if you're not already familiar.
74 |
75 | **SVAWC Wrapper:**
76 |
77 | ```js
78 | import { registerWebComponent } from 'svawc'
79 | import APerson from "./APerson.svelte"
80 | registerWebComponent({Component: APerson, tagname: "a-person", props: ["skinColor", "shirtColor", "pantsColor"] })
81 | ```
82 |
83 |
84 | Usage in A-Frame Scene:
85 |
86 | ```html
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | ```
99 |
100 | [Try it out](https://momentous-jelly-secure.glitch.me/)
101 |
102 | ## Why it's useful
103 |
104 | I love A-Frame, but the recurring pain points for me in large apps are handling complex reactive state
105 | and making nested entity structures re-usable.
106 |
107 | Solutions for the reactive state generally involve meta-components
108 | like `event-set` or the creation of one-off 'components' that just handle business logic.
109 | These tend to spread your logic around and make a large codebase harder to maintain.
110 | For re-usable structures, you're either stuck with HTML templates, which are awkward to use, bloat your index.html,
111 | and again serve to keep your structure far from your logic, or you've got to write tons of tedious
112 | `createElement` and `setAttribute` calls.
113 |
114 | SVAWC lets you write the organized, concise code we're accustomed to from modern
115 | reactive frameworks and integrate it seamlessly in any A-Frame project. SVAWC is
116 | the A-Frame answer to React Three Fiber, which is a lovely and powerful framework,
117 | but never feels quite right to me due the lack of ECS.
118 |
119 | ## API documentation
120 |
121 | View the full API documentation at
122 | [https://immers-space.github.io/svawc](https://immers-space.github.io/svawc)
123 |
124 | ## Get Started
125 |
126 | The [svawc-template repo](https://github.com/immers-space/svawc-template/) has everything you need to start building and publishing SVAWCs.
127 | [Click here to create a copy of it](https://github.com/immers-space/svawc-template/generate).
128 |
129 | ## Feature status
130 |
131 | This library is fully functional, but some of the features still need some polish
132 |
133 |
134 | - 🙂 Svelte props as HTML Attributes
135 | -
136 | Svelte props become attributes on the custom element, converting camelCase to dash-case
137 | automatically. For now, the props must be explicitly listed in the `props` option, but
138 | I'd like to be able to infer them automatically in the future.
139 |
140 | - 😀 Light DOM
141 | -
142 | All component output is rendered to the light DOM as children of the custom element.
143 | Shadow DOM is not available as the boundary breaks A-Frame's scene graph logic,
144 | and the benefits of Shadow DOM are primarily CSS encapsulation which isn't relevant here.
145 |
146 | - 😀 Slots
147 | -
148 | Full slot functionality is available including default and named slots with the small caveat
149 | that the slot content must be wrapped in a
template
tag.
150 | See slots tutorial for details.
151 |
152 | - 😦 Dependency Injection
153 | -
154 | Not available yet, but I'd like to have it work where dependencies on A-Frame components can be
155 | re-used from the consuming app if already installed or injected via CDN if not so that we don't
156 | have bundle extra code in our SVAWCs nor worry about duplicate component registration.
157 |
158 | - 😦 Loading order gotcha
159 | -
160 | SVAWC scripts mustn't be loaded as type="module", async, or defer as this puts it in
161 | conflict with the A-Frame loading order. Instead, load it as a normal script in your HEAD
162 | and it will work.
163 | (This is now fixed in A-Frame source, and will be available in the next release after 1.4.1 or you can use the current master build)
164 |
165 |
166 |
167 | Key: 😀 complete, 🙂 fully functional but could be improved, 😦 missing or has issues
168 |
169 | ## Acknowledgements
170 |
171 | Big thanks to @dmarcos for undertaking the massive task of porting A-Frame over to
172 | native Custom Elements for v1.4.0; this would not be possible otherwise.
173 |
174 | Code adapted from [svelte-tag](https://github.com/crisward/svelte-tag) by Chris Ward.
175 |
176 | Logo is CC-BY-NC-SA, adapted from a photo by Leonard J Matthews.
177 |
--------------------------------------------------------------------------------
/guides/guides.json:
--------------------------------------------------------------------------------
1 | {
2 | "slots": {
3 | "title": "Using Slots"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/guides/slots.md:
--------------------------------------------------------------------------------
1 | Slots are an important feature for customization and composability.
2 | You can specify locations in your SVAWC's entity tree where
3 | the users of your component can provide their own entities.
4 | For example, a humanoid character SVAWC might have a slot placed inside their
5 | hand entity so that different items can be easily inserted where they'll
6 | be parented to the hand and move along with it.
7 |
8 | ### Defining SVAWC Slots
9 |
10 | On the SVAWC development side, slots work exactly as they do in
11 | Svelte components ([see guide](https://svelte.dev/tutorial/slots)).
12 | All you do is place `` a element in your component's HTML template to
13 | mark the place where content can be inserted.
14 | You can also place default content inside the slot tags that will be used
15 | only when custom content is not provided by the SVAWC user.
16 |
17 | This `a-cage` SVAWC creates a wireframe box around whatever content
18 | is placed in its slot:
19 |
20 | ```svelte
21 |
22 |
23 |
24 |
25 | ```
26 |
27 | ### Using SVAWC Slots
28 |
29 | On the SVAWC usage side, there is one difference from how slots are used
30 | in Svelte or HTML WebComponents.
31 | You still add entities as children of the SVAWC to populate the slots,
32 | but you must wrap it in a `` tag.
33 | This is to prevent the A-Frame entities from initializing until after
34 | they are moved to their slotted locations.
35 |
36 | This scene places a sphere within the `a-cage` SVAWC
37 |
38 | ```html
39 |
40 |
41 |
42 |
43 |
44 | ```
45 |
46 | ### Multiple slots
47 |
48 | Just as in Svelte or WebComponents, you can have multiple slots
49 | that are differentiate with the `name` attribute, and you can
50 | have one default slot that doesn't have a name. Using
51 | these slots also works just like standard slots, using
52 | the `slot` attribute on the to specify the name.
53 |
54 | This `a-camera-rig` SVAWC has named slots to attach
55 | entities to the camera and each tracked controller
56 | as well as a default slot to place content relative to the entire rig
57 |
58 | ```svelte
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ```
71 |
72 | ```html
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | ```
88 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | Adapted from svelte-tag by Chris Ward
3 | */
4 |
5 | // witchcraft from svelte issue - https://github.com/sveltejs/svelte/issues/2588
6 | import { detach, insert, noop } from 'svelte/internal'
7 | function createSlots (slots) {
8 | const svelteSlots = {}
9 | for (const slotName in slots) {
10 | svelteSlots[slotName] = [createSlotFn(slots[slotName])]
11 | }
12 | function createSlotFn (element) {
13 | return function () {
14 | return {
15 | c: noop,
16 | m: function mount (target, anchor) {
17 | insert(target, element.cloneNode(true), anchor)
18 | },
19 | d: function destroy (detaching) {
20 | if (detaching && element.innerHTML) {
21 | detach(element)
22 | }
23 | },
24 | l: noop
25 | }
26 | }
27 | }
28 | return svelteSlots
29 | }
30 | /**
31 | * Convert a Svelte component into a Web Component for A-Frame.
32 | *
33 | * Provides the wrapperElement context to the Svelte component with a reference to the containing
34 | * custom element instance.
35 | * @param {object} opts
36 | * @param {function} opts.Component - the Svelte component constructor
37 | * @param {string} opts.tagname - the element tag for the Web Component, must contain a '-'
38 | * @param {string[]} [opts.props] - the prop names from the Svelte copmonent which will be settable via HTML attributes (auto-converted between camelCase an dash-case)
39 | * @param {HTMLElement} [opts.baseClass] - base class that Web Component element will inherit from, default's to AEntity
40 | * @param {boolean} [opts.noWraper] - EXPERIMENTAL: render the Svelte component output as siblings to the Web Component element instead of as children
41 | * @example Basic usage
42 | * // creates and registers the custom element from the APerson.svelte component
43 | * import { registerWebComponent } from 'svawc'
44 | * import APerson from "./APerson.svelte"
45 | * registerWebComponent({ component: APerson, tagname: "a-person", attributes: ["skin", "shirt", "pants"] })
46 | * @example Using context to modify the containing element from inside the Svelte component
47 | * import { getContext } from "svelte";
48 | * getContext('wrapperElement').setAttribute('shadow', '')
49 | */
50 | export function registerWebComponent (opts) {
51 | const BaseClass = opts.baseClass ?? window.AFRAME.AEntity
52 | opts.props ??= []
53 | // setup camel/dash case conversions
54 | const attributes = opts.props.map(prop => dashify(prop))
55 | const toDash = Object.fromEntries(opts.props.map((prop, i) => [prop, attributes[i]]))
56 | const toCamel = Object.fromEntries(opts.props.map((prop, i) => [attributes[i], prop]))
57 | class Wrapper extends BaseClass {
58 | constructor () {
59 | super()
60 | this.addEventListener('nodeready', () => this.init())
61 | }
62 |
63 | static get observedAttributes () {
64 | return (attributes).concat(BaseClass.observedAttributes || [])
65 | }
66 |
67 | // use init on nodeready instead of connectedCallback to avoid
68 | // issues with multiple calls due to A-Frame's initialization delay
69 | init () {
70 | const props = {}
71 | props.$$scope = {}
72 | opts.props.forEach(prop => {
73 | if (this.hasAttribute(toDash[prop])) {
74 | props[prop] = this.getAttribute(toDash[prop])
75 | }
76 | })
77 | props.$$scope = {}
78 | const slots = this.getSlots()
79 | props.$$slots = createSlots(slots)
80 | const context = new Map([['wrapperElement', opts.noWrapper ? null : this]])
81 | this.elem = new opts.Component({ target: opts.noWrapper ? this.parentElement : this, props, context })
82 | }
83 |
84 | disconnectedCallback () {
85 | super.disconnectedCallback?.()
86 | if (this.observe) {
87 | this.observer.disconnect()
88 | }
89 | try { this.elem.$destroy() } catch (err) {} // detroy svelte element when removed from dom
90 | }
91 |
92 | unwrap (from) {
93 | if (!from.content) {
94 | console.warn('svawc: entities in slots should be wrapped in a template element')
95 | const frag = document.createDocumentFragment()
96 | frag.appendChild(from)
97 | return frag
98 | }
99 | from.remove()
100 | return from.content
101 | }
102 |
103 | getSlots () {
104 | const namedSlots = this.querySelectorAll('[slot]')
105 | const slots = {}
106 | namedSlots.forEach(n => {
107 | slots[n.slot] = this.unwrap(n)
108 | })
109 | const defaultSlot = this.firstElementChild
110 | if (defaultSlot) {
111 | slots.default = this.unwrap(defaultSlot)
112 | } else if (this.textContent.trim().length) {
113 | // if the only child is text, wrap in fragment and use as default slot
114 | slots.default = document.createDocumentFragment()
115 | slots.default.textContent = this.textContent.trim()
116 | }
117 | this.innerHTML = ''
118 | return slots
119 | }
120 |
121 | attributeChangedCallback (name, oldValue, newValue) {
122 | if (!attributes.includes(name)) {
123 | // passthrough for inherited attrs
124 | return super.attributeChangedCallback?.()
125 | }
126 | if (this.elem && newValue !== oldValue) {
127 | this.elem.$set({ [toCamel[name]]: newValue })
128 | }
129 | }
130 | }
131 | window.customElements.define(opts.tagname, Wrapper)
132 | return Wrapper
133 | }
134 |
135 | /*!
136 | * dashify
137 | *
138 | * Copyright (c) 2015-2017, Jon Schlinkert.
139 | * Released under the MIT License.
140 | */
141 |
142 | function dashify (str, options) {
143 | if (typeof str !== 'string') throw new TypeError('expected a string')
144 | return str.trim()
145 | .replace(/([a-z])([A-Z])/g, '$1-$2')
146 | .replace(/\W/g, m => /[À-ž]/.test(m) ? m : '-')
147 | .replace(/^-+|-+$/g, '')
148 | .replace(/-{2,}/g, m => options && options.condense ? '-' : m)
149 | .toLowerCase()
150 | };
151 |
--------------------------------------------------------------------------------
/jsdoc.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "tags": {
3 | "allowUnknownTags": ["category", "optional"]
4 | },
5 |
6 | "plugins": [
7 | "plugins/markdown",
8 | "node_modules/better-docs/category",
9 | "node_modules/better-docs/typedef-import"
10 | ],
11 | "opts": {
12 | "encoding": "utf8",
13 | "destination": "doc/",
14 | "readme": "README.md",
15 | "recurse": true,
16 | "verbose": true,
17 | "tutorials": "guides",
18 | "template": "node_modules/better-docs"
19 | },
20 | "templates": {
21 | "cleverLinks": false,
22 | "monospaceLinks": false,
23 | "search": false,
24 | "default": {
25 | },
26 | "better-docs": {
27 | "name": "SVAWC Documentation",
28 | "title": "SVAWC Documentation",
29 | "css": "readme_files/docstyle.css",
30 | "trackingCode": "",
31 | "hideGenerator": false,
32 | "navLinks": [
33 | {
34 | "label": "Github",
35 | "href": "https://github.com/immers-space/svawc"
36 | }
37 | ]
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svawc",
3 | "version": "0.0.2",
4 | "description": "Modern reactive development and HTML templating to A-Frame component development without compromising on speed, usability, or bundle size",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "doc": "jsdoc -c jsdoc.config.json -d doc -r index.js -R README.md && cp -R readme_files doc/"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/immers-space/svawc.git"
13 | },
14 | "keywords": [
15 | "aframe",
16 | "svelte",
17 | "web",
18 | "components"
19 | ],
20 | "author": "William Murphy",
21 | "license": "MIT",
22 | "peerDependencies": {
23 | "aframe": "^1.4.1",
24 | "svelte": "^3.55.1"
25 | },
26 | "devDependencies": {
27 | "better-docs": "^2.7.2",
28 | "jsdoc": "^4.0.0",
29 | "standard": "^17.0.0",
30 | "taffydb": "^2.7.3"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/readme_files/docstyle.css:
--------------------------------------------------------------------------------
1 | .tag:not(body) {
2 | text-transform: none;
3 | color: #c66;
4 | background: none;
5 | }
6 |
--------------------------------------------------------------------------------
/readme_files/svawc logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/immers-space/svawc/0822c8a836b4eae4f518083166e5685ee4dbe9c2/readme_files/svawc logo.png
--------------------------------------------------------------------------------