Skip to content

Instantly share code, notes, and snippets.

@bigopon
Last active August 7, 2017 14:39
Show Gist options
  • Save bigopon/91f357a81dcf8d9a6a1720646d3b7cc2 to your computer and use it in GitHub Desktop.
Save bigopon/91f357a81dcf8d9a6a1720646d3b7cc2 to your computer and use it in GitHub Desktop.
View Compiler let
<template>
<require from='./dynamic-input.htm'></require>
<style>
.input-wrap { display: block; padding: 5px; background: #dedede; border: 1px solid #eee; }
.input-wrap + .input-wrap { margin-top: 10px; }
.input-wrap > * { padding-top: 5px; }
fieldset { margin: 5px; padding: 5px; border: 1px solid #999; }
dynamic-input { display: block; }
</style>
<div class='input-wrap'>
<h5>Dynamic Input Element:</h5>
<hr/>
<dynamic-input
view-model.ref='input'
type='text'
prop1.bind='num1'
prop2.bind='num2'
prop-1.trigger='onProp1Changed($event)'></dynamic-input>
</div>
<fieldset test='value.bind: num1; name: num3'>
<div class="input-wrap">[num1] in App: <div>${num1 || 'not known'}</div></div>
<div class="input-wrap">[prop1] in DynamicInput Element: <div>${input.prop1 || 'not known'}</div></div>
<button click.delegate="makeRandom('num1')">Set App's [num1] to a number</button>
</fieldset>
<fieldset if.bind='num2'>
<div class="input-wrap">[num2] in App: <div>${num2 || 'not known'}</div></div>
<div class="input-wrap">[prop2] in DynamicInput Element: <div>${input.prop2 || 'not known'}</div></div>
<button click.delegate="makeRandom('num2')">Set App's [num2] to a number</button>
</fieldset>
<br/>
<fieldset>
[num3] will be updated via event
<div class="input-wrap">[num3] in App: <div>${num3 || 'not known'}</div></div>
<div class="input-wrap">[prop1] in DynamicInput Element: <div>${input.prop1 || 'not known'}</div></div>
</fieldset>
</template>
import {observable} from './aurelia-framework'
import {bindable} from './aurelia-framework'
import {
HtmlBehaviorResource
} from 'aurelia-framework'
export class App {
makeRandom(prop) {
this[prop] = Math.floor(Math.random() * 1e5);
}
attached() {
// console.log(this)
}
onProp1Changed($event) {
console.log($event.detail); // notice console log, logged 2 times because we bind 2 times to same property
this.num3 = $event.detail[0];
}
}
/*eslint padded-blocks:0*/
import {useView, customElement, bindable} from 'aurelia-templating';
import {bindingMode} from 'aurelia-binding'
import {DOM} from 'aurelia-pal'
export function _createDynamicElement(name: string, viewUrl: string, bindableNames: string[]): Function {
@customElement(name)
@useView(viewUrl)
class DynamicElement {
constructor(element) {
if (element) {
this.$el = element; // is this name safe ?
}
}
bind(bindingContext) {
this.$parent = bindingContext;
}
}
let parts;
let bindableConfig
let bindableOptions;
let _bindingMode;
let baseBinding;
let eventMap;
let prop;
let notifyEvent;
for (let i = 0, ii = bindableNames.length; i < ii; ++i) {
bindableConfig = bindableNames[i];
parts = bindableConfig.split('&');
_bindingMode = parts[1];
bindableOptions = {};
// Check if has binding mode
if (_bindingMode) {
bindableOptions.defaultBindingMode = bindingMode[_bindingMode.trim()] || bindingMode.oneWay;
}
// Check if has notify prop changes by event
baseBinding = parts[0].split('^');
prop = baseBinding[0].trim();
if (notifyEvent = baseBinding[1].trim()) {
(eventMap || (eventMap = {}))[prop] = notifyEvent;
}
bindableOptions.name = prop;
bindable(bindableOptions)(DynamicElement);
}
if (eventMap) {
// should use define property or just assign ???
let proto = DynamicElement.prototype;
proto.$eventMap = eventMap;
proto.propertyChanged = propertyChanged;
// Inject element too
DynamicElement.inject = [DOM.Element];
}
return DynamicElement;
}
function propertyChanged(name, newVal, oldVal) {
const $event = DOM.createCustomEvent(this.$eventMap[name], { bubbles: true, detail: [newVal, oldVal] });
this.$el.dispatchEvent($event);
}
<template
bindable='prop1 ^prop-1 & oneWay, prop2 ^prop2Changed & twoWay'>
<div>
<label>[prop1] -- oneWay<div>Change event: 'prop-1'</div>
<input value.bind='prop1'>
</label>
</div>
<hr/>
<div>
<label>[prop2] -- twoWay<div>Change event: 'prop-2'</div>
<input value.bind='prop2'>
</label>
</div>
</template>
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
<style>
body {
padding: 20px;
}
.form-component {
display: block;
margin-bottom: 20px;
}
</style>
</head>
<body aurelia-app="main">
<h1>Loading...</h1>
<script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/config.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script>
<script>
require(['aurelia-bootstrapper']);
</script>
</body>
</html>
import './xtend-compiler';
import './xtend-html-behavior'
console.clear();
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.globalResources([
'./test'
])
.plugin('./xtend-html-resource-plugin');
aurelia.start().then(() => aurelia.setRoot());
}
import {bindable} from 'aurelia-framework'
export class TestCustomAttribute {
@bindable value
@bindable name
}
import {
DOM,
BehaviorInstruction,
TargetInstruction,
ViewCompiler
} from 'aurelia-framework'
ViewCompiler.prototype._compileElement = function(node, resources, instructions, parentNode, parentInjectorId, targetLightDOM) {
let tagName = node.tagName.toLowerCase();
let attributes = node.attributes;
let expressions = [];
let expression;
let behaviorInstructions = [];
let providers = [];
let bindingLanguage = resources.getBindingLanguage(this.bindingLanguage);
let liftingInstruction;
let viewFactory;
let type;
let elementInstruction;
let elementProperty;
let i;
let ii;
let attr;
let attrName;
let attrValue;
let originalAttrName;
let instruction;
let info;
let property;
let knownAttribute;
let auTargetID;
let injectorId;
if (tagName === 'slot') {
if (targetLightDOM) {
node = makeShadowSlot(this, resources, node, instructions, parentInjectorId);
}
return node.nextSibling;
} else if (tagName === 'template') {
if (!('content' in node)) {
throw new Error('You cannot place a template element within ' + node.namespaceURI + ' namespace')
}
viewFactory = this.compile(node, resources);
viewFactory.part = node.getAttribute('part');
} else {
type = resources.getElement(node.getAttribute('as-element') || tagName);
if (type) {
elementInstruction = BehaviorInstruction.element(node, type);
console.log({elementInstruction})
type.processAttributes(this, resources, node, attributes, elementInstruction);
behaviorInstructions.push(elementInstruction);
}
}
for (i = 0, ii = attributes.length; i < ii; ++i) {
attr = attributes[i];
originalAttrName = attrName = attr.name;
attrValue = attr.value;
info = bindingLanguage.inspectAttribute(resources, tagName, attrName, attrValue);
if (targetLightDOM && info.attrName === 'slot') {
info.attrName = attrName = 'au-slot';
}
type = resources.getAttribute(info.attrName);
elementProperty = null;
if (type) { //do we have an attached behavior?
knownAttribute = resources.mapAttribute(info.attrName); //map the local name to real name
if (knownAttribute) {
property = type.attributes[knownAttribute];
if (property) { //if there's a defined property
info.defaultBindingMode = property.defaultBindingMode; //set the default binding mode
if (!info.command && !info.expression) { // if there is no command or detected expression
info.command = property.hasOptions ? 'options' : null; //and it is an optons property, set the options command
}
// if the attribute itself is bound to a default attribute value then we have to
// associate the attribute value with the name of the default bindable property
// (otherwise it will remain associated with "value")
if (info.command && (info.command !== 'options') && type.primaryProperty) {
const primaryProperty = type.primaryProperty;
attrName = info.attrName = primaryProperty.name;
// note that the defaultBindingMode always overrides the attribute bindingMode which is only used for "single-value" custom attributes
// when using the syntax `<div square.bind="color"></div>`
info.defaultBindingMode = primaryProperty.defaultBindingMode;
}
}
}
} else if (elementInstruction) { //or if this is on a custom element
elementProperty = elementInstruction.type.attributes[info.attrName];
if (elementProperty) { //and this attribute is a custom property
info.defaultBindingMode = elementProperty.defaultBindingMode; //set the default binding mode
}
}
if (elementProperty) {
instruction = bindingLanguage.createAttributeInstruction(resources, node, info, elementInstruction);
} else {
instruction = bindingLanguage.createAttributeInstruction(resources, node, info, undefined, type);
}
if (instruction) { //HAS BINDINGS
if (instruction.alteredAttr) {
type = resources.getAttribute(instruction.attrName);
}
if (instruction.discrete) { //ref binding or listener binding
expressions.push(instruction);
} else { //attribute bindings
if (type) { //templator or attached behavior found
instruction.type = type;
this._configureProperties(instruction, resources);
if (type.liftsContent) { //template controller
instruction.originalAttrName = originalAttrName;
liftingInstruction = instruction;
break;
} else { //attached behavior
behaviorInstructions.push(instruction);
}
} else if (elementProperty) { //custom element attribute
elementInstruction.attributes[info.attrName].targetProperty = elementProperty.name;
} else { //standard attribute binding
expressions.push(instruction.attributes[instruction.attrName]);
}
}
} else { //NO BINDINGS
if (type) { //templator or attached behavior found
instruction = BehaviorInstruction.attribute(attrName, type);
instruction.attributes[resources.mapAttribute(attrName)] = attrValue;
if (type.liftsContent) { //template controller
instruction.originalAttrName = originalAttrName;
liftingInstruction = instruction;
break;
} else { //attached behavior
behaviorInstructions.push(instruction);
}
} else if (elementProperty) { //custom element attribute
elementInstruction.attributes[attrName] = attrValue;
}
//else; normal attribute; do nothing
}
}
if (liftingInstruction) {
liftingInstruction.viewFactory = viewFactory;
node = liftingInstruction.type.compile(this, resources, node, liftingInstruction, parentNode);
auTargetID = makeIntoInstructionTarget(node);
instructions[auTargetID] = TargetInstruction.lifting(parentInjectorId, liftingInstruction);
} else {
if (expressions.length || behaviorInstructions.length) {
injectorId = behaviorInstructions.length ? getNextInjectorId() : false;
for (i = 0, ii = behaviorInstructions.length; i < ii; ++i) {
instruction = behaviorInstructions[i];
console.log(instruction)
instruction.type.compile(this, resources, node, instruction, parentNode);
providers.push(instruction.type.target);
}
for (i = 0, ii = expressions.length; i < ii; ++i) {
expression = expressions[i];
if (expression.attrToRemove !== undefined) {
node.removeAttribute(expression.attrToRemove);
}
}
auTargetID = makeIntoInstructionTarget(node);
instructions[auTargetID] = TargetInstruction.normal(
injectorId,
parentInjectorId,
providers,
behaviorInstructions,
expressions,
elementInstruction
);
}
if (elementInstruction && elementInstruction.skipContentProcessing) {
return node.nextSibling;
}
let currentChild = node.firstChild;
while (currentChild) {
currentChild = this._compileNode(currentChild, resources, instructions, node, injectorId || parentInjectorId, targetLightDOM);
}
}
return node.nextSibling;
}
/**
* ======================================
* Just overriding things to make it work
*
*
*
*/
ViewCompiler.prototype._compileNode = function(node, resources, instructions, parentNode, parentInjectorId, targetLightDOM) {
switch (node.nodeType) {
case 1: //element node
return this._compileElement(node, resources, instructions, parentNode, parentInjectorId, targetLightDOM);
case 3: //text node
//use wholeText to retrieve the textContent of all adjacent text nodes.
let expression = resources.getBindingLanguage(this.bindingLanguage).inspectTextContent(resources, node.wholeText);
if (expression) {
let marker = DOM.createElement('au-marker');
let auTargetID = makeIntoInstructionTarget(marker);
(node.parentNode || parentNode).insertBefore(marker, node);
node.textContent = ' ';
instructions[auTargetID] = TargetInstruction.contentExpression(expression);
//remove adjacent text nodes.
while (node.nextSibling && node.nextSibling.nodeType === 3) {
(node.parentNode || parentNode).removeChild(node.nextSibling);
}
} else {
//skip parsing adjacent text nodes.
while (node.nextSibling && node.nextSibling.nodeType === 3) {
node = node.nextSibling;
}
}
return node.nextSibling;
case 11: //document fragment node
let currentChild = node.firstChild;
while (currentChild) {
currentChild = this._compileNode(currentChild, resources, instructions, node, parentInjectorId, targetLightDOM);
}
break;
default:
break;
}
return node.nextSibling;
}
let nextInjectorId = 0;
function getNextInjectorId() {
return ++nextInjectorId;
}
let lastAUTargetID = 0;
function getNextAUTargetID() {
return (++lastAUTargetID).toString();
}
function makeIntoInstructionTarget(element) {
let value = element.getAttribute('class');
let auTargetID = getNextAUTargetID();
element.setAttribute('class', (value ? value + ' au-target' : 'au-target'));
element.setAttribute('au-target-id', auTargetID);
return auTargetID;
}
function makeShadowSlot(compiler, resources, node, instructions, parentInjectorId) {
let auShadowSlot = DOM.createElement('au-shadow-slot');
DOM.replaceNode(auShadowSlot, node);
let auTargetID = makeIntoInstructionTarget(auShadowSlot);
let instruction = TargetInstruction.shadowSlot(parentInjectorId);
instruction.slotName = node.getAttribute('name') || ShadowDOM.defaultSlotKey;
instruction.slotDestination = node.getAttribute('slot');
if (node.innerHTML.trim()) {
let fragment = DOM.createDocumentFragment();
let child;
while (child = node.firstChild) {
fragment.appendChild(child);
}
instruction.slotFallbackFactory = compiler.compile(fragment, resources);
}
instructions[auTargetID] = instruction;
return auShadowSlot;
}
import {
BehaviorInstruction,
Controller,
HtmlBehaviorResource
} from 'aurelia-framework'
/**
* Creates an instance of this behavior.
* @param container The DI container to create the instance in.
* @param instruction The instruction for this behavior that was constructed during compilation.
* @param element The element on which this behavior exists.
* @param bindings The bindings that are associated with the view in which this behavior exists.
* @return The Controller of this behavior.
*/
HtmlBehaviorResource.prototype.create = function(container: Container, instruction?: BehaviorInstruction, element?: Element, bindings?: Binding[]): Controller {
let viewHost;
let au = null;
// console.log({ t: this, instruction, element, bindings });
instruction = instruction || BehaviorInstruction.normal;
element = element || null;
bindings = bindings || null;
if (this.elementName !== null && element) {
if (this.usesShadowDOM) {
viewHost = element.attachShadow(this.shadowDOMOptions);
container.registerInstance(DOM.boundary, viewHost);
} else {
viewHost = element;
if (this.targetShadowDOM) {
container.registerInstance(DOM.boundary, viewHost);
}
}
}
if (element !== null) {
element.au = au = element.au || {};
}
let viewModel = instruction.viewModel || container.get(this.target);
let controller = new Controller(this, instruction, viewModel, container);
let childBindings = this.childBindings;
let viewFactory;
if (this.liftsContent) {
//template controller
au.controller = controller;
} else if (this.elementName !== null) {
//custom element
viewFactory = instruction.viewFactory || this.viewFactory;
container.viewModel = viewModel;
if (viewFactory) {
controller.view = viewFactory.create(container, instruction, element);
}
if (element !== null) {
au.controller = controller;
if (controller.view) {
if (!this.usesShadowDOM && (element.childNodes.length === 1 || element.contentElement)) { //containerless passes content view special contentElement property
let contentElement = element.childNodes[0] || element.contentElement;
controller.view.contentView = { fragment: contentElement }; //store the content before appending the view
contentElement.parentNode && DOM.removeNode(contentElement); //containerless content element has no parent
}
if (instruction.anchorIsContainer) {
if (childBindings !== null) {
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
controller.view.addBinding(childBindings[i].create(element, viewModel, controller));
}
}
controller.view.appendNodesTo(viewHost);
} else {
controller.view.insertNodesBefore(viewHost);
}
} else if (childBindings !== null) {
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
bindings.push(childBindings[i].create(element, viewModel, controller));
}
}
} else if (controller.view) {
//dynamic element with view
controller.view.controller = controller;
if (childBindings !== null) {
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
controller.view.addBinding(childBindings[i].create(instruction.host, viewModel, controller));
}
}
} else if (childBindings !== null) {
//dynamic element without view
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
bindings.push(childBindings[i].create(instruction.host, viewModel, controller));
}
}
} else if (childBindings !== null) {
//custom attribute
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
bindings.push(childBindings[i].create(element, viewModel, controller));
}
}
if (au !== null) {
au[this.htmlName] = controller;
}
if (instruction.initiatedByBehavior && viewFactory) {
controller.view.created();
}
return controller;
}
import {ViewEngine} from 'aurelia-templating';
import {_createDynamicElement} from './dynamic-element';
/*eslint padded-blocks:0*/
import {useView, customElement, bindable} from 'aurelia-templating';
export function getElementName(address) {
return /([^\/^\?]+)\.html?/i.exec(address)[1].toLowerCase();
}
export function configure(config) {
let viewEngine = config.container.get(ViewEngine);
let loader = config.aurelia.loader;
viewEngine.addResourcePlugin('.htm', {
'fetch': function(address) {
return loader.loadTemplate(address).then(registryEntry => {
let bindable = registryEntry.template.getAttribute('bindable');
let elementName = getElementName(address);
if (bindable) {
bindable = bindable.split(',').map(x => x.trim());
registryEntry.template.removeAttribute('bindable');
} else {
bindable = [];
}
return { [elementName]: _createDynamicElement(elementName, address, bindable) };
});
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment