Skip to content

Instantly share code, notes, and snippets.

@mbroadst
Last active December 18, 2016 00:12
Show Gist options
  • Save mbroadst/44d1dfb9390e28e2f5cec39b0bd0762a to your computer and use it in GitHub Desktop.
Save mbroadst/44d1dfb9390e28e2f5cec39b0bd0762a to your computer and use it in GitHub Desktop.
Aurelia Templating Child/Children Bug
<template>
<require from="grid"></require>
<require from="column"></require>
<au-grid view-model.ref="grid" rows.bind="people" class="table table-condensed table-bordered">
<au-column header="first name">
<compose view="first-name-view.html"></compose>
</au-column>
<au-column header="last name">
<compose view-model="last-name" model.bind="row"></compose>
</au-column>
<au-column header="value">${row.value}</au-column>
</au-grid>
</template>
export class App {
people = [
{ firstName: 'Rob', lastName: 'Eisenberg', value: 0 },
{ firstName: 'Jeremy', lastName: 'Danyow', value: 0 },
{ firstName: 'Matt', lastName: 'Broadstone', value: 0 }
];
attached() {
// const source = [
// { firstName: 'Richard', lastName: 'Pryor', value: 0 },
// { firstName: 'Mitch', lastName: 'Hedberg', value: 0 },
// { firstName: 'Bobcat', lastName: 'Goldthwait', value: 0 }
// ];
// setInterval(() => {
// let idx = Math.floor(Math.random() * this.people.length),
// srcIdx = Math.floor(Math.random() * source.length);
// let person = source[srcIdx];
// person.value = Math.floor(Math.random() * 1000);
// this.people.splice(idx, 1, source[srcIdx]);
// }, 50);
}
}
import {inject} from 'aurelia-dependency-injection';
import {
bindable,
customElement,
noView,
processContent,
ViewCompiler
} from 'aurelia-templating';
@noView
@processContent(false)
@customElement('au-column')
@inject(Element, ViewCompiler)
export class Column {
@bindable header
constructor(element, viewCompiler) {
let template = `<template>${element.innerHTML}</template>`;
this.viewFactory = viewCompiler.compile(template);
element.innerHTML = '';
}
}
<template>
<b>${row.firstName}</b>
</template>
import {
createOverrideContext,
BindingBehavior,
ValueConverter,
sourceContext,
bindingMode
} from 'aurelia-binding';
const oneTime = bindingMode.oneTime;
/**
* Update the override context.
* @param startIndex index in collection where to start updating.
*/
export function updateOverrideContexts(views, startIndex) {
let length = views.length;
if (startIndex > 0) {
startIndex = startIndex - 1;
}
for (; startIndex < length; ++startIndex) {
updateOverrideContext(views[startIndex].overrideContext, startIndex, length);
}
}
/**
* Creates a complete override context.
* @param data The item's value.
* @param index The item's index.
* @param length The collections total length.
* @param key The key in a key/value pair.
*/
export function createFullOverrideContext(repeat, data, index, length, key) {
let bindingContext = {};
let overrideContext = createOverrideContext(bindingContext, repeat.scope.overrideContext);
// is key/value pair (Map)
if (typeof key !== 'undefined') {
bindingContext[repeat.key] = key;
bindingContext[repeat.value] = data;
} else {
bindingContext[repeat.local] = data;
}
updateOverrideContext(overrideContext, index, length);
return overrideContext;
}
/**
* Updates the override context.
* @param context The context to be updated.
* @param index The context's index.
* @param length The collection's length.
*/
export function updateOverrideContext(overrideContext, index, length) {
let first = (index === 0);
let last = (index === length - 1);
let even = index % 2 === 0;
overrideContext.$index = index;
overrideContext.$first = first;
overrideContext.$last = last;
overrideContext.$middle = !(first || last);
overrideContext.$odd = !even;
overrideContext.$even = even;
}
/**
* Gets a repeat instruction's source expression.
*/
export function getItemsSourceExpression(instruction, attrName) {
return instruction.behaviorInstructions
.filter(bi => bi.originalAttrName === attrName)[0]
.attributes
.items
.sourceExpression;
}
/**
* Unwraps an expression to expose the inner, pre-converted / behavior-free expression.
*/
export function unwrapExpression(expression) {
let unwrapped = false;
while (expression instanceof BindingBehavior) {
expression = expression.expression;
}
while (expression instanceof ValueConverter) {
expression = expression.expression;
unwrapped = true;
}
return unwrapped ? expression : null;
}
/**
* Returns whether an expression has the OneTimeBindingBehavior applied.
*/
export function isOneTime(expression) {
while (expression instanceof BindingBehavior) {
if (expression.name === 'oneTime') {
return true;
}
expression = expression.expression;
}
return false;
}
/**
* Forces a binding instance to reevaluate.
*/
export function updateOneTimeBinding(binding) {
if (binding.call && binding.mode === oneTime) {
binding.call(sourceContext);
} else if (binding.updateOneTimeBindings) {
binding.updateOneTimeBindings();
}
}
<template>
<slot></slot>
<table ref="table" class="${class}">
<thead>
<th repeat.for="column of columns" class="ui single line">
${ column.header }
</th>
</thead>
<tbody ref="tbody"></tbody>
</table>
</template>
import {inject, Container} from 'aurelia-dependency-injection';
import {
bindable,
children,
customElement,
ViewCompiler,
ViewSlot
} from 'aurelia-templating';
import {ObserverLocator} from 'aurelia-binding';
import {AbstractRepeater, RepeatStrategyLocator} from 'aurelia-templating-resources';
import {updateOneTimeBinding} from './grid-utilities';
@customElement('au-grid')
@inject(Container, ViewSlot, ViewCompiler, ObserverLocator, RepeatStrategyLocator)
export class Grid extends AbstractRepeater {
@children('au-column') columns = [];
@bindable rows;
@bindable class;
columnViewFactories = [];
constructor(container, viewSlot, viewCompiler, observerLocator, strategyLocator) {
super({
local: 'row',
viewsRequireLifecycle: false
});
this.container = container;
this.viewSlot = viewSlot;
this.observerLocator = observerLocator;
this.strategyLocator = strategyLocator;
this.scope = null;
this.strategy = null;
this.rowViewFactory =
viewCompiler.compile(`<template><slot></slot></template`);
this.rowViewSlots = [];
}
// life-cycle business
attached() {
this.scrapeColumnViewFactories();
}
bind(bindingContext, overrideContext) {
this.scope = { bindingContext, overrideContext };
this.rowsChanged();
}
unbind() {
this.scope = null;
this.rows = null;
this.viewSlot.removeAll(true);
this._stopObservation();
}
call(context, changes) {
this[context](this.rows, changes);
}
// view related
scrapeColumnViewFactories() {
console.log('columns: ', this.columns);
console.log('column count: ', this.columns.length);
for (let i = 0, ii = this.columns.length; i < ii; ++i) {
this.columnViewFactories.push(this.columns[i].viewFactory);
}
}
rowsChanged() {
this._stopObservation();
if (!this.scope) return;
let rows = this.rows;
this.strategy = this.strategyLocator.getStrategy(rows);
this._observeCollection();
this.strategy.instanceChanged(this, rows);
}
// collection observation
_stopObservation() {
if (this.collectionObserver) {
this.collectionObserver.unsubscribe(this.callContext, this);
this.collectionObserver = null;
this.callContext = null;
}
}
_observeCollection() {
let rows = this.rows;
this.collectionObserver =
this.strategy.getCollectionObserver(this.observerLocator, rows);
if (this.collectionObserver) {
this.callContext = 'handleCollectionMutated';
this.collectionObserver.subscribe(this.callContext, this);
}
}
handleCollectionMutated(collection, changes) {
this.strategy.instanceMutated(this, collection, changes);
}
// @override AbstractRepeater
views() { return this.rowViewSlots; }
view(index) { return this.rowViewSlots[index]; }
viewCount() { return this.rowViewSlots.length; }
addView(bindingContext, overrideContext) {
// console.log('addView(bctx= ', bindingContext, ')');
let rowElement = document.createElement('tr');
this.tbody.appendChild(rowElement);
let rowView = this.rowViewFactory.create(this.container);
this.viewSlot.add(rowView);
let rowViewSlot = new ViewSlot(rowElement, true);
for (let x = 0, xx = this.columnViewFactories.length; x < xx; x++) {
let cellViewFactory = this.columnViewFactories[x];
let cellView = cellViewFactory.create(this.container);
rowViewSlot.add(cellView);
let cellElement = document.createElement('td');
rowElement.appendChild(cellElement);
// move the view to the `td` element
cellView.removeNodes();
cellView.appendNodesTo(cellElement);
}
rowViewSlot.bind(bindingContext, overrideContext);
this.rowViewSlots.push(rowViewSlot);
}
insertView(index, bindingContext, overrideContext) {
// console.log('insertView(index=', index, ', bctx= ', bindingContext, ')');
let rowElement = document.createElement('tr');
let existingElement =
(!!this.rowViewSlots[index] && !!this.rowViewSlots[index].anchor) ?
this.rowViewSlots[index].anchor : null;
this.tbody.insertBefore(rowElement, existingElement);
let rowView = this.rowViewFactory.create(this.container);
this.viewSlot.insert(index, rowView);
let rowViewSlot = new ViewSlot(rowElement, true);
for (let x = 0, xx = this.columnViewFactories.length; x < xx; x++) {
let cellViewFactory = this.columnViewFactories[x];
let cellView = cellViewFactory.create(this.container);
rowViewSlot.add(cellView);
let cellElement = document.createElement('td');
rowElement.appendChild(cellElement);
// move the view to the `td` element
cellView.removeNodes();
cellView.appendNodesTo(cellElement);
}
rowViewSlot.bind(bindingContext, overrideContext);
this.rowViewSlots.splice(index, 0, rowViewSlot);
}
removeAllViews() {
// console.log('removeAllViews()');
let length = this.rowViewSlots.length;
while (length--) {
let rowViewSlot = this.rowViewSlots.pop();
let anchor = rowViewSlot.anchor;
let parentNode = anchor.parentNode;
rowViewSlot.removeAll(true);
parentNode.removeChild(anchor);
}
this.rowViewSlots = [];
this.viewSlot.removeAll(true);
}
removeView(index) {
// console.log('removeView(index=', index, ')');
let rowViewSlots = this.rowViewSlots;
let anchor = rowViewSlots[index].anchor;
let parentNode = anchor.parentNode;
rowViewSlots[index].removeAll(true);
parentNode.removeChild(anchor);
rowViewSlots.splice(index, 1);
this.viewSlot.removeAt(index, true);
}
updateBindings(view) {
// console.log('updateBindings(view=', view, ')');
let i = view.children.length;
while (i--) {
let j = view.children[i].bindings.length;
while (j--) updateOneTimeBinding(view.children[i].bindings[j]);
j = view.children[i].controllers.length;
while (j--) {
let k = view.children[i].controllers[j].boundProperties.length;
while (k--) {
let binding =
view.children[i].controllers[j].boundProperties[k].binding;
updateOneTimeBinding(binding);
}
}
}
}
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
</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>
<template>
<i>${row.lastName}</i>
</template>
export class LastName {
activate(model) { this.row = model; }
}
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
aurelia.start().then(() => aurelia.setRoot());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment