ViewModel-less compose with model.bind in aurelia - aurelia

I am having a structure where the main view composes a partial view that composes another partial view in a repeater.
Example:
main view
<template>
<h1>${factory.name}</h1>
<div class="column">
<compose view="./cars.html"></compose>
</div>
</template>
cars view
<template repeat.for="car of factory.cars">
<compose view="./specifications.html model.bind="{test: 'abc}"></compose>
</template>
specifications view
<template repeat.for="car of factory.cars">
<h1>${$parent.$parent.factory.name} - ${car.name}</h1>
${test}
</template>
The problem I am facing is that the model.bind in compose doesn't work. I tried it with the test above, but what I'd actually want to pass there is $parent.$parent.factory so I can output $parent.$parent.factory.name in the specifications view.
(I know I can print it like this, but the scenario gets way more complicated so
the binding is necessary)
Worth to mention that both specifications and cars view are viewmodel-less. Only themain view has a viewmodel where factory and cars are coming from.
According to this page, what I am trying to do is possible, but I can't wrap my head about what I'm doing wrong.

When composing with just an html file, the view-model for the referenced html file is the same as where the compose element is placed. In other words, it inherits the view-model of the parent. So you don't need to supply the model.
main view
<template>
<h1>${factory.name}</h1>
<div>
<compose view="./cars.html"></compose>
</div>
</template>
cars.html
<template>
<div repeat.for="car of factory.cars">
<compose view="./specifications.html"></compose>
</div>
</template>
specifications.html
<template>
<h1>${factory.name} - ${car.name}</h1>
</template>
Take a look at this GistRun example.

What #Jeff G said is correct, but for my specific use-case what solved my issue was creating a simple view-model that I could use for all compositions. It's looking like:
export class Main {
public parentFactory;
public activate(data) {
this.parentFactory= data;
}
}
And in the view
<compose
view="./car.html"
view-model="../view-models/main"
model.bind="$parent.$parent.factory">
</compose>

Related

How to properly create a popup component in Vue 3

As part of becoming a better Vue programmer, I am trying to implement a popup similar to Popper with a clean and Vueish architecture. Here is a simple schematic that I came up with:
So basically there is a target component, which is the reference for the popup's position. The popup can be positioned above, below, right and left of the target, therefore I will need to have access to the target element in my popup. Also, the target can be an arbitrary component. It can be a simple button or span, but also something much more complex.
Then there is the popup itself, which will be put into a modal at the end of the body, It contains the actual content. The content again can be an arbitrary component.
I have a working implementation of the popup, but the basic structure seems to be far from perfect. I am using two slots, one for the target element and one for the content.
Here is what I have come up with so far for the template:
<template>
<div ref="targetContainer">
<slot name="target"></slot>
</div>
<teleport to="body">
<div v-show="show" class="modal" ref="modal">
<div ref="popover" class="popover" :style="{top: popoverTop + 'px', left: popoverLeft + 'px'}">
<slot name="content"></slot>
</div>
</div>
</teleport>
</template>
There are several issues with this that I am not really happy with.
Using the popup is not very simple
When using this popup in another component, two <template> tags are rquired. This is ungly and not very intuitive. A very simple use case looks like this:
<modal :show="showPopup" #close="showPopup=false">
<template v-slot:target>
<button #click="showPopup=true"></button>
</template>
<template v-slot:content>
<div>Hello World!</div>
</template>
</modal>
The target is wrapped in another <div>
This is done to get access to the target element, that I need for the layout. In mounted() I am referencing the target element like this:
let targetElement = this.$refs.targetContainer.children[0];
Is this really the best way to do this? I would like to get rid of the wrapping <div> element, which just asks for unintended side effects.
The best solution would be to get rid of one slot and somehow reference the target element in another way because I only need its layout information, it does not have to be rendered inside the popover component.
Can someone point me in the right direction?
Here is my solution, which was inspired by a comment on my question and which I think is worth sharing.
Instead of putting the target element into a slot, I am now passing its ref as a prop, which makes things much cleaner.
The popover component's template now looks like this.
<template>
<teleport to="body">
<div v-show="show" class="modal" ref="modal">
<div ref="popover" class="popover" :style="{top: popoverTop + 'px', left: popoverLeft + 'px'}">
<slot ref="content"></slot>
</div>
</div>
</teleport>
</template>
I has a targetRefprop, so the component can be simply used like this:
<div ref="myTargetElement" #click="isPopupVisible=true">
</div>
<modal :show="isPopupVisible" #close="isPopupVisible=false" targetRef="myTargetElement">
<!-- popup content goes here -->
</modal>
And after mounting I can access the target element like this:
let targetElement = this.$parent.$refs[this.targetRef];
I like this solution a lot. However, ideas, advice or words of caution are still highly welcome.

control over inherited attributes by vue component

Is there a way to have control over attributes provided through the component tag?
For example:
<my-component class="myClass" style="myStyle"></my-component>
My component:
<template>
<div>
<div>
</div>
<div>
</div>
</div>
</template>
At render Vue applies given attributes on the root:
<div class="myClass" style="myStyle">
<div>
</div>
<div>
</div>
</div>
I want to control where those attributes are applied like so:
<div>
<div>
</div>
<div class="myClass" style="myStyle">
</div>
</div>
#Boussadjra Brahim answer is definitely one way to handle it, however this will require you to pass in all of the class attributes you want everytime you define the component.
This question is answered in this SO post already as well.How to style a nested component from its parent component in Vuejs?
If you want a bit more flexibility I would suggested using interpolation and properties as below. This will let you define some default classes and pass in whatever else in addition.
<app-header :headerclass="parent-header-class"> </app-header>
Inside of your child component, you can use these properties and v-bind the class inside the HTML, as shown in the example below:
<template>
<div :class=`${headerClass} internal-class-example button`> </div>
</template>
Note: This does not allow you to use any scoped parent CSS to pass to the child. The classes you pass down must be global. Otherwise, the child component will not know what it is.

Reference dynamically created component in Aurelia

I know I can create a reference to my component in my view model like this:
.html:
<template>
<mdfield view-model.ref="ref"></mdfield>
</template>
.ts:
export class Vm {
ref: any;
test(){
console.log(this.ref);
}
}
This works, but what is the syntax if I'm creating the components dynamically? Like this:
<template>
<div repeat.for="field of fields">
<mdfield view-model.ref="<what goes here?>"></mdfield>
</div>
</template>
I guess I want to add them to an array in my viewmodel for later reference, but how?
$index gives you the current index of the repeat.for. So, if you want to add the view-model references to an array:
<div repeat.for="field of fields">
<mdfield view-model.ref="refArray[$index]"></mdfield>
</div>

Providing the model for a component as a slot

Consider the following two custom elements in Aurelia (list & row):
row.html
<template>
<span>${name}</span>
</template>
row.js
export class Row
{
name = "Marry";
}
list.html
<template>
The List
<ol>
<li repeat.for="r of rows">
<slot name="rowItem" model.bind="r"></slot>
</li>
</ol>
</template>
list.js
import { bindable } from 'aurelia-framework';
export class List
{
#bindable
rows = [{name: "John"}];
}
The app will tie them together:
app.html
<template>
<require from="./list"></require>
<require from="./row"></require>
<list rows.bind="users">
<row slot="rowItem"></row>
</list>
</template>
app.js
export class App
{
users = [{name: "Joe"}, {name: "Jack"}, {name: "Jill"}];
}
The problem is that the model for the row is not set correctly. All I get as the output is the following:
The List
1.
2.
3.
So the question is; how can I provide the model for a slot in Aurelia?
Here's a Gist to demonstrate the problem in action.
Slots aren't going to work for what you want to do. It's a known limitation of slots in Aurelia. Slots can't be dynamically generated (such as inside a repeater).
Luckily, there's another option to accomplish what you want: template parts.
Template parts aren't well documented (my fault, I should have written the docs for them). But we have some docs in our cheat sheet. I've modified your gist to show how to use them: https://gist.run/?id=1c4c93f0d472729490e2934b06e14b50
Basically, you'll have a template element in your custom element's HTML that has the replaceable attribute on it along with a part="something" attribute (where something is replaced with the template part's name. Then, when you use the custom element, you'll have another template element that has the replace-part="something" attribute (again, where something is replaced with the template part's name). It looks like this:
list.html
<template>
The List
<ol>
<li repeat.for="row of rows">
<template replaceable part="row-template">
${row}
</template>
</li>
</ol>
</template>
app.html
<template>
<require from="./list"></require>
<require from="./row"></require>
<list rows.bind="users">
<template replace-part="row-template">
<row name.bind="row.name"></row>
</template>
</list>
</template>

How to use template instead of compose

I have a TreeView based on Aurelia binding and it works fine.
There is one component called TreeView with the usual view and viewmodel.
I then have another view TreeViewNode.html which the TreeView uses recursively.
<template>
<div content-id="treeview-root">
<compose view="./tree-view-node.html"></compose>
<compose repeat.for="item of root.items" view="./tree-view-node.html"></compose>
</div>
</template>
This all works. However, I would like to turn the TreeViewNode into a custom element instead of just using compose which inherits the parent view-model.
The issue with turning it into a custom element is that it loses the TreeView view-model which contains all the methods to process events such as drag and drop, and item selection.
You can use bindables to pass in the parts of the view-model needed by the custom element.
tree-view-node.html:
<template bindable="viewModelParts">
<div click.trigger="viewModelParts.itemSelected()">Click here</div>
</template>
consumer.html:
<template>
<require "./tree-view-node.html></require>
<div content-id="tree view-root">
<tree-view-node repeat.for="item of root.items" view-model-parts.one-time="theViewModelParts"></tree-view-node>
</div>
</template>