Skip to content

Commit

Permalink
feat: use haunted to manage incomplete templates
Browse files Browse the repository at this point in the history
* Render incomplete templates with haunted
** only update first instance when we switch between index 0/1
** only update last instance when we switch between index last/last-1
** only render incomplete instance to DOM if hitting an incomplete item
*** but keep it!

Need to observe 'haunted' since it's ready late.

element.__incomplete becomes the actual element to show/hide/render to
** will always be available, since creation, no need to check it

* Drop inline-README
* Upgraded dependencies.

Signed-off-by: Patrik Kullman <[email protected]>
  • Loading branch information
Patrik Kullman authored and cristinecula committed Jun 24, 2020
1 parent b9d9488 commit fac0c2b
Show file tree
Hide file tree
Showing 9 changed files with 1,184 additions and 1,305 deletions.
142 changes: 42 additions & 100 deletions cosmoz-data-nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import '@webcomponents/shadycss/entrypoints/apply-shim';
import '@polymer/paper-icon-button';
import '@polymer/paper-spinner/paper-spinner-lite';

import { render } from 'lit-html';

import { PolymerElement } from '@polymer/polymer/polymer-element';
import { html } from '@polymer/polymer/lib/utils/html-tag';
import { useShadow } from '@polymer/polymer/lib/utils/settings';
Expand All @@ -23,61 +25,15 @@ import '@neovici/cosmoz-page-router/cosmoz-page-location';

import { hauntedPolymer } from '@neovici/cosmoz-utils';

import { useCache } from './lib/use-cache.js';
import { useDataNav } from './lib/use-data-nav.js';

const _async = window.requestIdleCallback || window.requestAnimationFrame || window.setTimeout,
_hasDeadline = 'IdleDeadline' in window,
_asyncPeriod = (cb, timeout = 1500) => {
_async(() => cb(), _hasDeadline && { timeout });
},
_doAsyncSteps = (steps, timeout) => {
const callStep = () => {
if (!Array.isArray(steps) || steps.length < 1) {
return;
}
const step = steps.shift();
step();
_asyncPeriod(callStep, timeout);
};
return _asyncPeriod(callStep, timeout);
};

/**
`cosmoz-data-nav` provides a way to show each individual item of a list in a queue-style behavior
to the user.
A list of `items` and a `<template>` for each item is used to render the queue, and methods to
slide to next/previous item is exposed.
For performance and to avoid DOM bloat, the items are "lazy-rendered" and the `maxPreload` setting
decides how many _upcoming_ items that should be pre-rendered.
It can also be "lazy-loaded" when each item's details aren't available, by instead of (or in addition to)
`items` also set an `idList` array with identifiers for each item.
If `cosmoz-data-nav` tries to render an item that is missing but an id exists for the position, it will fire
a `need-data` event to announce this and expect an `object-details-fetched` event when the item details are
available. This event should have an `id` property that corresponds to the `id` requested and also an `object`
property with the item. This design enables parallelization since multiple items can be requested at once,
and responded to in an unordered, async manner. Also, `cosmoz-data-nav` does not need to be aware of any
template-design or item model.
Example:
<cosmoz-data-nav id-list="[[ getItemIds(myList) ]]" on-need-data="triggerDetailFetch">
<template>
<neon-animatable class="vertical layout fit">
<my-view>
<paper-icon-button slot="actions" disabled$="[[ isFirstItem ]]" icon="chevron-left" on-click="selectPrevious"></paper-icon-button>
<paper-icon-button slot="actions" disabled$="[[ isLastItem ]]" icon="chevron-right" on-click="selectNext"></paper-icon-button>
</my-view>
</neon-animatable>
</template>
</cosmoz-data-nav>
@demo demo/index.html
@appliesMixin translatable
*/
class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mixinBehaviors([IronResizableBehavior], PolymerElement))) {
class CosmozDataNav extends hauntedPolymer('haunted', useDataNav)(translatable(mixinBehaviors([IronResizableBehavior], PolymerElement))) {
static get template() { // eslint-disable-line max-lines-per-function
return html`
<style>
Expand Down Expand Up @@ -130,20 +86,6 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
<div id="templates">
<slot id="templatesSlot"></slot>
</div>
<template id="incompleteTemplate">
<cosmoz-bottom-bar-view active incomplete style="position: absolute; top: 0; right: 0; bottom: 0; left: 0;">
<div slot="scroller-content"
style="display: flex; flex: 1; flex-basis: 0.000000001px; flex-direction: row; justify-content: center; align-items: center;"
>
<paper-spinner-lite active></paper-spinner-lite>
<div style="margin-left: 10px">
<h3><span>[[ _('Data is updating', t) ]]</span></h3>
</div>
</div>
<paper-icon-button disabled$="[[ prevDisabled ]]" icon="chevron-left" cosmoz-data-nav-select="-1" slot="extra"></paper-icon-button>
<paper-icon-button disabled$="[[ nextDisabled ]]" icon="chevron-right" cosmoz-data-nav-select="+1" slot="extra"></paper-icon-button>
</cosmoz-bottom-bar-view>
</template>
`;
}

Expand All @@ -155,7 +97,7 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
_elements: {
type: Array,
value() {
return [this._createElement()];
return [];
}
},

Expand Down Expand Up @@ -353,6 +295,27 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
};
}

static get observers() {
return [
'renderIncomplete(selected, haunted)'
];
}

renderIncomplete(index, haunted) {
if (haunted == null) {
return;
}

const position = index < this.items.length ? index : index - 1,
element = this._getElement(position),
item = this.items[position];

if (element == null || !this.isIncompleteFn(item)) {
return;
}
render(haunted.incompleteTemplates[position], element.__incomplete);
}

constructor() {
super();
this._previouslySelectedItem = null;
Expand Down Expand Up @@ -389,23 +352,22 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
this.splice('_elements', 0, this._elements.length, this._createElement())
.forEach(element => {
this._removeInstance(element.__instance);
this._removeInstance(element.__incomplete);
element.removeChild(element.__incomplete);
element.__instance = element.__incomplete = null;
});
}

_onTemplatesChange(change) {
if (!this._elementTemplate) {
const templates = change.addedNodes.filter(n => n.nodeType === Node.ELEMENT_NODE && n.tagName === 'TEMPLATE'),
elementTemplate = templates.find(n => n.matches(':not([incomplete])')),
incompleteTemplate = templates.find(n => n.matches('[incomplete]')) || this.$.incompleteTemplate;
elementTemplate = templates[0];

if (!elementTemplate) {
// eslint-disable-next-line no-console
console.warn('cosmoz-data-nav requires a template');
return;
}
this._templatize(elementTemplate, incompleteTemplate);
this._templatize(elementTemplate);
}

const elements = this._elements,
Expand All @@ -414,15 +376,11 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
this.splice('_elements', -1, 0, ...Array(this.elementsBuffer - length)
.fill().map(this._createElement, this));

_doAsyncSteps(elements.map(el => {
this.appendChild(el);
return this._createIncomplete.bind(this, el);
}));
elements.forEach(el => this.appendChild(el));
}

_templatize(elementTemplate, incompleteTemplate) {
_templatize(elementTemplate) {
this._elementTemplate = elementTemplate;
this._incompleteTemplate = incompleteTemplate;

const baseProps = {
prevDisabled: true,
Expand All @@ -435,11 +393,6 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
forwardHostProp: this._forwardHostProp,
notifyInstanceProp: this._notifyInstanceProp
});
this._incompleteCtor = templatize(this._incompleteTemplate, this, {
instanceProps: baseProps,
parentModel: true,
forwardHostProp: this._forwardHostProp
});
}

get _allInstances() {
Expand Down Expand Up @@ -468,26 +421,20 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
if (prop !== this.as || value === item || this._allElementInstances.indexOf(inst) < 0) {
return;
}
this.__cache.dropItem(item);
this.haunted.cache.dropItem(item);
this.set(['items', index], value);
}

_createElement() {
const element = document.createElement('div');
const element = document.createElement('div'),
incDiv = document.createElement('div');
element.appendChild(incDiv);
element.__incomplete = incDiv;
element.setAttribute('slot', 'items');
element.classList.add('animatable');
return element;
}

_createIncomplete(element) {
if (element.__incomplete) {
return;
}
const incomplete = new this._incompleteCtor({});
element.__incomplete = incomplete;
element.appendChild(incomplete.root);
}

/**
* Selects an item by index.
*
Expand Down Expand Up @@ -523,7 +470,7 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
console.warn('Multiple replaceable items matches idPath', this.idPath, 'with id', id, 'in the item list', items, 'to replace with item', item);
}

this.__cache.set(id, item);
this.haunted.cache.set(id, item);
matches.forEach(match => this.set(['items', items.indexOf(match)], { ...item }));

this._preload();
Expand Down Expand Up @@ -553,7 +500,7 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
if (length) {
items.forEach((item, index) => {
if (this.isIncompleteFn(item)) {
const cachedItem = this.__cache?.get(item);
const cachedItem = this.haunted?.cache?.get(item);
if (cachedItem) {
this.set(['items', index], cachedItem);
}
Expand Down Expand Up @@ -755,7 +702,7 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
return;
}
// return reference to the rendered template instance or the incomplete template if missing
return selectedElement.children[1] || selectedElement.children[0];
return selectedElement.children[1] || selectedElement.__incomplete;
}

_getItem(index, items = this.items) {
Expand All @@ -765,13 +712,12 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix

_resetElement(index) { // eslint-disable-line max-statements
const element = this._getElement(index);
if (!element || !element.__incomplete) {
if (!element) {
return;
}

const item = this.items[index],
baseProps = this._getBaseProps(index),
incomplete = element.__incomplete,
instance = element.__instance;

if (instance) {
Expand All @@ -782,13 +728,11 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
return;
}

Object.assign(incomplete, baseProps);

if (element._reset) {
return;
}
element._reset = true;
incomplete._showHideChildren(false);
element.__incomplete.style.display = 'block';

if (!instance) {
return;
Expand Down Expand Up @@ -1070,9 +1014,7 @@ class CosmozDataNav extends hauntedPolymer('__cache', useCache)(translatable(mix
// maintain task in queue
return idx;
}
if (element.__incomplete) {
element.__incomplete._showHideChildren(true);
}
element.__incomplete.style.display = 'none';

const isSelected = idx === this.selected,
needsRender = element.item !== item;
Expand Down
9 changes: 6 additions & 3 deletions demo/demo.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
component, html, useMemo, useState, useEffect
component, html, useState, useEffect
} from 'haunted';
import '@polymer/paper-input/paper-textarea.js';
import './helpers/cosmoz-demo-view.js';
Expand All @@ -9,7 +9,9 @@ const asyncs = {},
// eslint-disable-next-line max-lines-per-function
DataNavDemo = function () {

const items = useMemo(() => Array(20).fill('').map((e, i) => i.toString()), []),
const
makeItems = () => Array(20).fill('').map((e, i) => i.toString()),
[items, setItems] = useState(makeItems()),
[selected, setSelected] = useState(),
[selItem, setSelItem] = useState(),
[instance, setInstance] = useState(),
Expand All @@ -21,7 +23,7 @@ const asyncs = {},
asyncs[id] = null;
}
// eslint-disable-next-line no-console
console.log('on need data', event.target, event.srcElement);
console.log('on need data', id);
asyncs[id] = setTimeout(() => dataNav.setItemById(id, { id }), 500);
},
computeJSON = index => JSON.stringify(items[index]);
Expand Down Expand Up @@ -73,6 +75,7 @@ const asyncs = {},
</cosmoz-data-nav>
<paper-textarea value="${ computeJSON(selected) }"></paper-textarea>
<div>Selected: ${ JSON.stringify(selItem) }</div>
<button @click="${ () => setItems(makeItems()) }">Make new items</button>
`;
};

Expand Down
2 changes: 1 addition & 1 deletion karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module.exports = config => {
global: {
statements: 80,
branches: 70,
functions: 90,
functions: 88,
lines: 80
}
}
Expand Down
15 changes: 15 additions & 0 deletions lib/use-data-nav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useMemo } from 'haunted';
import { useIncompleteTemplate } from './use-incomplete-template';
import { useCache } from './use-cache';

export const useDataNav = el => {
const incompleteTemplates = useMemo(() => ({
[el.selected - 1]: useIncompleteTemplate(el.selected - 1, el.items.length),
[el.selected]: useIncompleteTemplate(el.selected, el.items.length),
[el.selected + 1]: useIncompleteTemplate(el.selected + 1, el.items.length)
}), [el.selected, el.items.length]);
return {
cache: useCache(el),
incompleteTemplates
};
};
38 changes: 38 additions & 0 deletions lib/use-incomplete-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
html, useMemo
} from 'haunted';
import { _ } from '@neovici/cosmoz-i18next';

const viewStyle = `
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
`,
scrollerContentStyle = `
display: flex;
flex: 1;
flex-basis: 0.000000001px;
flex-direction: row;
justify-content: center;
align-items: center;
`;

export const useIncompleteTemplate = (index, length) => {
const atStart = index === 0,
atEnd = index === length - 1;

return useMemo(() => html`
<cosmoz-bottom-bar-view active incomplete style="${ viewStyle }">
<div slot="scroller-content" style="${ scrollerContentStyle }">
<paper-spinner-lite active></paper-spinner-lite>
<div style="margin-left: 10px">
<h3><span>${ _('Data is updating') }</span></h3>
</div>
</div>
<paper-icon-button ?disabled="${ atStart }" icon="chevron-left" cosmoz-data-nav-select="-1" slot="extra"></paper-icon-button>
<paper-icon-button ?disabled="${ atEnd }" icon="chevron-right" cosmoz-data-nav-select="+1" slot="extra"></paper-icon-button>
</cosmoz-bottom-bar-view>
`, [atStart, atEnd]);
};
Loading

0 comments on commit fac0c2b

Please sign in to comment.