Polymer has nice 2-way bindings:
<template>
<input value={{property}}>
<span>Result: [[property]]</span>
</template>
Change the value and property gets updated too. Combined with notify this becomes a really elegant way of updating complex objects with layered components.
In lit this becomes:
html`
<input value="${this.property}" on-input="${e => this.property = e.target.value}">
<span>Result: ${this.property}</span>`
Which is a little clunky - not a problem as such, but large forms are going to be a lot uglier for simple 2-way bindings.
Is there a better way? Some kind of directive?
Two-way bindings are not very easy for lit-html to implement because the expressions are completely opaque to the library. We don't know that value="${this.property}" only that value is bound to _something_. Essentially lit-html sees: value="${???}". So there's no easy way to know what property to set.
Not only that but lit-html is firmly in the functional / unidirectional dataflow camp of template systems. Even if it were possible, implementing side-effects of user interactions are basically out of scope.
If may be possible to make a directive that makes certain expression reflectable to get what you want. It'd look something like:
html`
<input value="${bind(this, 'property', 'change')}">
<span>Result: ${this.property}</span>`
Where bind evaluates the expression in the second argument, sets the part value, and adds an event listener with the third value.
I think it's still out of scope for lit-html, but welcome as a third-party directive.
why not consider input value kind of dependency attribute, if is bound to a object member, just subscribe to the change event and set the value to that member.
Set what value though?
the input value has a change event, it can be used to update the source if an event in the target is fired.
this can be used on other attributes, if they can notify changes it will be the friendly to-way databind that I know. from source to target and from target to source.
the dependency property or attribute needs to have a changed event (can be fired initially by other events), so if is fired the property on the source will be setted with the value of that attribute.
It will be needed to somehow configure if the implicit register on that event will do done or not. per example
and try to set source.someproperty = target.value
Remember expressions are JavaScript, evaluated by the VM, not lit-html. lit-html never receives the source of the expressions, so it can't know to try to set someproperty, it just gets a value. This is why my bind idea above took a property name.
the source of expression will be needed only after the changed event. on the template preparation the binding is not a direct association between target and source, but an object, that can hold a relactionship with a htlmelement and a source object. the source of that binding will know in the render
@nmocruz lit-html doesn't have access to the source of the expression at any time.
//Very little helper function
const bind = (property) => {
return (e) => {
this[property] = e.target.value
}
}
class MyElem extends LitElement {
static get properties() {
return {
property: String
}
}
render() {
return html`
<input value="${this.property}" @input="${bind('property')}">
<span>Result: ${this.property}</span>
`;
}
}
class MyElem extends LitElement {
static get properties() {
return {
property: String
}
}
render() {
return html`
<input value="${this.property2.val}" @input="${bind('property2.val')}">
<span>Result: ${this.property2.val}</span>
`;
}
// Accept `path` props like a myObj.val.val2
bind(property) {
return (e) => {
try {
var schema = this; // a moving reference to internal objects within obj
var pList = property.split('.');
var len = pList.length;
for (var i = 0; i < len - 1; i++) {
var elem = pList[i];
if (!schema[elem]) schema[elem] = {}
schema = schema[elem];
}
if (schema)
schema[pList[len - 1]] = e.target.value;
} catch (e) {
console.error("Error in 2-way DataBinding", e);
}
}
}
}
You lose code discoverability and prevent the IDE from helping you out when you mis-spell though when you bind to properties using strings. Our current solution is using rxjs observables.
Looks like this:
<input value=${bind(this.observable)} @input=${bind(this.observable)}></input>
In our case, we extended the parts to avoid having to write bind() every time, so it's even shorter!
Drawbacks of this approach:
private x = new BehaviorSubject(default); instead of private x = default;)Totally worth it though. You gain a lot of flexibility and readability. And most importantly: you can pipe the observables, so you basically get double data bindings with the old polymer computed() which we loved but with much less magic involved. For me, rxjs + lit-html is a heavenly combination which solves a lot of issues.
In this case, the helper function seems to be called when the component is mounted too :(
@yorrd Slightly out of scope here, but I'm curious about this statement:
In our case, we extended the parts to avoid having to write bind() every time, so it's even shorter!
Does this mean that you have forked lit-html, or is there some sensible way of making the vanilla library use custom part implementations?
@Legioth there is, you can use your own implementation of html which then uses its own template processor which then spawns its own parts. Everything can extend the lit-html way of doing things. Snippet from our stuff:
export class AdornisTemplateProcessor implements TemplateProcessor {
/**
* Create parts for an attribute-position binding, given the event, attribute
* name, and string literals.
*
* @param element The element containing the binding
* @param name The attribute name
* @param strings The string literals. There are always at least two strings,
* event for fully-controlled bindings with a single expression.
*/
public handleAttributeExpressions(element: Element, name: string, strings: string[], options: RenderOptions): Part[] {
const prefix = name[0];
if (prefix === '.') {
const propCommitter = new OptionallyBindingPropertyCommitter(element, name.slice(1), strings);
return propCommitter.parts;
}
if (prefix === '@') {
if (name[1] === '~') return [new SubjectEventPart(element, name.slice(2), options.eventContext)];
return [new EventPart(element, name.slice(1), options.eventContext)];
}
if (prefix === '?') {
return [new OptionallyBindingBooleanAttributePart(element, name.slice(1), strings)];
}
const comitter = new OptionallyBindingAttributeComitter(element, name, strings);
return comitter.parts;
}
/**
* Create parts for a text-position binding.
* @param templateFactory
*/
public handleTextExpression(options: RenderOptions) {
return new OptionallyBindingNodePart(options);
}
}
export const html = (strings: TemplateStringsArray, ...values: any[]) =>
new TemplateResult(strings, values, 'html', new AdornisTemplateProcessor());
if you're interested, I can provide you with our complete library, but that's very biased and not decoupled atm. Because of potential leaks, you need to unsubscribe, which we handle in the custom parts, but the unsubscription needs to be triggered at the level where render() is called (which is at the web component render cycle level for us)
Most helpful comment
@Legioth there is, you can use your own implementation of
htmlwhich then uses its own template processor which then spawns its own parts. Everything can extend thelit-htmlway of doing things. Snippet from our stuff:if you're interested, I can provide you with our complete library, but that's very biased and not decoupled atm. Because of potential leaks, you need to unsubscribe, which we handle in the custom parts, but the unsubscription needs to be triggered at the level where
render()is called (which is at the web component render cycle level for us)