all repos — h3 @ 3c61487e72e963e98ffcf4b9d102f4e1546d18eb

A tiny, extremely minimalist JavaScript microframework.

Updated redrawing algorithm to minimize number of DOM operations.
h3rald h3rald@h3rald.com
Tue, 09 Jun 2020 14:12:56 +0200
commit

3c61487e72e963e98ffcf4b9d102f4e1546d18eb

parent

05684fd06bc3f6600e89a3be707a472c1b651b40

M __tests__/h3.js__tests__/h3.js

@@ -32,7 +32,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,

@@ -91,7 +90,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: "test",

@@ -102,7 +100,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,

@@ -123,7 +120,6 @@ classList: ["a", "b", "c"],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "div",

@@ -141,7 +137,6 @@ data: {},

attributes: {}, eventListeners: {}, id: "test", - $key: undefined, $html: undefined, style: undefined, type: "div",

@@ -160,7 +155,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",

@@ -173,7 +167,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",

@@ -185,7 +178,6 @@ classList: ["test"],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,

@@ -205,7 +197,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",

@@ -218,7 +209,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",

@@ -228,7 +218,6 @@ ],

data: {}, eventListeners: {}, id: "test", - $key: undefined, $html: undefined, style: undefined, value: undefined,

@@ -244,7 +233,6 @@ children: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: "AAA",

@@ -263,7 +251,6 @@ eventListeners: {

click: fn, }, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,

@@ -289,7 +276,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",

@@ -305,7 +291,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",

@@ -317,7 +302,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,

@@ -332,7 +316,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",

@@ -344,7 +327,6 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,

@@ -354,7 +336,6 @@ classList: ["test"],

data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,
M __tests__/vnode.js__tests__/vnode.js

@@ -7,7 +7,6 @@ const obj = {

id: "test", type: "input", value: "AAA", - $key: "123", $html: "", data: { a: "1", b: "2" }, eventListeners: { click: fn },

@@ -20,7 +19,6 @@ const vnode1 = h3("br");

vnode1.from(obj); const vnode2 = h3("input#test.a1.a2", { value: "AAA", - $key: "123", $html: "", data: { a: "1", b: "2" }, onclick: fn,

@@ -336,10 +334,11 @@ expect(n.classList.value).toEqual("vn2");

const vn3 = h3("ul", [h3("span.vn3", { $onrender })]); vn1.redraw({ node: n1, vnode: vn3 }); expect(n.classList.value).toEqual("vn3"); - vn2.render(); - expect(n.classList.value).toEqual("vn2"); const rc = () => h3("div.rc", { $onrender }); await h3.init(rc); expect(n.classList.value).toEqual("rc"); + const rc2 = () => vn2; + await h3.init(rc2); + expect(n.classList.value).toEqual("vn2"); }); });
M docs/H3_DeveloperGuide.htmdocs/H3_DeveloperGuide.htm

@@ -7884,7 +7884,6 @@ <ul>

<li>Any attribute starting with <em>on</em> (e.g. onclick, onkeydown, etc.) will be treated as an event listener.</li> <li>The <code>classList</code> attribute can be set to a list of classes to apply to the element (as an alternative to using the element selector shorthand).</li> <li>The <code>data</code> attribute can be set to a simple object containing <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes">data attributes</a>.</li> -<li>The special <code>$key</code> attribute can be used to guarantee the uniqueness of two VNodes and it will not be translated into an HTML attribute.</li> <li>The special <code>$html</code> attribute can be used to set the <code>innerHTML</code> property of the resulting HTML element. Use only if you know what you are doing!</li> <li>The special <code>$onrender</code> attribute can be set to a function that will executed every time the VNode is rendered and added to the DOM.</li> </ul>

@@ -8051,7 +8050,7 @@ </ul>

</div> <div id="footer"> - <p><span class="copy"></span> Fabio Cevasco &ndash; June 8, 2020</p> + <p><span class="copy"></span> Fabio Cevasco &ndash; June 9, 2020</p> <p><span>Powered by</span> <a href="https://h3rald.com/hastyscribe"><span class="hastyscribe"></span></a></p> </div> </div>
M docs/example/assets/js/components/Todo.jsdocs/example/assets/js/components/Todo.js

@@ -2,18 +2,18 @@ import h3 from "../h3.js";

export default function Todo(data) { const todoStateClass = data.done ? ".done" : ".todo"; - const toggleTodo = (todo) => { - h3.dispatch("todos/toggle", data); - h3.redraw() + const toggleTodo = (key) => { + h3.dispatch("todos/toggle", key); + h3.redraw(); }; - const removeTodo = (todo) => { - h3.dispatch("todos/remove", data); - h3.redraw() + const removeTodo = (key) => { + h3.dispatch("todos/remove", key); + h3.redraw(); }; - return h3(`div.todo-item`, {$key: data.key}, [ + return h3(`div.todo-item`, { data: { key: data.key } }, [ h3(`div.todo-content${todoStateClass}`, [ - h3("span.todo-text", { onclick: () => toggleTodo(data) }, data.text), + h3("span.todo-text", { onclick: (e) => toggleTodo(e.currentTarget.parentNode.parentNode.dataset.key) }, data.text), ]), - h3("span.delete-todo", { onclick: () => removeTodo(data) }, "✘"), + h3("span.delete-todo", { onclick: (e) => removeTodo(e.currentTarget.parentNode.dataset.key) }, "✘"), ]); }
M docs/example/assets/js/h3.jsdocs/example/assets/js/h3.js

@@ -61,6 +61,8 @@ };

const selectorRegex = /^([a-z][a-z0-9:_=-]*)(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i; +let $onrenderCallbacks = []; + // Virtual Node Implementation with HyperScript-like syntax class VNode { constructor(...args) {

@@ -68,7 +70,6 @@ this.type = undefined;

this.attributes = {}; this.data = {}; this.id = undefined; - this.$key = undefined; this.$html = undefined; this.$onrender = undefined; this.style = undefined;

@@ -158,7 +159,6 @@ from(data) {

this.value = data.value; this.type = data.type; this.id = data.id; - this.$key = data.$key; this.$html = data.$html; this.$onrender = data.$onrender; this.style = data.style;

@@ -176,7 +176,6 @@ }

processProperties(attrs) { this.id = this.id || attrs.id; - this.$key = attrs.$key; this.$html = attrs.$html; this.$onrender = attrs.$onrender; this.style = attrs.style;

@@ -199,7 +198,6 @@ this.eventListeners[key.slice(2)] = attrs[key];

delete this.attributes[key]; }); delete this.attributes.value; - delete this.attributes.$key; delete this.attributes.$html; delete this.attributes.$onrender; delete this.attributes.id;

@@ -301,7 +299,7 @@ // Children

this.children.forEach((c) => { const cnode = c.render(); node.appendChild(cnode); - c.$onrender && c.$onrender(cnode); + c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); }); if (this.$html) { node.innerHTML = this.$html;

@@ -327,8 +325,6 @@ newvnode.$onrender && newvnode.$onrender(renderedNode);

oldvnode.from(newvnode); return; } - // $key - oldvnode.$key = newvnode.$key; // ID if (oldvnode.id !== newvnode.id) { node.id = newvnode.id || "";

@@ -416,41 +412,67 @@ });

oldvnode.eventListeners = newvnode.eventListeners; } // Children - function mapChildren(oldvnode, newvnode) { - const maxLength = Math.max( - oldvnode.children.length, - newvnode.children.length - ); let map = []; + let oldNodesFound = 0; + let newNodesFound = 0; + // First look for existing nodes for (let oldIndex = 0; oldIndex < oldvnode.children.length; oldIndex++) { - if (oldIndex >= newvnode.children.length) { - // not found in new node, remove from old - map.push(-3); - } else { - let found = -1; - for (let index = 0; index < oldvnode.children.length; index++) { - if ( - equal(oldvnode.children[oldIndex], newvnode.children[index]) && - !map.includes(index) - ) { - found = index; - break; - } + let found = -1; + for (let index = 0; index < newvnode.children.length; index++) { + if ( + equal(oldvnode.children[oldIndex], newvnode.children[index]) && + !map.includes(index) + ) { + found = index; + newNodesFound++; + oldNodesFound++; + break; } - map.push(found); } + map.push(found); } - // other nodes are new, needs to be added - if (maxLength > oldvnode.children.length) { - map = map.concat( - [...Array(maxLength - oldvnode.children.length)].map(() => -2) - ); + if ( + newNodesFound === oldNodesFound && + newvnode.children.length === oldvnode.children.length + ) { + // something changed but everything else is the same + return map; + } + if (newNodesFound === newvnode.children.length) { + // All children in newvnode exist in oldvnode + // All nodes that are not found must be removed + for (let i = 0; i < map.length; i++) { + if (map[i] === -1) { + map[i] = -3; + } + } + } + if (oldNodesFound === oldvnode.children.length) { + // All children in oldvnode exist in newvnode + // Check where the missing newvnodes children need to be added + for ( + let newIndex = 0; + newIndex < newvnode.children.length; + newIndex++ + ) { + if (!map.includes(newIndex)) { + map.splice(newIndex, 0, -2); + } + } + } + // Check if nodes needs to be removed (if there are fewer children) + if (newvnode.children.length < oldvnode.children.length) { + for (let i = 0; i < map.length; i++) { + if (map[i] === -1 && !newvnode.children[i]) { + map[i] = -3; + } + } } return map; } let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.length).keys()]; + let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; while (!equal(childMap, resultMap)) { let count = -1; for (let i of childMap) {

@@ -470,9 +492,9 @@ });

break; case -2: // add node - oldvnode.children.push(newvnode.children[count]); + oldvnode.children.splice(count, 0, newvnode.children[count]); const renderedNode = newvnode.children[count].render(); - node.appendChild(renderedNode); + node.insertBefore(renderedNode, node.childNodes[count]); newvnode.children[count].$onrender && newvnode.children[count].$onrender(renderedNode); breakFor = true;

@@ -642,6 +664,8 @@ const vnode = newRouteComponent(newRouteComponent.state);

const node = vnode.render(); this.element.appendChild(node); vnode.$onrender && vnode.$onrender(node); + $onrenderCallbacks.forEach((cbk) => cbk()); + $onrenderCallbacks = []; this.setRedraw(vnode, newRouteComponent.state); window.scrollTo(0, 0); this.store.dispatch("$redraw");
M docs/example/assets/js/modules.jsdocs/example/assets/js/modules.js

@@ -3,7 +3,9 @@

const app = () => { h3.on("app/load", () => { const storedData = localStorage.getItem("h3_todo_list"); - const { todos, settings } = storedData ? JSON.parse(storedData) : {todos: [], settings: {}}; + const { todos, settings } = storedData + ? JSON.parse(storedData) + : { todos: [], settings: {} }; return { todos, settings }; }); h3.on("app/save", (state, data) => {

@@ -37,13 +39,13 @@ text: data.text,

}); return { todos }; }); - h3.on("todos/remove", (state, data) => { - const todos = state.todos.filter(({ key }) => key !== data.key); + h3.on("todos/remove", (state, k) => { + const todos = state.todos.filter(({ key }) => key !== k); return { todos }; }); - h3.on("todos/toggle", (state, data) => { + h3.on("todos/toggle", (state, k) => { const todos = state.todos; - const todo = state.todos.find((t) => t.key === data.key); + const todo = state.todos.find(({ key }) => key === k); todo.done = !todo.done; return { todos }; });
M docs/js/h3.jsdocs/js/h3.js

@@ -61,6 +61,8 @@ };

const selectorRegex = /^([a-z][a-z0-9:_=-]*)(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i; +let $onrenderCallbacks = []; + // Virtual Node Implementation with HyperScript-like syntax class VNode { constructor(...args) {

@@ -68,7 +70,6 @@ this.type = undefined;

this.attributes = {}; this.data = {}; this.id = undefined; - this.$key = undefined; this.$html = undefined; this.$onrender = undefined; this.style = undefined;

@@ -158,7 +159,6 @@ from(data) {

this.value = data.value; this.type = data.type; this.id = data.id; - this.$key = data.$key; this.$html = data.$html; this.$onrender = data.$onrender; this.style = data.style;

@@ -176,7 +176,6 @@ }

processProperties(attrs) { this.id = this.id || attrs.id; - this.$key = attrs.$key; this.$html = attrs.$html; this.$onrender = attrs.$onrender; this.style = attrs.style;

@@ -199,7 +198,6 @@ this.eventListeners[key.slice(2)] = attrs[key];

delete this.attributes[key]; }); delete this.attributes.value; - delete this.attributes.$key; delete this.attributes.$html; delete this.attributes.$onrender; delete this.attributes.id;

@@ -301,7 +299,7 @@ // Children

this.children.forEach((c) => { const cnode = c.render(); node.appendChild(cnode); - c.$onrender && c.$onrender(cnode); + c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); }); if (this.$html) { node.innerHTML = this.$html;

@@ -327,8 +325,6 @@ newvnode.$onrender && newvnode.$onrender(renderedNode);

oldvnode.from(newvnode); return; } - // $key - oldvnode.$key = newvnode.$key; // ID if (oldvnode.id !== newvnode.id) { node.id = newvnode.id || "";

@@ -416,41 +412,67 @@ });

oldvnode.eventListeners = newvnode.eventListeners; } // Children - function mapChildren(oldvnode, newvnode) { - const maxLength = Math.max( - oldvnode.children.length, - newvnode.children.length - ); let map = []; + let oldNodesFound = 0; + let newNodesFound = 0; + // First look for existing nodes for (let oldIndex = 0; oldIndex < oldvnode.children.length; oldIndex++) { - if (oldIndex >= newvnode.children.length) { - // not found in new node, remove from old - map.push(-3); - } else { - let found = -1; - for (let index = 0; index < oldvnode.children.length; index++) { - if ( - equal(oldvnode.children[oldIndex], newvnode.children[index]) && - !map.includes(index) - ) { - found = index; - break; - } + let found = -1; + for (let index = 0; index < newvnode.children.length; index++) { + if ( + equal(oldvnode.children[oldIndex], newvnode.children[index]) && + !map.includes(index) + ) { + found = index; + newNodesFound++; + oldNodesFound++; + break; } - map.push(found); } + map.push(found); } - // other nodes are new, needs to be added - if (maxLength > oldvnode.children.length) { - map = map.concat( - [...Array(maxLength - oldvnode.children.length)].map(() => -2) - ); + if ( + newNodesFound === oldNodesFound && + newvnode.children.length === oldvnode.children.length + ) { + // something changed but everything else is the same + return map; + } + if (newNodesFound === newvnode.children.length) { + // All children in newvnode exist in oldvnode + // All nodes that are not found must be removed + for (let i = 0; i < map.length; i++) { + if (map[i] === -1) { + map[i] = -3; + } + } + } + if (oldNodesFound === oldvnode.children.length) { + // All children in oldvnode exist in newvnode + // Check where the missing newvnodes children need to be added + for ( + let newIndex = 0; + newIndex < newvnode.children.length; + newIndex++ + ) { + if (!map.includes(newIndex)) { + map.splice(newIndex, 0, -2); + } + } + } + // Check if nodes needs to be removed (if there are fewer children) + if (newvnode.children.length < oldvnode.children.length) { + for (let i = 0; i < map.length; i++) { + if (map[i] === -1 && !newvnode.children[i]) { + map[i] = -3; + } + } } return map; } let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.length).keys()]; + let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; while (!equal(childMap, resultMap)) { let count = -1; for (let i of childMap) {

@@ -470,9 +492,9 @@ });

break; case -2: // add node - oldvnode.children.push(newvnode.children[count]); + oldvnode.children.splice(count, 0, newvnode.children[count]); const renderedNode = newvnode.children[count].render(); - node.appendChild(renderedNode); + node.insertBefore(renderedNode, node.childNodes[count]); newvnode.children[count].$onrender && newvnode.children[count].$onrender(renderedNode); breakFor = true;

@@ -642,6 +664,8 @@ const vnode = newRouteComponent(newRouteComponent.state);

const node = vnode.render(); this.element.appendChild(node); vnode.$onrender && vnode.$onrender(node); + $onrenderCallbacks.forEach((cbk) => cbk()); + $onrenderCallbacks = []; this.setRedraw(vnode, newRouteComponent.state); window.scrollTo(0, 0); this.store.dispatch("$redraw");
M docs/md/api.mddocs/md/api.md

@@ -112,7 +112,6 @@

* Any attribute starting with *on* (e.g. onclick, onkeydown, etc.) will be treated as an event listener. * The `classList` attribute can be set to a list of classes to apply to the element (as an alternative to using the element selector shorthand). * The `data` attribute can be set to a simple object containing [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). -* The special `$key` attribute can be used to guarantee the uniqueness of two VNodes and it will not be translated into an HTML attribute. * The special `$html` attribute can be used to set the `innerHTML` property of the resulting HTML element. Use only if you know what you are doing! * The special `$onrender` attribute can be set to a function that will executed every time the VNode is rendered and added to the DOM.
M h3.jsh3.js

@@ -61,6 +61,8 @@ };

const selectorRegex = /^([a-z][a-z0-9:_=-]*)(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i; +let $onrenderCallbacks = []; + // Virtual Node Implementation with HyperScript-like syntax class VNode { constructor(...args) {

@@ -68,7 +70,6 @@ this.type = undefined;

this.attributes = {}; this.data = {}; this.id = undefined; - this.$key = undefined; this.$html = undefined; this.$onrender = undefined; this.style = undefined;

@@ -158,7 +159,6 @@ from(data) {

this.value = data.value; this.type = data.type; this.id = data.id; - this.$key = data.$key; this.$html = data.$html; this.$onrender = data.$onrender; this.style = data.style;

@@ -176,7 +176,6 @@ }

processProperties(attrs) { this.id = this.id || attrs.id; - this.$key = attrs.$key; this.$html = attrs.$html; this.$onrender = attrs.$onrender; this.style = attrs.style;

@@ -199,7 +198,6 @@ this.eventListeners[key.slice(2)] = attrs[key];

delete this.attributes[key]; }); delete this.attributes.value; - delete this.attributes.$key; delete this.attributes.$html; delete this.attributes.$onrender; delete this.attributes.id;

@@ -301,7 +299,7 @@ // Children

this.children.forEach((c) => { const cnode = c.render(); node.appendChild(cnode); - c.$onrender && c.$onrender(cnode); + c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); }); if (this.$html) { node.innerHTML = this.$html;

@@ -327,8 +325,6 @@ newvnode.$onrender && newvnode.$onrender(renderedNode);

oldvnode.from(newvnode); return; } - // $key - oldvnode.$key = newvnode.$key; // ID if (oldvnode.id !== newvnode.id) { node.id = newvnode.id || "";

@@ -416,41 +412,67 @@ });

oldvnode.eventListeners = newvnode.eventListeners; } // Children - function mapChildren(oldvnode, newvnode) { - const maxLength = Math.max( - oldvnode.children.length, - newvnode.children.length - ); let map = []; + let oldNodesFound = 0; + let newNodesFound = 0; + // First look for existing nodes for (let oldIndex = 0; oldIndex < oldvnode.children.length; oldIndex++) { - if (oldIndex >= newvnode.children.length) { - // not found in new node, remove from old - map.push(-3); - } else { - let found = -1; - for (let index = 0; index < oldvnode.children.length; index++) { - if ( - equal(oldvnode.children[oldIndex], newvnode.children[index]) && - !map.includes(index) - ) { - found = index; - break; - } + let found = -1; + for (let index = 0; index < newvnode.children.length; index++) { + if ( + equal(oldvnode.children[oldIndex], newvnode.children[index]) && + !map.includes(index) + ) { + found = index; + newNodesFound++; + oldNodesFound++; + break; } - map.push(found); } + map.push(found); } - // other nodes are new, needs to be added - if (maxLength > oldvnode.children.length) { - map = map.concat( - [...Array(maxLength - oldvnode.children.length)].map(() => -2) - ); + if ( + newNodesFound === oldNodesFound && + newvnode.children.length === oldvnode.children.length + ) { + // something changed but everything else is the same + return map; + } + if (newNodesFound === newvnode.children.length) { + // All children in newvnode exist in oldvnode + // All nodes that are not found must be removed + for (let i = 0; i < map.length; i++) { + if (map[i] === -1) { + map[i] = -3; + } + } + } + if (oldNodesFound === oldvnode.children.length) { + // All children in oldvnode exist in newvnode + // Check where the missing newvnodes children need to be added + for ( + let newIndex = 0; + newIndex < newvnode.children.length; + newIndex++ + ) { + if (!map.includes(newIndex)) { + map.splice(newIndex, 0, -2); + } + } + } + // Check if nodes needs to be removed (if there are fewer children) + if (newvnode.children.length < oldvnode.children.length) { + for (let i = 0; i < map.length; i++) { + if (map[i] === -1 && !newvnode.children[i]) { + map[i] = -3; + } + } } return map; } let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.length).keys()]; + let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; while (!equal(childMap, resultMap)) { let count = -1; for (let i of childMap) {

@@ -470,9 +492,9 @@ });

break; case -2: // add node - oldvnode.children.push(newvnode.children[count]); + oldvnode.children.splice(count, 0, newvnode.children[count]); const renderedNode = newvnode.children[count].render(); - node.appendChild(renderedNode); + node.insertBefore(renderedNode, node.childNodes[count]); newvnode.children[count].$onrender && newvnode.children[count].$onrender(renderedNode); breakFor = true;

@@ -642,6 +664,8 @@ const vnode = newRouteComponent(newRouteComponent.state);

const node = vnode.render(); this.element.appendChild(node); vnode.$onrender && vnode.$onrender(node); + $onrenderCallbacks.forEach((cbk) => cbk()); + $onrenderCallbacks = []; this.setRedraw(vnode, newRouteComponent.state); window.scrollTo(0, 0); this.store.dispatch("$redraw");