Skip to content

Instantly share code, notes, and snippets.

@acutmore
Last active December 7, 2023 00:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save acutmore/154fa3d4a17c3e124eb9ea0e7b69ae1d to your computer and use it in GitHub Desktop.
Save acutmore/154fa3d4a17c3e124eb9ea0e7b69ae1d to your computer and use it in GitHub Desktop.
Comparing web framework render functions

Web Framework Render Functions

A look at the different render functions created by different web-frameworks, for the same website.

Website is very minimal:

  • A large rectangle than can recursively render 9 more rectangles inside it
  • Can navigate around the rectangles (Cells) using the keyboard

Vanilla.js demo https://gistpreview.github.io/?154fa3d4a17c3e124eb9ea0e7b69ae1d/z_vanilla.html

Why this website design? Because it uses a lot of different template techniques all at the same time

  • Child elements
  • Recursive components
  • Loops/Lists
  • Conditional/Ifs
  • static & dynamic css classes
  • getting references to the DOM
  • DOM event listeners
  • Component event listeners

Framework Versions:

  • Angular: v9.0.0-rc.6
  • Ember: v3.15.0
  • React: v16.12.0
  • Svelte: v3.16.5
  • Vue: v2.6.10 and v3 pre-alpha

Notes:

  • Each example includes the original template, minified output and, a beautified output editied for readability (e.g. renamed variables, inlined functions)

Example render result:

<body>
    <div tabindex="-1" class="container cell">
        <div tabindex="-1" class="cell">
            <div tabindex="-1" class="cell">...</div>
            <div tabindex="-1" class="cell">...</div>
            <div tabindex="-1" class="cell">...</div>
            <div tabindex="-1" class="cell">...</div>
            <div tabindex="-1" class="cell">...</div>
            <div tabindex="-1" class="cell">...</div>
            <div tabindex="-1" class="cell">...</div>
            <div tabindex="-1" class="cell">...</div>
        </div>
        <div tabindex="-1" class="cell">...</div>
        <div tabindex="-1" class="cell">...</div>
        <div tabindex="-1" class="cell">...</div>
        <div tabindex="-1" class="cell">...</div>
        <div tabindex="-1" class="cell">...</div>
        <div tabindex="-1" class="cell">...</div>
        <div tabindex="-1" class="cell">...</div>
        <div tabindex="-1" class="cell">...</div>
    </div>
</body>
<ng-template [ngIf]="level > 0">
<app-cell *ngFor="let _ of [1, 2, 3, 4, 5, 6, 7, 8, 9]; index as i"
[level]="level - 1"
(back)="focus()"
(move)="onMove($event, i)"
/>
</app-cell>
</ng-template>
function n(n, e) {
if (1 & n) {
var a = N["ɵɵgetCurrentView"]();
N["ɵɵelementStart"](0, "app-cell", 2), N["ɵɵlistener"]("back", function(n) {
return N["ɵɵrestoreView"](a), N["ɵɵnextContext"](2).focus()
})("move", function(n) {
N["ɵɵrestoreView"](a);
var r = e.index;
return N["ɵɵnextContext"](2).onMove(n, r)
}), N["ɵɵelementEnd"]()
}
if (2 & n) {
var r = N["ɵɵnextContext"](2);
N["ɵɵproperty"]("level", r.level - 1)
}
}
function e(e, a) {
1 & e && N["ɵɵtemplate"](0, n, 1, 1, "app-cell", 1), 2 & e && N["ɵɵproperty"]("ngForOf", N["ɵɵpureFunction0"](1, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
}
Cell.ɵcmp = N["ɵɵdefineComponent"]({
hostBindings: function(n, e, a) {
1 & n && (N["ɵɵallocHostVars"](2), N["ɵɵlistener"]("keydown", function(n) {
return e.onKeyDown(n)
})), 2 & n && (N["ɵɵattribute"]("tabindex", e.tabIndex), N["ɵɵclassProp"]("container", e.containerClass))
},
outputs: {
back: "back",
move: "move"
},
decls: 1,
vars: 1,
consts: [
[3, "ngIf"],
[3, "level", "back", "move", 4, "ngFor", "ngForOf"],
[3, "level", "back", "move"]
],
template: function(n, a) {
1 & n && N["ɵɵtemplate"](0, e, 1, 2, "ng-template", 0), 2 & n && N["ɵɵproperty"]("ngIf", a.level > 0)
},
directives: [N_common.NgIf, N_common.NgForOf, Cell]
})
function Cell_1_Template(rf, ctx) {
if (rf & 1) {
var _r5 = _angular["ɵɵgetCurrentView"]();
_angular["ɵɵelementStart"](0, "app-cell", 2);
_angular["ɵɵlistener"]("back", function cell_back_listener($event) {
_angular["ɵɵrestoreView"](_r5);
var ctx_r4 = _angular["ɵɵnextContext"](2);
return ctx_r4.focus();
})("move", function cell_move_listener($event) {
_angular["ɵɵrestoreView"](_r5);
var i_r3 = ctx.index;
var ctx_r6 = _angular["ɵɵnextContext"](2);
return ctx_r6.onMove($event, i_r3);
});
_angular["ɵɵelementEnd"]();
}
if (rf & 2) {
var ctx_r1 = _angular["ɵɵnextContext"](2);
_angular["ɵɵproperty"]("level", ctx_r1.level - 1);
}
}
function Cell_0_Template(rf, ctx) {
if (rf & 1) {
_angular["ɵɵtemplate"](0, Cell_1_Template, 1, 1, "app-cell", 1);
}
if (rf & 2) {
_angular["ɵɵproperty"]("ngForOf", _angular["ɵɵpureFunction0"](1, [1, 2, 3, 4, 5, 6, 7, 8, 9]));
}
}
Cell.ɵcmp = _angular["ɵɵdefineComponent"]({
hostBindings: function Cell_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
_angular["ɵɵallocHostVars"](2);
_angular["ɵɵlistener"]("keydown", function Cell_keyDown($event) { return ctx.onKeyDown($event); });
}
if (rf & 2) {
_angular["ɵɵattribute"]("tabindex", ctx.tabIndex);
_angular["ɵɵclassProp"]("container", ctx.containerClass);
}
},
outputs: { back: "back", move: "move" },
decls: 1,
vars: 1,
consts: [[3, "ngIf"], [3, "level", "back", "move", 4, "ngFor", "ngForOf"], [3, "level", "back", "move"]],
template: function Cell_Template(rf, ctx) {
if (rf & 1) {
_angular["ɵɵtemplate"](0, Cell_0_Template, 1, 2, "ng-template", 0);
}
if (rf & 2) {
_angular["ɵɵproperty"]("ngIf", ctx.level > 0);
}
},
directives: [_angular_common["NgIf"], _angular_common["NgForOf"], Cell]
});
<div
tabindex="-1"
class="cell {{if @container "container"}}"
{{on "keydown" this.keydown}}
{{ref this "divRef"}}
>
{{#if renderChildren}}
{{#each nine as |n|}}
<Cell @level={{childrenLevel}} />
{{/each}}
{{/if}}
</div>
Ember.HTMLBars.template({
id: ...,
meta: ...,
block: "{\"symbols\":[\"n\",\"@container\"],\"statements\":[[7,\"div\",true],[10,\"tabindex\",\"-1\"],[11,\"class\",[29,[\"cell \",[28,\"if\",[[23,2,[]],\"container\"],null]]]],[8],[0,\"\\n\"],[4,\"if\",[[24,[\"renderChildren\"]]],null,{\"statements\":[[4,\"each\",[[24,[\"nine\"]]],null,{\"statements\":[[0,\" \"],[5,\"cell\",[],[[\"@level\"],[[22,\"childrenLevel\"]]]],[0,\"\\n\"]],\"parameters\":[1]},null]],\"parameters\":[]},null],[9],[0,\"\\n\"]],\"hasEval\":false}"
})
var x = {
symbols: ["n", "@container"],
statements: [
[7, "div", false],
[12, "tabindex", "-1"],
[
12,
"class",
[29, ["cell ", [28, "if", [
[23, 2, []], "container"
], null]]]
],
[3, "on", ["keydown", [23, 0, ["keydown"]]]],
[3, "ref", [
[23, 0, []], "divRef"
]],
[8],
[0, "\n"],
[
4,
"if",
[
[24, ["renderChildren"]]
],
null,
{
statements: [
[
4,
"each",
[
[24, ["nine"]]
],
null,
{
statements: [
[0, " "],
[5, "cell", [],
[
["@level"],
[
[22, "childrenLevel"]
]
]
],
[0, "\n"]
],
parameters: [1]
},
null
]
],
parameters: []
},
null
],
[9],
[0, "\n"]
],
hasEval: false
};
class Cell extends React.Component {
...
render() {
const { container, level } = this.props;
const innerCells = level > 0 && Nine.map((_, i) =>
<Cell
key={i}
ref={c => this.childrenRefs[i] = c}
level={level - 1}
onBack={this.focus}
onMove={m => this.move(m, i)}
/>
);
return <div
ref={this.ref}
onKeyDown={this.onKeyDown}
tabIndex="-1"
className={container ? "container cell" : "cell"} >
{innerCells}
</div>;
}
}
o.prototype.render = function() {
var r = this,
n = this.props,
e = n.container,
t = n.level,
u = 0 < t && q.map(function(n, e) {
return K.createElement(o, {
key: e,
ref: function(n) {
return r.childRefs[e] = n
},
level: t - 1,
onBack: r.focus,
onMove: function(n) {
return r.move(n, e)
}
})
});
return K.createElement("div", {
ref: this.divRef,
onKeyDown: this.onKeyDown,
tabIndex: "-1",
className: e ? "container cell" : "cell"
}, u)
}
Cell.prototype.render = function() {
var cell = this;
var children = 0 < cell.props.level && nine.map(function(_, index) {
return React.createElement(Cell, {
key: index,
ref: function(r) {
return cell.childRefs[index] = r
},
level: cell.props.level - 1,
onBack: cell.focus,
onMove: function(evt) {
return cell.move(evt, index)
}
})
});
return React.createElement("div", {
ref: this.divRef,
onKeyDown: this.onKeyDown,
tabIndex: "-1",
className: cell.props.container ? "container cell" : "cell"
}, children)
}
<div
bind:this={divRef}
class:container
class="cell"
tabindex="-1"
on:keydown={onKeyDown}>
{#if level > 0}
{#each nine as _, index}
<svelte:self
bind:this={childRefs[index]}
on:back={() => focus()}
on:move={e => move(e.detail, index)}
level={level - 1} />
{/each}
{/if}
</div>
function S(t, n, e) {
const o = t.slice();
return o[12] = n[e], o[14] = e, o
}
function q(t) {
let n, e, o = T, r = [];
for (let n = 0; n < o.length; n += 1) r[n] = F(S(t, o, n));
const c = t => L(r[t], 1, 1, () => {
r[t] = null
});
return {
c() {
for (let t = 0; t < r.length; t += 1) r[t].c();
n = s("")
},
m(t, o) {
for (let n = 0; n < r.length; n += 1) r[n].m(t, o);
i(t, n, o), e = !0
},
p(t, e) {
if (54 & e) {
let i;
for (o = T, i = 0; i < o.length; i += 1) {
const c = S(t, o, i);
r[i] ? (r[i].p(c, e), C(r[i], 1)) : (r[i] = F(c), r[i].c(), C(r[i], 1), r[i].m(n.parentNode, n))
}
for (_(), i = o.length; i < r.length; i += 1) c(i);
P()
}
},
i(t) {
if (!e) {
for (let t = 0; t < o.length; t += 1) C(r[t]);
e = !0
}
},
o(t) {
r = r.filter(Boolean);
for (let t = 0; t < r.length; t += 1) L(r[t]);
e = !1
},
d(t) {
! function (t, n) {
for (let e = 0; e < t.length; e += 1) t[e] && t[e].d(n)
}(r, t), t && u(n)
}
}
}
function F(t) {
let n, e = t[14];
const o = () => t[8](i, e),
r = () => t[8](null, e);
let c = {
level: t[1] - 1
};
const i = new G({
props: c
});
return o(), i.$on("back", t[9]), i.$on("move", (function (...n) {
return t[10]( t[14], ...n)
})), {
c() {
A(i.$$.fragment)
},
m(t, e) {
N(i, t, e), n = !0
},
p(n, c) {
e !== (t = n)[14] && (r(), e = t[14], o());
const u = {};
2 & c && (u.level = t[1] - 1), i.$set(u)
},
i(t) {
n || (C(i.$$.fragment, t), n = !0)
},
o(t) {
L(i.$$.fragment, t), n = !1
},
d(t) {
r(), j(i, t)
}
}
}
function M(t) {
let n, e, o, r = t[1] > 0 && q(t);
return {
c() {
var e, c, i, u, s;
e = "div", n = document.createElement(e), r && r.c(), l(n, "class", "cell svelte-bywmgz"), l(n, "tabindex", "-1"), a(n, "container", t[0]), c = n, i = "keydown", u = t[6], c.addEventListener(i, u, s), o = () => c.removeEventListener(i, u, s)
},
m(o, c) {
i(o, n, c), r && r.m(n, null)
, t[11](n), e = !0
},
p(t, [e]) {
t[1] > 0 ? r ? (r.p(t, e), C(r, 1)) : (r = q(t), r.c(), C(r, 1), r.m(n, null)) : r && (_(), L(r, 1, 1, () => {
r = null
}), P()), 1 & e && a(n, "container", t[0])
},
i(t) {
e || (C(r), e = !0)
},
o(t) {
L(r), e = !1
},
d(e) {
e && u(n), r && r.d()
, t[11](null), o()
}
}
}
function create_fragment(ctx) {
let div;
let current;
let dispose;
let if_block = /*level*/ ctx[1] > 0 && create_if_block(ctx);
return {
create() {
div = document.createElement("div");
if (if_block) if_block.create();
div.setAttribute("class", "cell svelte-bywmgz");
div.setAttribute(div, "tabindex", "-1");
div.classList.toggle("container", /*container*/ ctx[0]);
div.addEventListener("keydown", /*onKeyDown*/ ctx[6]);
dispose = () => div.removeEventListener("keydown", /*onKeyDown*/ ctx[6]);
},
mount(target, anchor) {
target.insertBefore(div, anchor || null);
if (if_block) if_block.mount(div, null);
/*div_binding*/ ctx[11](div);
current = true;
},
update(ctx, [dirty]) {
if (/*level*/ ctx[1] > 0) {
if (if_block) {
if_block.update(ctx, dirty);
if_block.intro(1);
} else {
if_block = create_if_block(ctx);
if_block.create();
if_block.intro(1);
if_block.mount(div, null);
}
} else if (if_block) {
group_outros();
transition_out(if_block, 1, 1, () => {
if_block = null;
});
check_outros();
}
if (dirty & /*container*/ 1) {
toggle_class(div, "container", /*container*/ ctx[0]);
}
},
intro(local) {
if (current) return;
if_block.intro();
current = true;
},
outro(local) {
outros.push(() => {
if_block.outro();
});
current = false;
},
destroy(detaching) {
if (detaching) detach(div);
if (if_block) if_block.destroy();
/*div_binding*/ ctx[11](null);
dispose();
}
};
}
function get_each_context(ctx, list, i) {
const child_ctx = ctx.slice();
child_ctx[12] = list[i];
child_ctx[14] = i;
return child_ctx;
}
// (115:2) {#if level > 0}
function create_if_block(ctx) {
let each_1_anchor;
let current;
let each_value = nine;
let each_blocks = [];
for (let i = 0; i < each_value.length; i += 1) {
each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i));
}
const out = i => transition_out(each_blocks[i], 1, 1, () => {
each_blocks[i] = null;
});
return {
create() {
for (let i = 0; i < each_blocks.length; i += 1) {
each_blocks[i].create();
}
each_1_anchor = empty();
},
mount(target, anchor) {
for (let i = 0; i < each_blocks.length; i += 1) {
each_blocks[i].mount(target, anchor);
}
insert(target, each_1_anchor, anchor);
current = true;
},
update(ctx, dirty) {
if (dirty & /*level, childRefs, focus, move*/ 54) {
each_value = nine;
let i;
for (i = 0; i < each_value.length; i += 1) {
const child_ctx = get_each_context(ctx, each_value, i);
if (each_blocks[i]) {
each_blocks[i].update(child_ctx, dirty);
each_blocks[i].intro(1);
} else {
each_blocks[i] = create_each_block(child_ctx);
each_blocks[i].create();
each_blocks[i].intro(1);
each_blocks[i].mount(each_1_anchor.parentNode, each_1_anchor);
}
}
group_outros();
for (i = each_value.length; i < each_blocks.length; i += 1) {
out(i);
}
check_outros();
}
},
intro(local) {
if (current) return;
for (let i = 0; i < each_value.length; i += 1) {
each_blocks[i].intro();
}
current = true;
},
outro(local) {
each_blocks = each_blocks.filter(Boolean);
for (let i = 0; i < each_blocks.length; i += 1) {
outros.push(() => each_blocks[i].outro());
}
current = false;
},
destroy(detaching) {
destroy_each(each_blocks, detaching);
if (detaching) detach(each_1_anchor);
}
};
}
// (116:4) {#each nine as _, index}
function create_each_block(ctx) {
let index = /*index*/ ctx[14];
let current;
function move_handler(...args) {
return /*move_handler*/ ctx[10](/*index*/ ctx[14], ...args);
}
let app_props = { level: /*level*/ ctx[1] - 1 };
const app = new App({ props: app_props });
ctx[8](app, index);
app.$on("back", /*back_handler*/ ctx[9]);
app.$on("move", move_handler);
return {
create() {
app.$$.fragment.create();
},
mount(target, anchor) {
app.$$.fragment.mounse( target, anchor);
add_render_callback(() => {
const new_on_destroy = app.$$.on_mount.map(run).filter(is_function);
app.$$.on_destroy.push(...new_on_destroy);
});
current = true;
},
update(new_ctx, dirty) {
ctx = new_ctx;
if (index !== /*index*/ ctx[14]) {
/*app_binding*/ ctx[8](null, index);
index = /*index*/ ctx[14];
ctx[8](app, index);
}
const app_changes = {};
if (dirty & /*level*/ 2) app_changes.level = /*level*/ ctx[1] - 1;
app.$set(app_changes);
},
intro(local) {
if (current) return;
app.$$.fragment.intro(local);
current = true;
},
outro(local) {
outros.push(() => app.$$.fragment.outro(local));
current = false;
},
destroy(detaching) {
/*app_binding*/ ctx[8](null, index);
run_all(app.on_destroy);
app.fragment.destroy(detaching);
}
};
}
<template>
<div ref="divRef" tabindex="-1" class="cell" :class="{ container }" @keydown="keydown">
<template v-if="level > 0">
<Cell
v-for="n in 9"
ref="childRefs"
:key="n"
:level="level - 1"
@back="focus"
@move="move($event, n - 1)"
/>
</template>
</div>
</template>
function() {
var e = this,
t = e.$createElement,
n = e._self._c || t;
return n("div", {
ref: "divRef",
staticClass: "cell",
class: { container: e.container },
attrs: { tabindex: "-1" },
on: { keydown: e.keydown }
}, [e.level > 0 ? e._l(9, (function(t) {
return n("Cell", {
key: t,
ref: "childRefs",
refInFor: !0,
attrs: { level: e.level - 1 },
on: {
back: e.focus,
move: function(n) {
return e.move(n, t - 1)
}
}
})
})) : e._e()], 2)
}
var cellRender = function() {
var $vm = this;
return $vm.$createElement('div', {
ref: "divRef",
staticClass: "cell",
class: { container: $vm.container },
attrs: { "tabindex": "-1" },
on: { "keydown": $vm.keydown }
}, [($vm.level > 0)
? $vm._loop(9, function(n) {
return $vm.$createElement('Cell', {
key: n,
ref: "childRefs",
refInFor: true,
attrs: { "level": $vm.level - 1 },
on: {
"back": $vm.focus,
"move": function($event) {
return $vm.move($event, n - 1)
}
}
})
})
: $vm._empty()
], /* normalizeChildren: true */ 2)
}
// from https://vue-next-template-explorer.netlify.com/
import { openBlock, renderList, createBlock, Fragment, resolveComponent, createVNode, createCommentVNode } from "vue"
export function render() {
const _ctx = this
const _cache = _ctx.$cache
const _component_Cell = resolveComponent("Cell")
return (openBlock(), createBlock("template", null, [
createVNode("div", {
ref: "divRef",
tabindex: "-1",
class: ["cell", { container: _ctx.container }],
onKeydown: _cache[2] || (_cache[2] = $event => (_ctx.keydown($event)))
}, [
(openBlock(), (_ctx.level > 0)
? createBlock(Fragment, { key: 0 }, renderList(9, (n) => {
return (openBlock(), createBlock(_component_Cell, {
ref: "childRefs",
key: n,
level: _ctx.level - 1,
onBack: _cache[1] || (_cache[1] = $event => (_ctx.focus($event))),
onMove: $event => (_ctx.move($event, n - 1))
}, null, 8 /* PROPS */, ["level", "onMove"]))
}), 128 /* KEYED_FRAGMENT */)
: createCommentVNode("v-if", true))
], 2 /* CLASS */)
]))
}
<!DOCTYPE html><html lang=en>
<head>
<title>Vanilla Grid</title>
<meta charset="UTF-8" />
<style>
.container {
width: 1280px !important;
height: 720px !important;
}
.cell {
display: flex;
display: -webkit-flex;
flex-wrap: wrap;
-webkit-flex-wrap: wrap;
border: 1px solid purple;
width: 30%;
height: 30%;
justify-content: space-around;
-webkit-justify-content: space-around;
align-content: space-around;
-webkit-align-content: space-around;
box-sizing: border-box;
}
.cell:focus {
outline: none;
border: 1px solid greenyellow;
}
</style>
</head>
<body>
Use keyboard keys: 0,1,2,3,4,enter,backspace,up,down,left,right
<div class="container cell">
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell"></div>
</div>
<script>
void function main() {
'use strict';
var toArray = Function.prototype.call.bind(Array.prototype.slice);
var container = document.body.querySelector(".container");
var TEMPLATE = container.innerHTML;
container.tabIndex = "-1";
document.body.addEventListener('keydown', function(e) {
if (e.keyCode === 13 /* enter */) {
container.focus();
}
var n = ({
48: 0,
49: 1,
50: 2,
51: 3,
52: 4,
})[e.keyCode];
typeof n === 'number' && createGrid(n);
});
createGrid(1);
function createGrid(n) {
document.activeElement.blur();
n = Math.max(n, 0); // 9^0 = 1 cell
n = Math.min(n, 4); // 9^4 = 6,562 cells
container.innerHTML = '';
for (var i = 0; i < n; i++) {
toArray(document.body.querySelectorAll(".cell")).forEach(function (cell, i) {
if (cell.innerHTML.length > 0) {
return;
}
cell.innerHTML = TEMPLATE;
});
}
// yield to render
setTimeout(function attachListeners() {
toArray(document.body.querySelectorAll(".cell")).forEach(function (cell, i) {
cell.tabIndex = "-1";
var indexOfParent = toArray(cell.parentElement.children)
.indexOf(cell);
var siblings = cell.parentElement.children;
cell.addEventListener("keydown", function (e) {
e.remainder || (e.remainder = []);
var indexToFocus = null;
switch (e.keyCode) {
case 13 /* Enter */: {
e.stopPropagation();
cell.firstElementChild.focus();
return;
}
case 8 /* Backspace */: {
e.stopPropagation();
cell.parentElement.focus();
return;
}
case 39 /* ArrowRight */:
indexToFocus = indexOfParent + 1;
break;
case 37 /* ArrowLeft */:
indexToFocus = indexOfParent - 1;
break;
case 38 /* ArrowUp */:
indexToFocus = indexOfParent - 3;
break;
case 40 /* ArrowDown */:
indexToFocus = indexOfParent + 3;
break;
default:
break;
}
if (indexToFocus != null) {
if (indexToFocus < 0 || indexToFocus >= 9) {
e.remainder.push((9 + (indexToFocus % 9)) % 9);
return;
}
e.stopPropagation();
e.preventDefault();
var next = siblings.item(indexToFocus);
while (e.remainder.length > 0) {
next = next.children.item(e.remainder.pop());
}
next.focus();
}
});
});
container.focus();
});
}
}();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment