list-render: Allow re-rendering individual list items.

Previously, we had to fiddle with the generated HTML to update
individual values. Now, we can simply ask the widget to rerender
the row that we updated.

This is done by passing an html_selector function that returns
a selector for the rendered item.

If:
  - we do not provide html_selector function
  - item is not currently rendered
  - new html is not a string.
then the render_item() call is a noop.
This commit is contained in:
Rohitt Vashishtha 2020-05-28 23:28:51 +05:30 committed by Steve Howell
parent 2cfead7601
commit a114b6a1b1
2 changed files with 129 additions and 0 deletions

View File

@ -19,6 +19,9 @@ set_global('$', (arg) => {
}
return {
replace: (regex, string) => {
arg = arg.replace(regex, string);
},
html: () => arg,
};
});
@ -731,3 +734,103 @@ run_test('opts.get_item', () => {
['two', 'three']
);
});
run_test('render item', () => {
const container = make_container();
const scroll_container = make_scroll_container(container);
const INITIAL_RENDER_COUNT = 80; // Keep this in sync with the actual code.
container.html = () => {};
let called = false;
scroll_container.find = (query) => {
const expected_queries = [`tr[data-item='${INITIAL_RENDER_COUNT}']`,
`tr[data-item='${INITIAL_RENDER_COUNT - 1}']`];
const item = INITIAL_RENDER_COUNT - 1;
const new_html = `<tr data-item=${item}>updated: ${item}</tr>\n`;
const regex = new RegExp(`\\<tr data-item=${item}\\>.*?\<\\/tr\\>`);
assert(expected_queries.includes(query));
if (query.includes(`data-item='${INITIAL_RENDER_COUNT}'`)) {
return; // This item is not rendered, so we find nothing
}
return {
// Return a JQuery stub for the original HTML.
// We want this to be called when we replace
// the existing HTML with newly rendered HTML.
replaceWith: (html) => {
assert.equal(new_html, html);
called = true;
container.appended_data.replace(regex, new_html);
},
};
};
const list = [...Array(100).keys()];
let text = 'initial';
const get_item = (item) => ({text: `${text}: ${item}`, value: item});
const widget = list_render.create(container, list, {
name: 'replace-list',
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
get_item: get_item,
html_selector: (item) => `tr[data-item='${item}']`,
});
const item = INITIAL_RENDER_COUNT - 1;
assert(container.appended_data.html().includes('<tr data-item=2>initial: 2</tr>'));
assert(container.appended_data.html().includes('<tr data-item=3>initial: 3</tr>'));
text = 'updated';
called = false;
widget.render_item(INITIAL_RENDER_COUNT - 1);
assert(called);
assert(container.appended_data.html().includes('<tr data-item=2>initial: 2</tr>'));
assert(container.appended_data.html().includes(`<tr data-item=${item}>updated: ${item}</tr>`));
// Item 80 should not be in the rendered list. (0 indexed)
assert(!container.appended_data.html().includes(`<tr data-item=${INITIAL_RENDER_COUNT}>initial: ${INITIAL_RENDER_COUNT}</tr>`));
called = false;
widget.render_item(INITIAL_RENDER_COUNT);
assert(!called);
widget.render_item(INITIAL_RENDER_COUNT - 1);
assert(called);
// Tests below this are for the corner cases, where we abort the rerender.
blueslip.expect('error', 'html_selector should be a function.');
list_render.create(container, list, {
name: 'replace-list',
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
get_item: get_item,
html_selector: 'hello world',
});
blueslip.reset();
let get_item_called;
const widget_2 = list_render.create(container, list, {
name: 'replace-list',
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
get_item: (item) => {
get_item_called = true;
return item;
},
});
get_item_called = false;
widget_2.render_item(item);
// Test that we didn't try to render the item.
assert(!get_item_called);
let rendering_item = false;
const widget_3 = list_render.create(container, list, {
name: 'replace-list',
modifier: (item) => {
return rendering_item ? undefined : `${item}\n`;
},
get_item: get_item,
html_selector: (item) => `tr[data-item='${item}']`,
});
// Once we have initially rendered the widget, change the
// behavior of the modifier function.
rendering_item = true;
blueslip.expect('error', 'List item is not a string: undefined');
widget_3.render_item(item);
blueslip.reset();
});

View File

@ -214,6 +214,32 @@ exports.create = function ($container, list, opts) {
meta.offset += load_count;
};
widget.render_item = (item) => {
if (!opts.html_selector) {
// We don't have any way to find the existing item.
return;
}
const html_item = meta.scroll_container.find(opts.html_selector(item));
if (!html_item) {
// We don't have the item in the current scroll container; it'll be
// rendered with updated data when it is scrolled to.
return;
}
if (opts.get_item) {
item = opts.get_item(item);
}
const html = opts.modifier(item);
if (typeof html !== 'string') {
blueslip.error('List item is not a string: ' + html);
return;
}
// At this point, we have asserted we have all the information to replace
// the html now.
html_item.replaceWith(html);
};
widget.clear = function () {
$container.html("");
meta.offset = 0;