Skip to content

Commit

Permalink
DEV: Upgrade our widget handlebars compiler
Browse files Browse the repository at this point in the history
Now supports subexpressions such as i18n and concat, plus automatic
attaching of widgets similar to ember.
  • Loading branch information
eviltrout committed May 2, 2019
1 parent e696903 commit 3cb0d27
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 110 deletions.
2 changes: 1 addition & 1 deletion app/assets/javascripts/discourse/lib/transform-post.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export default function transformPost(
acted,
count,
canUndo: a.can_undo,
canDeferFlags: a.can_defer_flags,
canIgnoreFlags: a.can_defer_flags,
description: actionDescription(action, acted, count)
};
});
Expand Down
67 changes: 16 additions & 51 deletions app/assets/javascripts/discourse/widgets/actions-summary.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,12 @@ createWidget("small-user-list", {

createWidget("action-link", {
tagName: "span.action-link",
template: hbs`<a>{{attrs.text}}. </a>`,

buildClasses(attrs) {
return attrs.className;
},

html(attrs) {
return h("a", [attrs.text, ". "]);
},

click() {
this.sendWidgetAction(this.attrs.action);
}
Expand All @@ -82,56 +79,24 @@ createWidget("actions-summary-item", {
buildKey: attrs => `actions-summary-item-${attrs.id}`,

defaultState() {
return { users: [] };
return { users: null };
},

html(attrs, state) {
const users = state.users;

const result = [];
const action = attrs.action;

if (users.length === 0) {
result.push(
this.attach("action-link", {
action: "whoActed",
text: attrs.description
})
);
} else {
result.push(
this.attach("small-user-list", {
users,
description: `post.actions.people.${action}`
})
);
}

if (attrs.canUndo) {
result.push(
this.attach("action-link", {
action: "undo",
className: "undo",
text: I18n.t(`post.actions.undo.${action}`)
})
);
}
template: hbs`
{{#if state.users}}
{{small-user-list users=state.users description=(concat "post.actions.people." attrs.action)}}
{{else}}
{{action-link action="whoActed" text=attrs.description}}
{{/if}}
if (attrs.canDeferFlags) {
const flagsDesc = I18n.t(`post.actions.defer_flags`, {
count: attrs.count
});
result.push(
this.attach("action-link", {
action: "deferFlags",
className: "defer-flags",
text: flagsDesc
})
);
}
{{#if attrs.canUndo}}
{{action-link action="undo" className="undo" text=(i18n (concat "post.actions.undo." attrs.action))}}
{{/if}}
return result;
},
{{#if attrs.canIgnoreFlags}}
{{action-link action="deferFlags" className="defer-flags" text=(i18n "post.actions.defer_flags" count=attrs.count)}}
{{/if}}
`,

whoActed() {
const attrs = this.attrs;
Expand Down Expand Up @@ -159,7 +124,7 @@ export default createWidget("actions-summary", {
tagName: "section.post-actions",
template: hbs`
{{#each attrs.actionsSummary as |as|}}
{{attach widget="actions-summary-item" attrs=as}}
{{actions-summary-item attrs=as}}
<div class='clearfix'></div>
{{/each}}
{{#if attrs.deleted_at}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import hbs from "discourse/widgets/hbs-compiler";
createWidget("header-contents", {
tagName: "div.contents.clearfix",
template: hbs`
{{attach widget="home-logo" attrs=attrs}}
{{home-logo attrs=attrs}}
{{#if attrs.topic}}
{{attach widget="header-topic-info" attrs=attrs}}
{{header-topic-info attrs=attrs}}
{{/if}}
<div class="panel clearfix">{{yield}}</div>
`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ createWidget("pm-map-user-group", {
<span class="group-name">{{attrs.group.name}}</span>
</a>
{{#if attrs.isEditing}}
{{#if attrs.canRemoveAllowedUsers}}
{{attach widget="pm-remove-group-link" attrs=attrs.group}}
{{/if}}
{{#if attrs.canRemoveAllowedUsers}}
{{pm-remove-group-link attrs=attrs.group}}
{{/if}}
{{/if}}
`
});
Expand Down
15 changes: 6 additions & 9 deletions app/assets/javascripts/discourse/widgets/user-menu.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,12 @@ createWidget("user-menu-dismiss-link", {
template: hbs`
<ul class='menu-links'>
<li>
{{attach
widget="link"
attrs=(hash
action="dismissNotifications"
className="dismiss"
tabindex="0"
icon="check"
label="user.dismiss"
title="user.dismiss_notifications_tooltip")}}
{{link action="dismissNotifications"
className="dismiss"
tabindex="0"
icon="check"
label="user.dismiss"
title="user.dismiss_notifications_tooltip"}}
</li>
</ul>
`
Expand Down
97 changes: 69 additions & 28 deletions lib/javascripts/widget-hbs-compiler.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,73 @@ function resolve(path) {
return path;
}

function sexpValue(value) {
if (!value) {
return;
}

let pValue = value.original;
if (value.type === "StringLiteral") {
return JSON.stringify(pValue);
} else if (value.type === "SubExpression") {
return sexp(value);
}
return pValue;
}

function pairsToObj(pairs) {
let result = [];

pairs.forEach(p => {
result.push(`"${p.key}": ${sexpValue(p.value)}`);
});

return `{ ${result.join(", ")} }`;
}

function i18n(node) {
let key = sexpValue(node.params[0]);

let hash = node.hash;
if (hash.pairs.length) {
return `I18n.t(${key}, ${pairsToObj(hash.pairs)})`;
}

return `I18n.t(${key})`;
}

function sexp(value) {
if (value.path.original === "hash") {
let result = [];

value.hash.pairs.forEach(p => {
let pValue = p.value.original;
if (p.value.type === "StringLiteral") {
pValue = JSON.stringify(pValue);
}
return pairsToObj(value.hash.pairs);
}

result.push(`"${p.key}": ${pValue}`);
if (value.path.original === "concat") {
let result = [];
value.params.forEach(p => {
result.push(sexpValue(p));
});
return result.join(" + ");
}

return `{ ${result.join(", ")} }`;
if (value.path.original === "i18n") {
return i18n(value);
}
}

function argValue(arg) {
let value = arg.value;
function valueOf(value) {
if (value.type === "SubExpression") {
return sexp(arg.value);
return sexp(value);
} else if (value.type === "PathExpression") {
return value.original;
} else if (value.type === "StringLiteral") {
return JSON.stringify(value.value);
}
}

function argValue(arg) {
return valueOf(arg.value);
}

function useHelper(state, name) {
let id = state.helpersUsed[name];
if (!id) {
Expand Down Expand Up @@ -60,17 +99,7 @@ function mustacheValue(node, state) {
return `this.attrs.contents()`;
break;
case "i18n":
let value;
if (node.params[0].type === "StringLiteral") {
value = `"${node.params[0].value}"`;
} else if (node.params[0].type === "PathExpression") {
value = resolve(node.params[0].original);
}

if (value) {
return `I18n.t(${value})`;
}

return i18n(node);
break;
case "avatar":
let template = argValue(node.hash.pairs.find(p => p.key === "template"));
Expand All @@ -82,14 +111,26 @@ function mustacheValue(node, state) {
)}(${size}, { template: ${template}, username: ${username} })`;
break;
case "date":
value = resolve(node.params[0].original);
return `${useHelper(state, "dateNode")}(${value})`;
return `${useHelper(state, "dateNode")}(${valueOf(node.params[0])})`;
break;
case "d-icon":
let icon = node.params[0].value;
return `${useHelper(state, "iconNode")}("${icon}")`;
return `${useHelper(state, "iconNode")}(${valueOf(node.params[0])})`;
break;
default:
// Shortcut: If our mustach has hash arguments, we can assume it's attaching.
// For example `{{home-logo count=123}}` can become `this.attach('home-logo, { "count": 123 });`
let hash = node.hash;
if (hash.pairs.length) {
let widgetString = JSON.stringify(path);
// magic: support applying of attrs. This is commonly done like `{{home-logo attrs=attrs}}`
let firstPair = hash.pairs[0];
if (firstPair.key === "attrs") {
return `this.attach(${widgetString}, ${firstPair.value.original})`;
}

return `this.attach(${widgetString}, ${pairsToObj(hash.pairs)})`;
}

if (node.escaped) {
return `${resolve(path)}`;
} else {
Expand Down Expand Up @@ -168,7 +209,7 @@ class Compiler {
case "MustacheStatement":
const value = mustacheValue(node, this.state);
if (value) {
instructions.push(`${parentAcc}.push(${value})`);
instructions.push(`${parentAcc}.push(${value});`);
}
break;
case "BlockStatement":
Expand Down
20 changes: 5 additions & 15 deletions script/test_hbs_compiler.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
template = <<~HBS
{{attach widget="widget-name" attrs=attrs}}
{{a}}
{{{htmlValue}}}
{{#if state.category}}
{{attach widget="category-display" attrs=(hash category=state.category someNumber=123 someString="wat")}}
{{/if}}
{{#each transformed.something as |s|}}
{{s.wat}}
{{/each}}
{{attach widget=settings.widgetName}}
{{#unless settings.hello}}
XYZ
{{/unless}}
{{attach widget="wat" attrs=(hash test="abc" text=(i18n "hello" count=attrs.wat))}}
{{action-link action="undo" className="undo" text=(i18n (concat "post.actions.undo." attrs.action))}}
{{actions-summary-item attrs=as}}
{{attach widget="actions-summary-item" attrs=as}}
{{testing value="hello"}}
HBS

ctx = MiniRacer::Context.new(timeout: 15000)
Expand Down
2 changes: 1 addition & 1 deletion test/javascripts/widgets/actions-summary-test.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ widgetTest("deferFlags", {
{
action: "off_topic",
description: "very off topic",
canDeferFlags: true,
canIgnoreFlags: true,
count: 1
}
]
Expand Down
39 changes: 39 additions & 0 deletions test/javascripts/widgets/widget-test.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,45 @@ widgetTest("widget attaching", {
}
});

widgetTest("magic attaching by name", {
template: `{{mount-widget widget="attach-test"}}`,

beforeEach() {
createWidget("test-embedded", { tagName: "div.embedded" });

createWidget("attach-test", {
tagName: "div.container",
template: hbs`{{test-embedded attrs=attrs}}`
});
},

test(assert) {
assert.ok(find(".container").length, "renders container");
assert.ok(find(".container .embedded").length, "renders attached");
}
});

widgetTest("custom attrs to a magic attached widget", {
template: `{{mount-widget widget="attach-test"}}`,

beforeEach() {
createWidget("testing", {
tagName: "span.value",
template: hbs`{{attrs.value}}`
});

createWidget("attach-test", {
tagName: "div.container",
template: hbs`{{testing value=(concat "hello" " " "world")}}`
});
},

test(assert) {
assert.ok(find(".container").length, "renders container");
assert.equal(find(".container .value").text(), "hello world");
}
});

widgetTest("handlebars d-icon", {
template: `{{mount-widget widget="hbs-icon-test" args=args}}`,

Expand Down

0 comments on commit 3cb0d27

Please sign in to comment.