Skip to content

Commit

Permalink
Support SVG templates (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewp authored Sep 27, 2022
1 parent 85e1fa0 commit ef19155
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-zebras-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"corset": minor
---

Allow using SVG templates
86 changes: 86 additions & 0 deletions examples/clock/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import sheet, { mount } from '../../src/main.js';

const getSecondsSinceMidnight = () => (Date.now() - new Date().setHours(0, 0, 0, 0)) / 1000;
const rotateFixed = (index, length) => `rotate(${(360 * index) / length})`;
const rotateDyn = (rotate, fixed = 1) => `rotate(${(rotate * 360).toFixed(fixed)})`;

mount(document, class {
time = getSecondsSinceMidnight();

bind() {
return sheet`
.lines {
--rotate: ${rotateFixed};
}
.dyn {
--rotate: ${rotateDyn};
}
.hours {
each-template: select(#hand-tmpl);
each-items: ${new Array(12)};
--num-of-lines: 12;
--length: 5;
--width: 2;
}
.subseconds {
each-template: select(#hand-tmpl);
each-items: ${new Array(60)};
--num-of-lines: 60;
--length: 2;
--width: 1;
}
.lines line {
--rotation: --rotate(index(), var(--num-of-lines));
class-toggle: fixed true;
}
.hand.dyn {
attach-template: select(#hand-tmpl);
}
.hour {
--rotation: --rotate(${((this.time / 60 / 60) % 12) / 12});
--length: 50;
--width: 4;
}
.minute {
--rotation: --rotate(${((this.time / 60) % 60) / 60});
--length: 70;
--width: 3;
}
.second {
--rotation: --rotate(${(this.time % 60) / 60});
--length: 80;
--width: 2;
}
.subsecond {
--rotation: --rotate(${this.time % 1});
--length: 85;
--width: 5;
}
line {
attr:
stroke-width var(--width),
transform var(--rotation);
}
line.fixed {
attr:
y1 get(var(--length), ${len => len - 95}),
y2 ${-95};
}
line:not(.fixed) {
attr: y2 var(--length);
}
`;
}
});
33 changes: 33 additions & 0 deletions examples/clock/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<meta name="description" content="Clock Example">
<meta charset="utf-8">
<title>Clock</title>
<link rel="stylesheet" href="./styles.css">
<script type="module" src="./app.js"></script>

<div class="clock">
<svg viewBox="0 0 200 200" width="95vh">
<g transform="translate(100, 100)">
{/* static */}
<circle class="text-neutral-900" r="99" fill="white" stroke="currentColor" />
<g class="hours lines"></g>
<g class="subseconds lines"></g>
{/* dynamic */}
<g class="hour hand dyn"></g>
<g class="minute hand dyn"></g>
<g class="second hand dyn"></g>
<g class="subsecond hand dyn"></g>
</g>
</svg>
</div>

<svg id="hand-tmpl">
<defs>
<g>
<line
stroke="currentColor"
stroke-linecap="round" />
</g>
</defs>
</svg>
20 changes: 20 additions & 0 deletions examples/clock/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.clock {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
height: 100vh;
}

.subsecond {
color: silver;
}

.hour,
.minute {
color: black;
}

.second {
color: tomato;
}
10 changes: 6 additions & 4 deletions src/each.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check
import { addItemToScope } from './scope.js';
import { createCloneElement } from './element.js';
const eachSymbol = Symbol.for('corset.each');
const itemSymbol = Symbol.for('corset.item');
const indexSymbol = Symbol.for('corset.index');
Expand All @@ -18,14 +19,16 @@ export class EachInstance {
keyMap = new Map();
/**
* @param {HostElement} host
* @param {HTMLTemplateElement} template
* @param {HTMLTemplateElement | SVGElement} template
* @param {string} key
*/
constructor(host, template, key) {
/** @type {HostElement} */
this.host = host;
/** @type {HTMLTemplateElement} */
/** @type {HTMLTemplateElement | SVGElement} */
this.template = template;
/** @type {() => DocumentFragment} */
this.clone = createCloneElement(template).bind(null, host, template);
/** @type {string} */
this.key = key;
/** @type {(item: any, index: number) => any} */
Expand Down Expand Up @@ -85,9 +88,8 @@ export class EachInstance {
* @returns
*/
render(index, value) {
let doc = this.host.ownerDocument || document;
/** @type {EachFragment} */
let frag = /** @type {EachFragment} */(doc.importNode(this.template.content, true));
let frag = /** @type {EachFragment} */(this.clone());
frag.nodes = Array.from(frag.childNodes);
frag.data = { item: value, index };
this.setData(frag, value, index);
Expand Down
51 changes: 51 additions & 0 deletions src/element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// @ts-check

/**
* @typedef {import('./types').HostElement} HostElement
*/

/**
* @param {HostElement} host
*/
function getDocument(host) {
return host.ownerDocument || host;
}

/**
* @param {HostElement} host
* @param {HTMLTemplateElement} template
* @returns {DocumentFragment}
*/
export function cloneTemplate(host, template) {
return getDocument(host).importNode(template.content, true);
}

/**
* @param {HostElement} host
* @param {SVGElement} element
* @returns {DocumentFragment}
*/
export function cloneSVG(host, element) {
let g = element.firstElementChild?.firstElementChild?.cloneNode(true);
let frag = getDocument(host).createDocumentFragment();
while(g?.firstChild) {
frag.append(g.firstChild);
}
return frag;
}

/**
* @param {HTMLTemplateElement | SVGElement} template
* @returns {(host: HostElement, a: any) => DocumentFragment}
*/
export function createCloneElement(template) {
return template.nodeName === 'TEMPLATE' ? cloneTemplate : cloneSVG;
}

/**
* @param {HostElement} host
* @param {HTMLTemplateElement | SVGElement} template
*/
export function cloneElement(host, template) {
return createCloneElement(template)(host, template);
}
6 changes: 3 additions & 3 deletions src/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { flags } from './property.js';
import { EachInstance } from './each.js';
import { cloneElement } from './element.js';
import { Mountpoint } from './mount.js';
import { lookup, addItemToScope, removeItemFromScope } from './scope.js';
import { storePropName, storeDataSelector, Store } from './store.js';
Expand Down Expand Up @@ -64,7 +65,7 @@ function render(element, bindings, root, changeset) {
}

if(bflags & flags.custom) {
if(!(element instanceof HTMLElement)) {
if(!((element instanceof HTMLElement) || (element instanceof SVGElement))) {
throw new Error('Custom properties cannot be used on non-HTML elements.');
}

Expand Down Expand Up @@ -129,8 +130,7 @@ function render(element, bindings, root, changeset) {
let result = binding.update(changeset);
if(Array.isArray(result)) result = result[0];
if(result === undefined) break attach;
let doc = element.ownerDocument || document;
let frag = doc.importNode(result.content, true);
let frag = cloneElement(element, result);
element.replaceChildren(frag);
invalid = true;
changeset.flags |= flags.attach;
Expand Down
4 changes: 2 additions & 2 deletions src/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function lookup(element, dataSelector, propName) {

/**
*
* @param {HTMLElement} element
* @param {HTMLElement | SVGElement} element
* @param {symbol} listSym
* @param {string} key
* @param {symbol} keySym
Expand All @@ -45,7 +45,7 @@ export function lookup(element, dataSelector, propName) {

/**
*
* @param {HTMLElement} element
* @param {HTMLElement | SVGElement} element
* @param {symbol} listSym
* @param {string} key
* @param {symbol} keySym
Expand Down

0 comments on commit ef19155

Please sign in to comment.