I thought I was trying to do something very simple but I just can't make this work. This entire example is on plunkr
I have a very basic custom element that present a #bindable data member that it displays and monitors with a changed event. It look like this:
import {bindable} from "aurelia-framework";
export class ChildElementCustomElement {
#bindable childData;
childDataChanged(value) {
alert("Child Data changed " + value);
}
}
and the view:
<template>
<div style="border: solid 1pt green;">
<h2>This is the child</h2>
This is the child data : ${childData}
</div>
</template>
The parent shows the child element but I want a member in its view model that's bound to the child so any change in the parent member is automatically reflected in the child. Here's the parent code:
import {bindable} from "aurelia-framework";
export class App {
parentData = "this is parent data";
}
and the view:
<template>
<h1>Two-way binding between parent and child custom elements</h1>
<require from="./child-element"></require>
<child-element childData.bind="parentData"></child-element>
<hr/>
<label>The following is the parent data:</label>
<input type="text" value.bind="parentData"></input>
</template>
What I'd like to see is any updates typed in the input field will automatically appear in the child (plus the changed event fires) but the child doesn't appear bound at all! I've also tried swapping bind for two-way just in case the convention has bound one-way but that still hasn't worked.
Please highlight my stupidity :) because currently I'm stuck thinking this should just work.
The default convention for #bindable is to turn the camel-cased property names to attribute names using the naming convention 'myProperty' -> 'my-property' (dash-casing).
From the documentation:
#bindable({
name:'myProperty', //name of the property on the class
attribute:'my-property', //name of the attribute in HTML
changeHandler:'myPropertyChanged', //name of the method to invoke when the property changes
defaultBindingMode: bindingMode.oneWay, //default binding mode used with the .bind command
defaultValue: undefined //default value of the property, if not bound or set in HTML
})
The defaults and conventions are shown above. So, you would only need
to specify these options if you need to deviate.
So you need to write child-data.bind in your HTML
<child-element child-data.bind="parentData"></child-element>
Plunkr
Related
Is there anyway I can get a reference to all the viewmodels of the child components in my lanes property on the BoardComponent viewmodel? I need to have a reference of al <boardlane></boardlane> in the viewmodel of the <board></board> component. Is this possible?
App.ts
export class App {
public groups: any[] = [
{
title: 'first group'
},
{
title: 'second group'
}
]
}
<template>
<board>
<boardlane repeat.for="group of groups" title.bind="group.title"></boardlane>
</board>
</template>
Board Component
#customElement('board')
export class BoardComponent {
public lanes: BoardLaneComponent[];
}
<template>
<div class="board">
<slot></slot>
</div>
</template>
BoardLane Component
#customElement('boardlane')
export class BoardLaneComponent { }
<template>
<div class="boardlane">
I am a board lane
</div>
</template>
You can try the #children decorator:
Board Component
import {children} from 'aurelia-framework';
#customElement('board')
#children({ name: 'lanes', selector: 'boardlane' })
export class BoardComponent {
public lanes: BoardLaneComponent[];
lanesChanged() {
// Handle any mutations to the array here
}
}
<template>
<div class="board">
<slot></slot>
</div>
</template>
In theory this should maintain a list of child VMs on BoardComponent by using the selector to gather the elements within the view.
If the elements are non-aurelia backed elements they will be represented by Element instances in the specified array, otherwise they will be a reference to the actual Aurelia VM backing the element.
Also, it will by default create a function called <x>Changed where <x> is the name of the backing array. You can use this to be notified of any mutations happening to the tracked elements.
The only issue may be nesting - I believe the original implementation deep-selected into descendants but that was removed later. I'm not sure if it was re-introduced but the details are here:
https://github.com/aurelia/templating/issues/451
Assuming you don't need to go to grandchildren this should work.
Disclaimer: not done any Aurelia dev for a little while :(
Note: I don't think the docs clearly list the API for children and the selectorOrConfig parameter it takes
In the source it looks like this:
constructor(config) {
this.name = config.name;
this.changeHandler = config.changeHandler || this.name + 'Changed';
this.selector = config.selector;
this.all = config.all;
}
So it looks like the object can have those properties - not sure what all does though but interesting that you can change the name of the change handler that's fired when the array contents mutate.
I have ref for form which is a custom element
<form ref="domRef" ...>
I have ref for field too, which is another custom element(being used inside the form)
<input type="text" ref="domRef" .....>
but inside attach() of form's view model I am getting this.domRef is input's reference.
attached(){
console.log(this.domRef);
}
So, as the execution goes on domRef is being overridden by the latest one. Why?
Why domRef's are not different for different scopes?
I cannot use different name for ref as all are being generated dynamically.
Please help me on this if there is any alternative.
Update
After Ashley's Answer:
Custom Element Form has its own VM and Custom Element Field has its own VM too.
Views:
<template>
<form ref="domRef">
<compose view-model="resources/elements/field" ..... containerLess>
</compose>
</form>
</template>
<template>
<input type="text" ref="domRef"></input>
</template>
View-Models:
export class Form{
..
attached(){
console.log(this.domRef); //returns Input's Ref Which is not correct
}
}
export class Field{
..
attached(){
console.log(this.domRef); //returns Input's Ref Which is correct
}
}
Then if domRef belongs to the current VM why is it happening?
The scope is your VM, not any HTML element, so this.domRef is gonna be set to the last element that Aurelia set that property to.
If the name is being generated dynamically, couldn't you just change the name generation code?
After digging out everything, I got the solution. i.e initializing the domRef at the time of constructing.
export class Form{
constructor(){
this.domRef = null;
}
attached(){
console.log(this.domRef); //returns Form's Ref Which is correct
}
}
export class Field{
constructor(){
this.domRef = null;
}
attached(){
console.log(this.domRef); //returns Input's Ref Which is correct
}
}
Strange but Worked.
A few days ago I asked this question 2 way databinding in Aurelia custom elements - bind custom element to parent viewmodel
Now I need to be able to reuse the allSelectableValues from my custom element (my-custom.js) in my parent element (create.js).
I need this for a custom value converter I have on create.js which contains some Ids which I need to display names for instead, by looping through the array of elements, currently fetched and residing in my custom element.
**create.html**
<td>${d.SomeID | allSelectableValuesMapping}</td>
and
**value-converters/all-selectable-values-mapping.js**
export class AllSelectableValuesMappingValueConverter {
toView(value) {
for(let item in allSelectableValues) {
if (item.SomeID == value){
return item.Name;
}
}
}
}
In the ideal world I'd have hoped something like this would have worked:
**my-custom.js**
async attached() {
this.allSelectableValues= await await this.myService.getAllValues();
this.parent.allSelectableValues = this.allSelectableValues;
}
But my custom element have no idea of the parent which is requiring it.
Does anyone have an idea how to set the parent's allSelectableValues equal to the custom element's allSelectableValues from within the custom element? Or is there another, better way of achieving it, while still maintaining the two-way databound custom element?
Something like this ?
Please take extra note of the #customElement('CustomElement') declarator above the export class CustomElement line of code.
Custom Element View Model
import {inject} from 'aurelia-framework';
import {customElement} from 'aurelia-framework';
import {bindable} from 'aurelia-framework';
#customElement('CustomElement')
export class CustomElement {
#bindable arrItems
}
Custom Element HTML
<template>
<div repeat.for="item of arrItems">$(item.someProperty}</div>
</template>
Parent View Model
export class ParentViewModel {
parentArrItems = [];
}
Parent HTML
<template>
<require from="customelement"></require>
<CustomElement arrItems.bind="parentArrItems"></CustomElement>
</template>
In my application I have made a lot of "services" which I can inject in my viewmodels to save som redundancy and time.
Now I'm looking to take it 1 step further, and make those form elements (select, text, checkboxes - a select dropdown for starters) and turn them into custom elements, injecting the service in only the custom element.
I can get it working to some extent. The custom element (select in this case) is showing when I require it in the "parent" view, however when I change the selected value of the custom select element, it does not bind to the "parent" viewmodel, which is my requirement.
I want to be able to bind my selected value from the custom element to a property on the "parent" viewmodel via the bind attribute in it's template.
I'll update which a little code snippet in a few minutes.
create.js (what I refer to as parent viewmodel)
import {bindable} from 'aurelia-framework';
export class Create{
heading = 'Create';
#bindable myCustomElementValue = 'initial value';
}
create.html (parent view)
<template>
<require from="shared/my-custom-element"></require>
<my-custom selectedValue.bind="myCustomElementValue"></my-custom>
<p>The output of ${myCustomElementValue} should ideally be shown and changed here, as the select dropdown changes</p>
</template>
my-custom.js
import { inject, bindable} from 'aurelia-framework';
import MyService from 'my-service';
#inject(MyService )
export class MyCustomCustomElement {
#bindable selectedValue;
constructor(myService ) {
this.myService = myService ;
}
selectedValueChanged(value) {
alert(value);
this.selectedValue = value;
}
async attached() {
this.allSelectableValues = await this.myService.getAllValues();
}
}
What happens is initially the create.html view outputs "initial value", and as I change the value of the custom element select, the newly selected value gets alerted out, but it does not update the bound parent variable, which is still just displaying "initial value".
There are a couple of issues here:
You need to kebab-case any property names in the DOM due to case-insensitivity
selected-value.bind="property"
not
selectedValue.bind="property"
You need to bind two-way. When creating a #bindable using the decorator, one of the arguments is BindingMode which you use to set the default binding mode.
Aurelia sets some sensible defaults, e.g. the default for input value.bind=".." is two way without being explicit
If you don't want to set a default binding mode, you can just be explicit with your binding:
selected-value.two-way="prop"
Hope this helps :)
Edit: I think the API changed a little after this answer.
The #bindable decorator has the following sig:
bindable({
name:'myProperty',
attribute:'my-property',
changeHandler:'myPropertyChanged',
defaultBindingMode: bindingMode.oneWay,
defaultValue: undefined
})
One of the best places to check for quick reference is the Aurelia cheat-sheet in the docs:
http://aurelia.io/docs/fundamentals/cheat-sheet
I've created a plunkr to illustrate the problem I'm having.
I'm in the middle of creating a dashboard which at the moment contains four different items. Each of these items I'm creating as Custom Elements then wrapping them in a Custom Element called Widget to give them a frame, title and styling. Here's what that snippet looks like:
<widget title="A Widget" icon="fa-question">
<template replace-part="item-template">
<child-element text.bind="$parent.$parent.someText"></child-element>
</template>
</widget>
For reference the widget view looks like this:
<template>
<require from="./widget.css!"></require>
<div class="widget">
<div class="widget-header">
<i class="fa ${icon}"></i>
<h3>${title}</h3>
</div>
<div class="widget-content">
<template replaceable part="item-template"></template>
</div>
</div>
</template>
and the view model is:
import {bindable} from "aurelia-framework";
export class WidgetCustomElement {
#bindable title;
#bindable icon;
#bindable show; // This is something I want the child element
// to be able to bind to and control but haven't
// got there yet!!
}
But notice I'm trying to bind data from the ViewModel into the child-element where the child-element looks like this:
import {bindable} from "aurelia-framework";
export class ChildElementCustomElement {
#bindable text;
}
and the view:
<template>
<p>The widget passed us : ${text}</p>
</template>
The problem is no matter what expression I use (and here I'm currently trying $parent.$parent.someText) I can't get the binding to work.
Should this work? I've also tried defining the variable someText in the main ViewModel as `#bindable someText' but that throws the following exception:
Unhandled promise rejection TypeError: Cannot read property 'some-text' of null
at BindableProperty.initialize (https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/github/aurelia/templating#0.14.4/aurelia-templating.js:2448:33)
at new BehaviorInstance (https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/github/aurelia/templating#0.14.4/aurelia-templating.js:2199:23)
at HtmlBehaviorResource.create (https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/github/aurelia/templating#0.14.4/aurelia-templating.js:2821:30)
at https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/github/aurelia/templating#0.14.4/aurelia-templating.js:3385:27
at f (https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/npm/core-js#0.9.18/client/shim.min.js:1415:56)
at https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/npm/core-js#0.9.18/client/shim.min.js:1423:13
at b.exports (https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/npm/core-js#0.9.18/client/shim.min.js:453:24)
at b.(anonymous function) (https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/npm/core-js#0.9.18/client/shim.min.js:1625:11)
at Number.f (https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/npm/core-js#0.9.18/client/shim.min.js:1596:24)
at q (https://cdn.rawgit.com/jdanyow/aurelia-plunker/v0.4.0/jspm_packages/npm/core-js#0.9.18/client/shim.min.js:1600:11)
I am not sure whether you are looking for this or not, however, introducing a #bindable text property in WidgetCustomElement makes this a whole lot easier. I worked on your plunk and introduced a #bindable wtext; in WidgetCustomElement, used this property to bind someText in app view like below:
<widget title="A Widget" icon="fa-question" wtext.bind="someText">
<template replace-part="item-template">
<child-element text.bind="wtext"></child-element>
<!--<child-element text.bind="$parent.$parent.someText"></child-element>-->
</template>
</widget>
And this works. However as I said I am not sure whether this approach works for you or not. What I have assumed is your widget will mostly contain one single child element and if this assumption is correct then this should work, however in case one-to-many mapping (many child element in single widget), something else is needed.
Hope this helps.
EDIT: I forked your plunk to make the changes and here is the link to that.
I have since stumbled across something in the Aurelia document detailing Template Parts. In example.js a bind() method is defined which assigns the bindingContext to a local member this.$parent.
This means all I have to do is defined the following in the view model for my widget:
bind(bindingContext) {
this.$parent = bindingContext;
}
Then the child-element is bound with the following:
<child-element text.bind="$parent.someText"></child-element>
I've forked my original plunkr to demonstrate it working.
Given repeat-for provides it's own $parent I had mistakenly believed there was one automatically defined here!!