Skip to content

Commit

Permalink
fix(<let/>): enable backward compat, fix doc
Browse files Browse the repository at this point in the history
  • Loading branch information
bigopon authored and EisenbergEffect committed Sep 29, 2018
1 parent 44c8b87 commit 90684ed
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 34 deletions.
23 changes: 19 additions & 4 deletions doc/article/en-US/templating-custom-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/binding-language.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
25 changes: 15 additions & 10 deletions src/view-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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');
Expand All @@ -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);
Expand Down
73 changes: 54 additions & 19 deletions test/view-compiler.spec.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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.