Changes related to #492 - Added support for body-slot and putting functions in state

This commit is contained in:
Patrick Steele-Idem 2016-12-30 12:16:52 -07:00
parent 58ca13be22
commit 306e19b88e
64 changed files with 578 additions and 93 deletions

View File

@ -403,6 +403,10 @@ class CustomTag extends HtmlElement {
Object.assign(this._additionalProps, additionalProps);
}
hasProp(name) {
return this._additionalProps && this._additionalProps.hasOwnProperty(name);
}
addProp(name, value) {
if (!this._additionalProps) {
this._additionalProps = {};

View File

@ -137,6 +137,7 @@
},
"minprops": {
"exclude": [
"b",
"c",
"ca",
"e",

View File

@ -0,0 +1,11 @@
module.exports = function codeGenerator(elNode, generator) {
var context = generator.context;
var builder = generator.builder;
var includeNode = context.createNodeForEl('include');
includeNode.addProp(
'_target',
builder.memberExpression(
builder.identifier('data'),
builder.identifier('renderBody')));
return includeNode;
};

View File

@ -163,8 +163,30 @@ var coreAttrHandlers = [
'include', function(attr, node, el) {
var context = this.context;
var includeNode = context.createNodeForEl('include', null, attr.argument);
node.appendChild(includeNode);
if (attr.argument) {
var includeNode = context.createNodeForEl('include', null, attr.argument);
node.appendChild(includeNode);
} else {
context.addError(el, 'The include attribute must have an argument. For example: include("./target.marko") or include(data.renderBody)');
}
}
],
[
'body-slot', function(attr, node, el) {
var context = this.context;
if (attr.argument) {
context.addError(el, 'The body-slot attribute should not have an argument.');
return;
}
if (attr.value) {
context.addError(el, 'The body-slot attribute should not have a value.');
return;
}
var bodySlot = context.createNodeForEl('body-slot');
node.appendChild(bodySlot);
}
]
];

View File

@ -1,34 +1,29 @@
'use strict';
module.exports = function codeGenerator(el, context) {
module.exports = function(el, context) {
let builder = context.builder;
let target;
let arg;
if (el.argument) {
let args = el.argument && builder.parseJavaScriptArgs(el.argument);
el.argument = null;
target = args[0];
arg = args[1];
} else {
return;
let target = args[0];
let arg = args[1];
if (target.type === 'Literal') {
target = context.importTemplate(target.value);
}
var includeProps = {
_target: target
};
if (arg) {
includeProps._arg = arg;
}
el.addProps(includeProps);
} else if (!el.hasProp('_target')) {
context.addError(el, 'The <include(...)> tag must have an argument: <include("./target.marko")/> or <include(data.renderBody)/>');
}
if (target.type === 'Literal') {
target = context.importTemplate(target.value);
}
var includeProps = {
_target: target
};
if (arg) {
includeProps._arg = arg;
}
el.data.includeTarget = target;
el.addProps(includeProps);
};

View File

@ -9,6 +9,17 @@
}
]
},
"<body-slot>": {
"code-generator": "./body-slot-tag",
"attributes": {},
"autocomplete": [
{
"displayText": "body-slot",
"snippet": "body-slot",
"openTagOnly": true
}
]
},
"<else>": {
"node-factory": "./else-tag",
"attributes": {},
@ -299,8 +310,8 @@
"descriptionMoreURL": "http://markojs.com/docs/marko/language-guide/#includes"
},
{
"displayText": "include()",
"snippet": "include()",
"displayText": "include(data.renderBody)",
"snippet": "include(data.renderBody)",
"descriptionMoreURL": "http://markojs.com/docs/marko/language-guide/#includes"
},
{

View File

@ -0,0 +1 @@
<div><h1>Test</h1><div class="body"><strong>This is the body</strong></div></div>

View File

@ -0,0 +1,3 @@
<div body-slot(invalid)>
</div>

View File

@ -0,0 +1,8 @@
var expect = require('chai').expect;
exports.templateData = {};
exports.checkError = function(e) {
var message = e.toString();
expect(message).to.contain('The body-slot attribute should not have an argument');
};

View File

@ -0,0 +1 @@
<div><h1>Test</h1><div class="body"><strong>This is the body</strong></div></div>

View File

@ -0,0 +1,3 @@
<div body-slot='invalid'>
</div>

View File

@ -0,0 +1,8 @@
var expect = require('chai').expect;
exports.templateData = {};
exports.checkError = function(e) {
var message = e.toString();
expect(message).to.contain('The body-slot attribute should not have a value');
};

View File

@ -0,0 +1 @@
<div><h1>Test</h1><div class="body"><strong>This is the body</strong></div></div>

View File

@ -0,0 +1,4 @@
<div>
<h1>Test</h1>
<div.body body-slot/>
</div>

View File

@ -0,0 +1,3 @@
<include('./include-target.marko')>
<strong>This is the body</strong>
</include>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -0,0 +1 @@
<div><h1>Test</h1><div class="body"><strong>This is the body</strong></div></div>

View File

@ -0,0 +1,6 @@
<div>
<h1>Test</h1>
<div.body>
<body-slot/>
</div>
</div>

View File

@ -0,0 +1,3 @@
<include('./include-target.marko')>
<strong>This is the body</strong>
</include>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -0,0 +1 @@
<div><h1>Test</h1><div class="body"><strong>This is the body</strong></div></div>

View File

@ -0,0 +1,2 @@
<div include()>
</div>

View File

@ -0,0 +1,9 @@
var expect = require('chai').expect;
exports.templateData = {};
exports.checkError = function(e) {
var message = e.toString();
expect(message).to.contain('template.marko:1:0');
expect(message).to.contain('The include attribute must have an argument');
};

View File

@ -0,0 +1 @@
<div><h1>Test</h1><div class="body"><strong>This is the body</strong></div></div>

View File

@ -0,0 +1,3 @@
<div>
<include()/>
</div>

View File

@ -0,0 +1,8 @@
var expect = require('chai').expect;
exports.templateData = {};
exports.checkError = function(e) {
var message = e.toString();
expect(message).to.contain('The <include(...)> tag must have an argument');
};

View File

@ -18,6 +18,7 @@
"ref",
"w-for",
"w-id",
"body-slot",
"w-body",
"w-preserve",
"w-preserve-body",

View File

@ -11,6 +11,7 @@
"lasso-resource",
"browser-refresh",
"assign",
"body-slot",
"else",
"else-if",
"for",

View File

@ -16,6 +16,7 @@
"await-timeout",
"bar",
"body",
"body-slot",
"browser-refresh",
"cached-fragment",
"else",

View File

@ -4,5 +4,5 @@
'app-button-' + state.variant,
'app-button-' + state.size
] onClick("handleClick")>
<span include()/>
<span body-slot/>
</button>

View File

@ -1,4 +1,6 @@
<app-button ref="button" class=state.className onClick('handleClick')>
<span class="app-checkbox-icon"/>
<span ref="checkboxLabel" include()/>
<span ref="checkboxLabel">
<body-slot/>
</span>
</app-button>

View File

@ -10,5 +10,5 @@
<div.foo>
<span>Hello ${state.name}!</span>
<div.body><include()/></div>
<div.body><body-slot/></div>
</div>

View File

@ -0,0 +1,31 @@
<script>
module.exports = {
onInput: function(input) {
this.state = {
size: input.size || 'normal',
variant: input.variant || 'primary'
};
this.body = input.renderBody;
},
// Add any other methods here
setVariant: function(variant) {
this.state.variant = variant;
},
setSize: function(size) {
this.state.size = size;
},
setLabel: function(label) {
this.state.label = label;
}
};
</script>
var variantClassName=(state.variant !== 'primary' && 'app-button-' + state.variant)
var sizeClassName=(state.size !== 'normal' && 'app-button-' + state.size)
<button class=['app-button', variantClassName, sizeClassName]>
<span body-slot/>
</button>

View File

@ -0,0 +1,11 @@
<script>
module.exports = {
};
</script>
<div>
<app-button size="small" variant="primary" ref="button">
<b>${data.name}</b>
</app-button>
</div>

View File

@ -0,0 +1,23 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {
name: 'Frank'
});
var buttonWidget = widget.getWidget('button');
expect(buttonWidget.el.innerHTML).to.contain('Frank');
expect(buttonWidget.el.className).to.equal('app-button app-button-small');
// Button widget will not rerender since it's state did not change and that means that the
// button content will remain as 'John' instead of 'Frank'
widget.setProps({ name: 'John '});
widget.update();
expect(buttonWidget.el.innerHTML).to.contain('John');
buttonWidget.setSize('large');
buttonWidget.update();
expect(buttonWidget.el.innerHTML).to.contain('John');
expect(buttonWidget.el.className).to.equal('app-button app-button-large');
};

View File

@ -0,0 +1,32 @@
<script>
module.exports = {
onInput: function(input) {
this.state = {
size: input.size || 'normal',
variant: input.variant || 'primary'
};
},
// Add any other methods here
setVariant: function(variant) {
this.state.variant = variant;
},
setSize: function(size) {
this.state.size = size;
},
setLabel: function(label) {
this.state.label = label;
}
};
</script>
var variantClassName=(state.variant !== 'primary' && 'app-button-' + state.variant)
var sizeClassName=(state.size !== 'normal' && 'app-button-' + state.size)
<button class=['app-button', variantClassName, sizeClassName]>
<span>
<body-slot/>
</span>
</button>

View File

@ -0,0 +1,11 @@
<script>
module.exports = {
};
</script>
<div>
<app-button size="small" variant="primary" ref="button">
<b>${data.name}</b>
</app-button>
</div>

View File

@ -0,0 +1,21 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {
name: 'Frank'
});
var buttonWidget = widget.getWidget('button');
expect(buttonWidget.el.innerHTML).to.contain('Frank');
expect(buttonWidget.el.className).to.equal('app-button app-button-small');
widget.setProps({ name: 'John '});
widget.update();
expect(buttonWidget.el.innerHTML).to.contain('John');
buttonWidget.setSize('large');
buttonWidget.update();
expect(buttonWidget.el.innerHTML).to.contain('John');
expect(buttonWidget.el.className).to.equal('app-button app-button-large');
};

View File

@ -0,0 +1,33 @@
<script>
module.exports = {
onInput: function(input) {
this.state = {
size: input.size || 'normal',
variant: input.variant || 'primary'
};
this.body = input.renderBody;
},
// Add any other methods here
setVariant: function(variant) {
this.state.variant = variant;
},
setSize: function(size) {
this.state.size = size;
},
setLabel: function(label) {
this.state.label = label;
}
};
</script>
var variantClassName=(state.variant !== 'primary' && 'app-button-' + state.variant)
var sizeClassName=(state.size !== 'normal' && 'app-button-' + state.size)
<button class=['app-button', variantClassName, sizeClassName]>
<span>
<body-slot/>
</span>
</button>

View File

@ -0,0 +1,11 @@
<script>
module.exports = {
};
</script>
<div>
<app-button size="small" variant="primary" ref="button">
<b>${data.name}</b>
</app-button>
</div>

View File

@ -0,0 +1,21 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {
name: 'Frank'
});
var buttonWidget = widget.getWidget('button');
expect(buttonWidget.el.innerHTML).to.contain('Frank');
expect(buttonWidget.el.className).to.equal('app-button app-button-small');
widget.setProps({ name: 'John '});
widget.update();
expect(buttonWidget.el.innerHTML).to.contain('John');
buttonWidget.setSize('large');
buttonWidget.update();
expect(buttonWidget.el.innerHTML).to.contain('John');
expect(buttonWidget.el.className).to.equal('app-button app-button-large');
};

View File

@ -0,0 +1,31 @@
<script>
module.exports = {
onInput: function(input) {
this.state = {
size: input.size || 'normal',
variant: input.variant || 'primary',
body: input.label || input.renderBody
};
},
// Add any other methods here
setVariant: function(variant) {
this.state.variant = variant;
},
setSize: function(size) {
this.state.size = size;
},
setLabel: function(label) {
this.state.label = label;
}
};
</script>
var variantClassName=(state.variant !== 'primary' && 'app-button-' + state.variant)
var sizeClassName=(state.size !== 'normal' && 'app-button-' + state.size)
<button class=['app-button', variantClassName, sizeClassName]>
<span include(state.body)/>
</button>

View File

@ -0,0 +1,11 @@
<script>
module.exports = {
};
</script>
<div>
<app-button size="small" variant="primary" ref="button">
<b>${data.name}</b>
</app-button>
</div>

View File

@ -0,0 +1,29 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {
name: 'Frank'
});
var buttonWidget = widget.getWidget('button');
expect(buttonWidget.el.innerHTML).to.contain('Frank');
expect(buttonWidget.el.className).to.equal('app-button app-button-small');
widget.rerender({ name: 'John '});
expect(buttonWidget.el.innerHTML).to.contain('John');
buttonWidget.setSize('large');
buttonWidget.update();
expect(buttonWidget.el.innerHTML).to.contain('John');
expect(buttonWidget.el.className).to.equal('app-button app-button-large');
buttonWidget.rerender({
size: 'small',
variant: 'secondary'
// NOTE: We aren't including renderBody() but we expect that content to be preserved
});
expect(buttonWidget.el.innerHTML).to.contain('John');
expect(buttonWidget.el.className).to.equal('app-button app-button-secondary app-button-small');
};

View File

@ -0,0 +1,30 @@
<script>
module.exports = {
onInput: function(input) {
this.state = {
size: input.size || 'normal',
variant: input.variant || 'primary'
};
},
// Add any other methods here
setVariant: function(variant) {
this.state.variant = variant;
},
setSize: function(size) {
this.state.size = size;
},
setLabel: function(label) {
this.state.label = label;
}
};
</script>
var variantClassName=(state.variant !== 'primary' && 'app-button-' + state.variant)
var sizeClassName=(state.size !== 'normal' && 'app-button-' + state.size)
<button class=['app-button', variantClassName, sizeClassName]>
<span include(data.renderBody)/>
</button>

View File

@ -0,0 +1,11 @@
<script>
module.exports = {
};
</script>
<div>
<app-button size="small" variant="primary" ref="button">
<b>${data.name}</b>
</app-button>
</div>

View File

@ -0,0 +1,23 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {
name: 'Frank'
});
var buttonWidget = widget.getWidget('button');
expect(buttonWidget.el.innerHTML).to.contain('Frank');
expect(buttonWidget.el.className).to.equal('app-button app-button-small');
// Button widget will not rerender since it's state did not change and that means that the
// button content will remain as 'John' instead of 'Frank'
widget.setProps({ name: 'John '});
widget.update();
expect(buttonWidget.el.innerHTML).to.contain('Frank');
buttonWidget.setSize('large');
buttonWidget.update();
expect(buttonWidget.el.innerHTML).to.contain('Frank');
expect(buttonWidget.el.className).to.equal('app-button app-button-large');
};

View File

@ -6,10 +6,7 @@ module.exports = {
className: input['class'],
attrs: input['*']
};
},
getInitialBody: function(input) {
return input.label || input.renderBody;
this.body = input.label || input.renderBody;
},
getTemplateData: function(state, input) {
var rootAttrs = {};

View File

@ -1,4 +1,4 @@
<button ${data.rootAttrs}
onClick("handleClick")
w-bind
include()></button>
onClick("handleClick")>
<body-slot/>
</button>

View File

@ -1,4 +1,3 @@
<button ${data.rootAttrs}
onClick("handleClick")
w-bind
include()></button>
<button ${data.rootAttrs} onClick("handleClick")>
<body-slot/>
</button>

View File

@ -1,3 +1,4 @@
<button ${data.rootAttrs}
onClick("handleClick")
include()></button>
onClick("handleClick")>
<body-slot/>
</button>

View File

@ -1,4 +1,4 @@
<button ${data.rootAttrs}
onClick("handleClick")
w-bind
include()></button>
onClick("handleClick")>
<body-slot/>
</button>

View File

@ -4,6 +4,8 @@ var className=('alert alert-'+type)
<div class=className role="alert">
<div>
<h1>ALERT! ${type} ${Date.now()}</h1>
<div include/>
<div>
<body-slot/>
</div>
</div>
</div>

View File

@ -17,7 +17,7 @@ function render(data, out, widget, state) {
">");
include_tag({
_target: widget.body,
_target: widget.b,
_elId: widget.elId(0),
_arg: widget
}, out);

View File

@ -1,3 +1,4 @@
<button ${data.rootAttrs}
onClick("handleClick")
include()></button>
onClick("handleClick")>
<body-slot/>
</button>

View File

@ -69,10 +69,6 @@ State.prototype = {
ensure(self, name);
}
if (typeof value === 'function') {
return;
}
if (value === null) {
// Treat null as undefined to simplify our comparison logic
value = undefined;

View File

@ -21,7 +21,7 @@ function WidgetDef(id, out, widgetStack, widgetStackLen) {
this.$__customEvents = // An array containing information about custom events
this.$__bodyElId = // The ID for the default body element (if any any)
this.$__roots = // IDs of root elements if there are multiple root elements
this.body =
this.b =
this.$__existingWidget =
this.$__children = // An array of nested WidgetDef instances
this.$__domEvents = // An array of DOM events that need to be added (in sets of three)

View File

@ -125,17 +125,6 @@ function initWidget(widgetDef, doc) {
widgetLookup[id] = widget;
if (state) {
for (var k in state) {
if (state.hasOwnProperty(k)) {
var v = state[k];
if (typeof v === 'function' || v == null) {
delete state[k];
}
}
}
}
widget.state = state || {}; // First time rendering so use the provided state or an empty state object
if (!config) {

View File

@ -159,8 +159,10 @@ module.exports = function createRendererFunc(templateRenderFunc, widgetProps, re
var lightweightWidget = Object.create(renderingLogic);
lightweightWidget.onInput(input);
widgetState = lightweightWidget.state;
widgetBody = lightweightWidget.body;
widgetConfig = lightweightWidget;
delete widgetConfig.state;
delete widgetConfig.body;
} else {
if (getWidgetConfig) {
// If getWidgetConfig() was implemented then use that to
@ -172,16 +174,17 @@ module.exports = function createRendererFunc(templateRenderFunc, widgetProps, re
} else {
widgetConfig = input.widgetConfig;
}
if (getInitialBody) {
// If we have widget a widget body then pass it to the template
// so that it is available to the widget tag and can be inserted
// at the w-body marker
widgetBody = getInitialBody(input, out);
}
}
if (getInitialBody) {
// If we have widget a widget body then pass it to the template
// so that it is available to the widget tag and can be inserted
// at the w-body marker
widgetBody = getInitialBody(input, out);
} else {
if (!widgetBody) {
// Default to using the nested content as the widget body
// getInitialBody was not implemented
widgetBody = input.renderBody;
}
@ -292,7 +295,7 @@ module.exports = function createRendererFunc(templateRenderFunc, widgetProps, re
widgetDef.$__existingWidget = existingWidget;
widgetDef.$__bodyElId = bodyElId;
widgetDef.$__roots = roots;
widgetDef.body = widgetBody;
widgetDef.b = widgetBody;
// Render the template associated with the component using the final template
// data that we constructed

View File

@ -0,0 +1,41 @@
'use strict';
var includeTagForWidgets = require.resolve('../include-tag');
module.exports = function(bodySlotNode) {
if (!this.hasBoundWidgetForTemplate()) {
return;
}
var context = this.context;
var builder = this.builder;
var parentNode = bodySlotNode.parentNode;
parentNode._normalizeChildTextNodes(context);
if (parentNode.childCount !== 1) {
throw new Error('TBD');
}
let parentTransformHelper = this.getTransformHelper(parentNode);
let includeNode = context.createNodeForEl('include');
includeNode.data.bodySlot = true;
includeNode.addProp('_target', builder.memberExpression(builder.identifier('widget'), builder.identifier('b')));
includeNode.addProp('_elId', parentTransformHelper.getIdExpression());
includeNode.addProp('_arg', builder.identifier('widget'));
parentTransformHelper.assignWidgetId(false /* not repeated */);
var widgetProps = this.getWidgetProps();
widgetProps.body = parentTransformHelper.getNestedIdExpression();
includeNode.setRendererPath(includeTagForWidgets);
// includeNode.onBeforeGenerateCode(function() {
// includeNode.addProp('_elId', parentTransformHelper.getIdExpression());
// includeNode.addProp('_arg', builder.identifier('widget'));
// });
bodySlotNode.replaceWith(includeNode);
};

View File

@ -21,7 +21,11 @@ module.exports = function(includeNode) {
let parentTransformHelper = this.getTransformHelper(parentNode);
if (includeNode.argument) {
if (includeNode.data.bodySlot) {
parentTransformHelper.assignWidgetId(false /* not repeated */);
var widgetProps = this.getWidgetProps();
widgetProps.body = parentTransformHelper.getNestedIdExpression();
} else {
let widgetIdInfo = parentTransformHelper.assignWidgetId(true /* repeated */);
if (!widgetIdInfo.idVarNode) {
let idVarNode = widgetIdInfo.createIdVarNode();
@ -29,19 +33,11 @@ module.exports = function(includeNode) {
event.insertCode(idVarNode);
});
}
} else {
parentTransformHelper.assignWidgetId(false /* not repeated */);
var widgetProps = this.getWidgetProps();
widgetProps.body = parentTransformHelper.getNestedIdExpression();
}
includeNode.setRendererPath(includeTagForWidgets);
includeNode.onBeforeGenerateCode(function() {
if (!includeNode.data.includeTarget) {
includeNode.addProp('_target', builder.memberExpression(builder.identifier('widget'), builder.identifier('body')));
}
includeNode.addProp('_elId', parentTransformHelper.getIdExpression());
includeNode.addProp('_arg', builder.identifier('widget'));
});

View File

@ -142,6 +142,7 @@ class TransformHelper {
}
TransformHelper.prototype.assignWidgetId = require('./assignWidgetId');
TransformHelper.prototype.handleBodySlotNode = require('./handleBodySlotNode');
TransformHelper.prototype.handleRootNodes = require('./handleRootNodes');
TransformHelper.prototype.handleIncludeNode = require('./handleIncludeNode');
TransformHelper.prototype.handleWidgetEvents = require('./handleWidgetEvents');

View File

@ -96,14 +96,12 @@
}
]
},
"@body-slot": {
"preserve-name": true
},
"@w-body": {
"preserve-name": true,
"autocomplete": [
{
"openTagOnly": true,
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/get-started/#adding-dom-event-listeners"
}
]
"autocomplete": []
},
"@w-preserve": {
"type": "flag",

View File

@ -10,11 +10,23 @@ module.exports = function transform(el, context) {
}
if (el.hasAttribute('w-body')) {
var bodyAttr = el.getAttributeValue('w-body');
let bodyValue = el.getAttributeValue('w-body');
el.removeAttribute('w-body');
let includeNode = context.createNodeForEl('include', null, bodyAttr && bodyAttr.toString());
el.appendChild(includeNode);
if (bodyValue) {
let includeNode = context.createNodeForEl('include');
includeNode.addProp('_target', bodyValue);
el.appendChild(includeNode);
} else {
el.setAttributeValue('body-slot', null);
}
}
if (el.hasAttribute('body-slot')) {
el.removeAttribute('body-slot');
let bodySlotNode = context.createNodeForEl('body-slot');
el.appendChild(bodySlotNode);
}
if (el.tagName === 'widget-types') {
@ -23,6 +35,9 @@ module.exports = function transform(el, context) {
transformHelper.handleIncludeNode(el);
transformHelper.getWidgetArgs().compile(transformHelper);
return;
} else if (el.tagName === 'body-slot') {
transformHelper.handleBodySlotNode(el);
return;
}
if (el.hasAttribute('w-el-id')) {