Aurelia: How to bind viewmodels of child DOM elements to parent's viewmodel? - aurelia

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.

Related

Passing parent vm as a prop to a component

So I’m building a Nuxt app for working with docs (in a broad sense), and it will have a menu, which I will obviously make a component. The menu will be home to lots of actions on the doc itself, such as opening/saving files, editing, etc. etc.
I know the standard way to pass info from a component to its parent (the doc vm in this case) is via messages, but it feels like a bit of an overkill, what with the syntax (emit handlers just don’t feel natural to me in this case) and whatnot.
For this reason I was wondering why can’t I just pass the parent vm as a prop to the menu component? It will contain all kinds of methods, and I will be able to easily invoke them via the menu. Something like:
Parent (Document.vue):
<template>
<main-menu :document='vm'/>
</template>
<script>
import MainMenu from '~/components/MainMenu.vue'
export default {
data(): {
return {
vm: this,
//...
}
},
methods: {
save() {
//...
}
}
//...
</script>
Menu component (MainMenu.vue):
<template>
<button #click='document.save()'>Save document</button>
</template>
<script>
export default {
props = ['document']
}
</script>
The question: Is there something intrinsically bad in this approach?
(I imagine this could be problematic if the app architecture could change, but it’s hard to imagine that I would for some reason need a menu without an underlying document.)
IF your Menu is always the child of the component, then you don't have to pass your parent. It is already held in a Vue variable called this.$parent.
I made a little sandbox to give you an example.
The parent has a function, for example:
/// PARENT
export default {
name: "App",
components: {
HelloWorld,
},
methods: {
iExist(add) {
console.log("I am in parent" + add);
},
},
};
Then you can call it from child with this.$parent.iExist('something').
Since this.$parent is not defined when the template is being evaluated, we have to make a method in the child as well, to call(super) the corresponding function on it's parent.
/// CHILD
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<button #click="iExist(', but was called from child')">Click Me</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
methods: {
iExist(add) {
this.$parent.iExist(add);
},
},
};
</script>
The question: Is there something intrinsically bad in this approach?
(I imagine this could be problematic if the app architecture could change, but it’s hard to imagine that I would for some reason need a menu without an underlying document.)
Yes, this is bad design. Parents can be aware of children, children shouldn't be aware of parents. A child could be tested in isolation, or be nested inside wrapper component that doesn't have this method.
As another answer suggests, a way to access a parent is to use $parent property. This part was borrowed in Vue from AngularJS 1.x, accessing it was considered a bad practice even then.
This is generally achieved by providing a callback from a parent that does exactly a desired thing, without allowing to access the whole instance and break the encapsulation. It's unnecessary to explicitly define callback function in Vue because this is naturally provided by Vue template syntax:
In a parent:
<child #save="save()">
In a child:
<button #click="$emit('save')">
In case of deeply nested components the event can be passed through them to a parent.

Aurelia Custom Elements: Access Parent method

I am using Aurelia's Custom Elements to repeat over a set of entries. Here is the sample gist: https://gist.run/?id=38aee854447122f021bc05e1e0de25ae
Now, I need to access the deleteEntry(entry) method when clicked on the button defined in custom element. I tried using $parent.deleteEntry(entry) but it's not working.
Saw this issue, but it's more than an year old and I am wondering if there is a cleaner way to achieve this now.
Why not use the call binding to accomplish this?
Here's an example: https://gist.run?id=3cc553ea3bd7ed1862d87d8dbe4f5f84
app.html
<template>
<require from="./entry"></require>
<h2 class='text-center'>Journal Entries</h2>
<div>
<entry repeat.for='entry of entries' entry.bind='entry' delete-function.call="deleteEntry(entry)"></entry>
</div>
</template>
app.js
export class App {
entries = [{
'date': 'Jan 1',
'note': 'Hello World'
}, {
'date': 'Jan 2',
'note': 'Good Morning'
}];
deleteEntry(entry) {
console.log("Deleting entry");
console.log(entry);
const index = this.entries.indexOf(entry);
this.entries.splice(index, 1);
}
}
entry.html
<template>
<div>${entry.date} <button click.trigger='delete()'>X</button></div>
<div>${entry.note}</div>
</template>
entry.js
import {bindable} from 'aurelia-framework';
export class EntryCustomElement {
#bindable entry;
#bindable deleteFunction;
delete() {
this.deleteFunction();
}
}
Obviously in a real implementation, you'll need to make sure that what is bound to deleteFunction is actually a function before trying to call it.
Using bind life cycle event you can get parent View modal in Aurelia.
bind(bindingContext, overrideContext) {
this.parent = bindingContext;
}
Now you can access all the variables and methods from parent view to your view.
Like below code in child view
this.parent.parentmethod();

Aurelia - call function on nested component

I want to call a child component's function from its parent. I have a way to do it, but I want to know if I'm missing a better way.
From Ashley Grant's blog post about accessing a custom element's viewModel from a custom attribute, I see that Aurelia adds au to the element and you can access the viewModel through that. So, if I add a nested component with a ref, like this:
<template>
<nested-element ref="childElement"></nested-element>
</template>
I can call a function on it like this:
this.childElement.au.controller.viewModel.someFunction();
This feels roundabout. I was hoping I would be able to access a nested element's viewModel through the parameters to a hook that the parent implements, such as created(owningView, myView) but I can't find a path to it.
Have I missed a better way?
Edit: I forgot to add that I need a return value from the function I'm calling, so having access to the viewmodel itself is preferable
ref gives you the element. view-model.ref gives you the element's view model.
<template>
<nested-element view-model.ref="childViewModel"></nested-element>
</template>
Call it like this in the parent view-model:
this.childViewModel.someFunction();
If you only have one instance of the nested-element or don't care if multiple nested-elements respond to the event. Then you could use standard Javascript event functionality for this:
bar.html
<template>
<h1>${value}</h1>
<input type="text" value.bind="value"></input>
<foo>text</foo>
</template>
bar.ts
import {bindable} from 'aurelia-framework';
export class Bar {
#bindable value;
public valueChanged(newValue, oldValue) {
var event = new CustomEvent("some-event-name", { "detail": { message: "Hello from Bar", oldValue, newValue } });
document.dispatchEvent(event);
}
}
foo.html
<template>
<h1>${value}</h1>
</template>
foo.ts
import {bindable} from 'aurelia-framework';
export class Foo {
constructor() {
document.addEventListener("some-event-name", (e) => {
console.log('hello here is Foo, recieved event from Bar : ', e);
}, true);
}
}

Passing the parent when parent is the same component type in Aurelia

Edit: This question used to be titled "Getting parent via DI when parent is the same type in Aurelia" but due to how my elements are nested, it makes more sense to just bind the parent to the element, so the title has been changed to reflect this.
If I have a custom element, Thing which has a child Thing (which has a another child Thing, etc), how can I inject the parent instance when the class is the same?
export class Thing {
static inject = [Thing]; // <-- no reference to Thing as we are inside the class
constructor(parentThing) {
this.parent = parentThing;
}
}
As a further complexity, the root Thing element will have no parent, so the DI needs to allow for optional injection.
I don't think your problem can be solved with DI. Thing has to be #transient if you want to have several instances of it. This means that the container will not hold references to things it creates.
This problem doesn't look right or necessary to use DI. If an element need to receive some specific input data from its consumer, #bindable would be my natural first thinking. So how about creating a #bindable parentThing in Thing?
In other hand, if what you want is to access parent binding context, consider bind() component life cycle.
Here's how to do that: https://gist.run?id=b075366d29f2400d3cc931f6fc26db24
app.html
<template>
<require from="./thing"></require>
<thing name="A">
<thing name="B">
<thing name="C">
<thing name="D">
</thing>
</thing>
</thing>
</thing>
</template>
app.js
export class App {
}
optional-parent.js
import {resolver} from 'aurelia-dependency-injection';
#resolver()
export class OptionalParent {
constructor(key) {
this.key = key;
}
get(container) {
if (container.parent && container.parent.hasResolver(this.key, false)) {
return container.parent.get(this.key)
}
return null;
}
static of(key) {
return new OptionalParent(key);
}
}
thing.html
<template>
<h3>
My Name is "${name}".
<span if.bind="parent">My parent's name is "${parent.name}".</span>
<span if.bind="!parent">I don't have a parent.</span>
</h3>
<content></content>
</template>
thing.js
import {bindable, inject} from 'aurelia-framework';
import {OptionalParent} from './optional-parent';
#inject(OptionalParent.of(Thing))
export class Thing {
#bindable name;
constructor(parent) {
this.parent = parent;
}
}

How to set a parent property from within a custom element in Aurelia?

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>