Skip to content

Commit

Permalink
fix(live): handle more live editing edge cases
Browse files Browse the repository at this point in the history
Offloads more of the live editing logic
to the respective engines.
Additionally, fixes some bugs with the data
flow when live editing Jekyll components.
  • Loading branch information
bglw committed Oct 9, 2021
1 parent 958b79f commit 9ee51c0
Show file tree
Hide file tree
Showing 6 changed files with 925 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function (Liquid) {
this.args = token.args;
},
render: function (ctx, hash) {
const argsString = this.args.replace(/\binclude\./, '').replace(/\s+in\s+/, '=').split(' ')[0];
const argsString = this.args.replace(/\binclude\./g, '').replace(/\s+in\s+/, '=').split(' ')[0];
return `<!--bookshop-live context(${argsString}[${ctx.get(['forloop','index0'])}])-->`;
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const rewriteTag = function(token, src, liveMarkup) {
if (liveMarkup) {
let params = token.args.split(' ');
params.shift();
raw = `<!--bookshop-live name(${componentName}) params(${params.join(' ')})-->${raw}<!--bookshop-live end-->`
raw = `<!--bookshop-live name(${componentName}) params(${params.join(' ').replace(/\binclude\./g, '')})-->${raw}<!--bookshop-live end-->`
}
}

Expand All @@ -51,7 +51,7 @@ const rewriteTag = function(token, src, liveMarkup) {
if (liveMarkup) {
let params = token.args.split(' ');
params.shift();
raw = `<!--bookshop-live name(${componentName}) params(${params.join(' ')})-->${raw}<!--bookshop-live end-->`
raw = `<!--bookshop-live name(${componentName}) params(${params.join(' ').replace(/\binclude\./g, '')})-->${raw}<!--bookshop-live end-->`
}
}

Expand Down
50 changes: 17 additions & 33 deletions javascript-modules/live/lib/app/live.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export const getLive = (engines) => class BookshopLive {
}
}

update(data) {
async update(data) {
this.data = data;
this.render();
await this.render();
}

async render() {
Expand All @@ -44,13 +44,15 @@ export const getLive = (engines) => class BookshopLive {
let currentNode = iterator.iterateNext();
while(currentNode){
const matches = currentNode.textContent.match(/bookshop-live ((?<end>end)|name\((?<name>[^)]*)\) params\((?<params>[^)]*)\)).*/);
const contextMatches = currentNode.textContent.match(/bookshop-live.*context\((?<context>[^)]*)\)/);
const contextMatches = currentNode.textContent.match(/bookshop-live.*context\((?<context>.+)\)\s*$/);

if(contextMatches?.groups["context"]){
const prevScope = {...(stack[stack.length-1]?.scope ?? this.data), ...bindings};
contextMatches.groups["context"].replace(/: /g, '=').split(' ').forEach((binding) => {
const assignments = contextMatches.groups["context"].replace(/: /g, '=').split(' ');
for (const binding of assignments) {
const [name, identifier] = binding.split('=');
bindings[name] = this.dig(identifier, prevScope);
})
const scopes = [this.data, ...stack.map(s => s.scope), bindings];
bindings[name] = await this.engines[0].eval(identifier, scopes);
}
}

if(matches?.groups["end"]){
Expand All @@ -65,15 +67,17 @@ export const getLive = (engines) => class BookshopLive {
updates.push({startNode, endNode: currentNode, output});
} else if(matches){
let scope = {};
const prevScope = {...(stack[stack.length-1]?.scope ?? this.data), ...bindings};
matches.groups["params"].replace(/: /g, '=').split(' ').forEach((param) => {
const params = matches.groups["params"].replace(/: /g, '=').split(' ');
for (const param of params) {
const [name, identifier] = param.split('=');
const scopes = [this.data, ...stack.map(s => s.scope), bindings];
if(name === 'bind'){
scope = {...scope, ...this.dig(identifier, prevScope)};
const bindVals = await this.engines[0].eval(identifier, scopes);
scope = {...scope, ...bindVals};
} else {
scope[name] = this.dig(identifier, prevScope);
scope[name] = await this.engines[0].eval(identifier, scopes);
}
});
};

stack.push({
startNode: currentNode,
Expand Down Expand Up @@ -116,24 +120,4 @@ export const getLive = (engines) => class BookshopLive {
output.outerHTML = output.innerHTML;
})
}


dig(keys, scope) {
if (!keys) return null;
if (typeof keys === "string" && /^('|").*('|")$/.test(keys)) return keys.substr(1, keys.length-2)
if (!Array.isArray(keys)) keys = keys.split('.');
const key = keys.shift();
const indexMatches = key.match(/(?<key>.*)\[(?<index>\d*)\]$/);
scope = {...this.globalData, ...(scope ?? this.data)};
if (indexMatches) {
scope = (scope ?? this.data)[indexMatches.groups["key"]];
scope = (scope ?? this.data)[parseInt(indexMatches.groups["index"])];
} else {
scope = (scope ?? this.data)[key];
}
if(scope && keys.length) {
return this.dig(keys, scope);
}
return scope;
}
}
}
133 changes: 133 additions & 0 deletions javascript-modules/live/lib/app/live.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import test from 'ava';
import browserEnv from 'browser-env';
import { getLive } from './live.js';
import { Engine as JekyllEngine } from '@bookshop/jekyll-engine';
import { Engine as EleventyEngine } from '@bookshop/eleventy-engine';

browserEnv();

const jekyllComponent = (k) => `components/${k}/${k}.jekyll.html`;
const jekyllFiles = {
[jekyllComponent('title')]: "<h1>{{ include.title }}</h1>",
[jekyllComponent('super-title')]: "{% bookshop title title=include.one %}<h1>{{ include.two }}</h2>{% bookshop title title=include.three %}",
[jekyllComponent('title-loop')]: "{% for t in include.titles %}{% bookshop title title=t %}{% endfor %}",
[jekyllComponent('num-loop')]: "{% for t in (include.min..include.max) %}{% bookshop title title=t %}{% endfor %}",
[jekyllComponent('wrapper')]: "{% bookshop {{include.component}} bind=page %}",
}

const eleventyComponent = (k) => `components/${k}/${k}.eleventy.liquid`;
const eleventyFiles = {
[eleventyComponent('title')]: "<h1>{{ title }}</h1>",
[eleventyComponent('super-title')]: "{% bookshop 'title' title: one %}<h1>{{ two }}</h2>{% bookshop 'title' title: three %}",
[eleventyComponent('title-loop')]: "{% for t in titles %}{% bookshop 'title' title: t %}{% endfor %}",
[eleventyComponent('num-loop')]: "{% for t in (min..max) %}{% bookshop 'title' title: t %}{% endfor %}",
[eleventyComponent('wrapper')]: "{% bookshop '{{component}}' bind: page %}",
}

const initial = async (liveInstance, component, props) => {
await liveInstance.engines[0].render(
document.querySelector('body'),
component,
props
);
}

const initialSub = async (liveInstance, component, props) => {
await liveInstance.engines[0].render(
document.querySelector('body'),
'wrapper',
{component},
{page: props}
);
}

const getBody = () => document.querySelector('body').innerHTML;
const setBody = (h) => document.querySelector('body').innerHTML = Array.isArray(h) ? h.join('') : h;

test.beforeEach(async t => {
setBody('');
t.context = {
jekyll: new (getLive([new JekyllEngine({files: jekyllFiles})]))(),
eleventy: new (getLive([new EleventyEngine({files: eleventyFiles})]))()
};
})

for (const impl of ['jekyll', 'eleventy']) {
test.serial(`[${impl}]: Re-renders a simple component`, async t => {
await initialSub(t.context[impl], 'title', {title: 'Bookshop'});
t.is(getBody(), [
`<!--bookshop-live name(title) params(bind: page)-->`,
`<h1>Bookshop<\/h1>`,
`<!--bookshop-live end-->`
].join(''));

await t.context[impl].update({page: {title: 'Live Love Laugh'}})
t.is(getBody(), [
`<!--bookshop-live name(title) params(bind: page)-->`,
`<h1>Live Love Laugh<\/h1>`,
`<!--bookshop-live end-->`
].join(''));
});

test.serial(`[${impl}]: Re-renders in a loop`, async t => {
await initialSub(t.context[impl], 'title-loop', {titles: ['Bookshop', 'Jekyll', 'Eleventy']});
t.regex(getBody(), /<h1>Jekyll<\/h1>/);

let trigger = false;
// Add event listener to the first h1 'Bookshop'
document.querySelectorAll('h1')[0].addEventListener('click', () => trigger = true);

await t.context[impl].update({page: {titles: ['Bookshop', 'Hugo', 'Eleventy']}})
t.regex(getBody(), /<h1>Hugo<\/h1>/);
t.notRegex(getBody(), /<h1>Jekyll<\/h1>/);

// Check that the page was only partially rendered
// By clicking the first h1 that should have been
// left untouched.
t.is(trigger, false);
document.querySelectorAll('h1')[0].click();
t.is(trigger, true);
});

test.serial(`[${impl}]: Re-renders top-level loop`, async t => {
await initial(t.context[impl], 'title-loop', {titles: ['Bookshop', 'Jekyll', 'Eleventy']});
t.regex(getBody(), /<h1>Jekyll<\/h1>/);

await t.context[impl].update({titles: ['Bookshop', 'Hugo', 'Eleventy']})
t.regex(getBody(), /<h1>Hugo<\/h1>/);
t.notRegex(getBody(), /<h1>Jekyll<\/h1>/);
});

test.serial(`[${impl}]: Re-renders range loop`, async t => {
await initial(t.context[impl], 'num-loop', {min: 0, max: 1});
t.regex(getBody(), /<h1>0<\/h1>.*<h1>1<\/h1>/);
t.notRegex(getBody(), /<h1>2<\/h1>/);

await t.context[impl].update({min: 4, max: 5});
t.regex(getBody(), /<h1>4<\/h1>.*<h1>5<\/h1>/);
t.notRegex(getBody(), /<h1>0<\/h1>/);
});

test.serial(`[${impl}]: Re-renders depth first`, async t => {
await initialSub(t.context[impl], 'super-title', {one: "One", two: "Two", three: "Three"});
t.regex(getBody(), /<h1>One<\/h1>/);

let trigger = false;
// Add event listener to h1 not rendered from a subcomponent 'Two'
document.querySelectorAll('h1')[1].addEventListener('click', () => trigger = true);

await t.context[impl].update({page: {one: "Uno", two: "Two", three: "Tres"}})
t.regex(getBody(), /<h1>Uno<\/h1>/);
t.regex(getBody(), /<h1>Two<\/h1>/);
t.regex(getBody(), /<h1>Tres<\/h1>/);
t.notRegex(getBody(), /<h1>One<\/h1>/);
t.notRegex(getBody(), /<h1>Three<\/h1>/);

// Check that the page was only partially rendered
// By clicking the h1 that should have been
// left untouched.
t.is(trigger, false);
document.querySelectorAll('h1')[1].click();
t.is(trigger, true);
});
}
3 changes: 3 additions & 0 deletions javascript-modules/live/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
"access": "public"
},
"devDependencies": {
"@bookshop/jekyll-engine": "workspace:engines/jekyll-engine",
"@bookshop/eleventy-engine": "workspace:engines/eleventy-engine",
"ava": "^3.15.0",
"browser-env": "^3.3.0",
"nyc": "^15.1.0"
},
"dependencies": {
Expand Down
Loading

0 comments on commit 9ee51c0

Please sign in to comment.