Permalink
Browse files

fix(<let/>): enable backward compat, fix doc

  • Loading branch information...
bigopon authored and EisenbergEffect committed Sep 28, 2018
1 parent 44c8b87 commit 90684ed8a6968a1514bd4b35cb465a45ab199d88
Showing with 89 additions and 34 deletions.
  1. +19 −4 doc/article/en-US/templating-custom-elements.md
  2. +1 −1 src/binding-language.js
  3. +15 −10 src/view-compiler.js
  4. +54 −19 test/view-compiler.spec.js
@@ -222,7 +222,7 @@ In this example, the `secret-message` custom element will check every ten second

Whether a secret message that is only shown to the person who writes the message is very useful is for you to decide.

## [Declarative computed value]
## Declarative computed value

As your application grows, custom elements get complicated and often values that are computed based on other values start to appear. It can be done either via getter in your custom element view model, or via aurelia `let` element. Think about it like a declaration in a JavaScript expression. For instance, a name tag form example would consist of two input fields with value bound to view model properties `firstName` and `lastName`, like the following example:

@@ -329,15 +329,30 @@ Or an expression:
</source-code>
</code-listing>

And now after either `firstName` or `lastName` has changed, `fullName` is recomputed automatically and is ready to be used in other part of the view model. Now instead of reacting to changes on both `firstName` and `lastName`, we only need to care about `fullName` like the following example
And now after either `firstName` or `lastName` has changed, `fullName` is recomputed automatically and is ready to be used in other part of the view model.

Additonally, if there is needs to react to changes on both `fullName`, we can specify a special attribute `to-binding-context` on `<let/>` element to notify bindings to assign the value to binding context, which is your view model, instead of override context, which is your view.

<code-listing heading="declarative-computed${context.language.fileExtension}">
<source-code lang="HTML">
<let to-binding-context full-name="${firstName} ${lastName}">
<div>
First name:
<input value.bind="firstName" />
Last name:
<input value.bind="lastName" />
</div>
Full name is: "${fullName}"
</source-code>
</code-listing>

<code-listing heading="declarative-computed${context.language.fileExtension}">
<source-code lang="ES 2016">
export class App {
@bindable firstName
@bindable lastName

@bindable fullName
@observable fullName

// Aurelia convention, called after fullName has changed
fullNameNameChanged(fullName) {
@@ -350,7 +365,7 @@ And now after either `firstName` or `lastName` has changed, `fullName` is recomp
@bindable firstName: string
@bindable lastName: string

@bindable fullName: string
@observable fullName: string

// Aurelia convention, called after fullName has changed
fullNameNameChanged(fullName: string) {
@@ -62,7 +62,7 @@ export class BindingLanguage {
* @param existingExpressions the array that will hold compiled let expressions from the let element
* @return the expression array created from the <let/> element
*/
createLetExpressions(resources: ViewResources, element: Element, existingExpressions?: LetExpression[]): LetExpession[] {
createLetExpressions(resources: ViewResources, element: Element): LetExpession[] {
mi('createLetExpressions');
}

@@ -52,6 +52,8 @@ function makeShadowSlot(compiler, resources, node, instructions, parentInjectorI
return auShadowSlot;
}

const defaultLetHandler = BindingLanguage.prototype.createLetExpressions;

/**
* Compiles html templates, dom fragments and strings into ViewFactory instances, capable of instantiating Views.
*/
@@ -306,16 +308,6 @@ export class ViewCompiler {
node = makeShadowSlot(this, resources, node, instructions, parentInjectorId);
}
return node.nextSibling;
} else if (tagName === 'let') {
auTargetID = makeIntoInstructionTarget(node);
instructions[auTargetID] = TargetInstruction.letElement(
bindingLanguage.createLetExpressions(
resources,
node,
instructions.letExpressions
)
);
return node.nextSibling;
} else if (tagName === 'template') {
if (!('content' in node)) {
throw new Error('You cannot place a template element within ' + node.namespaceURI + ' namespace');
@@ -324,6 +316,19 @@ export class ViewCompiler {
viewFactory.part = node.getAttribute('part');
} else {
type = resources.getElement(node.getAttribute('as-element') || tagName);
// Only attempt to process a <let/> when it's not a custom element,
// and the binding language has an implementation for it
// This is an backward compat move
if (tagName === 'let' && !type && bindingLanguage.createLetExpressions !== defaultLetHandler) {
auTargetID = makeIntoInstructionTarget(node);
instructions[auTargetID] = TargetInstruction.letElement(
bindingLanguage.createLetExpressions(
resources,
node
)
);
return node.nextSibling;
}
if (type) {
elementInstruction = BehaviorInstruction.element(node, type);
type.processAttributes(this, resources, node, attributes, elementInstruction);
@@ -1,9 +1,12 @@
import './setup';
import {ViewCompiler} from '../src/view-compiler';
import {ViewResources} from '../src/view-resources';
import { HtmlBehaviorResource } from '../src/html-behavior';
import { BindingLanguage } from '../src/binding-language';

class MockBindingLanguage {
inspectAttribute(resources, elementName, attrName, attrValue) {
return { attrName, attrValue };
}

createAttributeInstruction(resources, element, info, existingInstruction) {
@@ -135,27 +138,59 @@ describe('ViewCompiler', () => {
expect(node.className).toBe('foo bar baz au-target');
});

it('compiles let element by extracting bindings and remove the element', () => {
let instructions = { };

let letElement = document.createElement('let');
let parentNode = document.createElement('div');

parentNode.appendChild(letElement);
letElement.setAttribute('foo', 'bar');

viewCompiler._compileNode(letElement, resources, instructions, parentNode, 'root', true);
expect(Object.keys(instructions).length).toBe(1, 'It should have had 1 instruction');
let instruction;
// id in view compiler is universal across instances, cannot reset
for (var id in instructions) {
instruction = instructions[id];
}
expect(instruction).toBeDefined('First instruction should have been defined');
expect(instruction.letElement).toBe(true, 'Type of instruction should have been letElement');
expect(instruction.expressions.length).toBe(1, 'Should have had 1 expression');
describe('<let/>', () => {

it('treats like normal element when there is no binding language', () => {
const fragment = createFragment('<div><let>');
let instructions = { };

spyOn(viewCompiler.bindingLanguage, 'createLetExpressions').and.callThrough();
viewCompiler._compileNode(fragment, resources, instructions, null, 'root', true);
expect(Object.keys(instructions).length).toBe(1, 'It should have had 1 instruction');
expect(viewCompiler.bindingLanguage.createLetExpressions).toHaveBeenCalled();
});

describe('backward compat', () => {
it('does nothing if there is custom <let/> element', () => {
let instructions = { };
const fragment = createFragment('<div><let foo="bar">');
const letMeta = new HtmlBehaviorResource();

resources.getElement = name => name === 'let' ? letMeta : null;

viewCompiler._compileNode(fragment, resources, instructions, null, 'root', true);
expect(Object.keys(instructions).length).toBe(1, 'It should have had 1 instruction with let ce');
let instruction;
for (let id in instructions) {
instruction = instructions[id];
break;
}
expect(instruction.letElement).toBe(false, 'It should have not been let Element instruction');
expect(instruction.behaviorInstructions[0].type).toBe(letMeta, 'It should have been the letMeta instance');
});

it('does nothing if there is no binding language implementation for <let/>', () => {
let instructions = { };
const fragment = createFragment('<div><let>');

resources.getBindingLanguage = () => Object.assign(
viewCompiler.bindingLanguage,
{
createLetExpressions: BindingLanguage.prototype.createLetExpressions
});

viewCompiler._compileNode(fragment, resources, instructions, null, 'root', true);
expect(Object.keys(instructions).length).toBe(0, 'It should have had no instruction');
});
});
});

});

function createFragment(html) {
const parser = document.createElement('div');
parser.innerHTML = `<template>${html}</template>`;
return parser.firstElementChild.content;
}

});

0 comments on commit 90684ed

Please sign in to comment.