I'm building a custom data grid framework for a LOB-style Aurelia app and need help with how to template the main grid element so it can pick up custom cell templates from child column elements for rendering.
This is what I've done so far:
grid-example.html
<data-grid items-source.bind="rowItems">
<data-column property-name="Name" t="[displayName]fields_Name">
<div class="flex -va-middle">
<div class="user-avatar avatar-square">
<img
if.bind="row.dataItem.avatarUri"
src.bind="row.dataItem.avatarUri" />
</div>
<span class="name">${row.dataItem.name}</span>
</div>
</data-column>
<data-column property-name="StatusText" t="[displayName]fields_Status">
<span class="label ${row.statusClass}">${row.dataItem.statusText}</span>
</data-column>
<data-column property-name="Location" t="[displayName]fields_Location">
<span>${row.dataItem.location}</span>
</data-column>
<data-column property-name="DateCreated" t="[displayName]fields_MemberSince">
<span tool-tip.bind="row.dataItem.dateCreated | dateToString:'long-date-time'">
${row.dataItem.dateCreated | dateToString:'short-date'}
</span>
</data-column>
</data-grid>
data-column.ts
import {
autoinject,
bindable,
noView,
processContent,
ViewCompiler,
ViewFactory } from "aurelia-framework";
import { DataGridCustomElement } from "./data-grid";
#autoinject
#noView
#processContent(false)
export class DataColumnCustomElement {
#bindable
propertyName: string;
#bindable
displayName: string;
cellTemplate: ViewFactory;
constructor(private readonly _element: Element,
private readonly _dataGrid: DataGridCustomElement,
private readonly _viewCompiler: ViewCompiler) {
this.cellTemplate = this._viewCompiler.compile(`<template>${this._element.innerHTML}</template>`);
this._dataGrid.columns.push(this);
}
}
data-grid.html
<template>
<div class="table-wrapper -data-list -sticky-header">
<table class="hover unstriped">
<tbody>
<tr class="labels">
<th repeat.for="column of columns">
<span>${column.displayName}</span>
</th>
</tr>
<tr repeat.for="row of itemsSource">
<td repeat.for="column of columns">
<!-- inject view for column.cellTemplate here? -->
</td>
</tr>
</tbody>
</table>
</div>
</template>
data-grid.ts
import { autoinject, bindable } from "aurelia-framework";
import { DataColumnCustomElement } from "./data-column";
#autoinject
export class DataGridCustomElement {
#bindable
itemsSource: any[] = [];
columns: DataColumnCustomElement[] = [];
constructor(private readonly _element: Element) {
}
}
The data-column elements declare a cell template which is parsed manually into a ViewFactory instance - what I'm stuck on is how to use the cell template for each data-column in the corresponding td repeater in the data-grid template, so it behaves as if I had directly declared the template content there.
Is this possible to do with the default repeat.for syntax? Or do I need a custom template controller to do this, which can additionally accept a ViewFactory instance as a bindable parameter from the scope?
If there is a better way to achieve this requirement then I'm open to that too.
You're essentially trying to compile+render dynamic html. There is nothing special about this specific to repeat.for or tables, but depending on what you're trying to achieve this is usually a bit more involved than simply passing html through the viewCompiler.
You can see an example in a plugin I wrote: https://github.com/aurelia-contrib/aurelia-dynamic-html/
I would probably either use that plugin (or simply copy+paste the code and tweak/optimize it to your needs) and then, keeping the rest of your code as-is, do something like this:
data-column.ts
this.cellTemplate = this._element.innerHTML; // just assign the raw html
data-grid.html
<tr repeat.for="row of itemsSource">
<td repeat.for="column of columns"
as-element="dynamic-html"
html.bind="column.cellTemplate"
context.bind="row[column.propertyName]">
</td>
</tr>
In any case, you'll make this easier for yourself if you just use a custom element like this or in some other form. Making your own repeater will be very difficult ;)
Related
I am trying to filter a table that get data from api and I tried this solution but it doesnt work.
I couldnt find where the problem is and if I pass the search input event listener
and here is my table component :
<template>
<table class="mt-12 border-2 border-gray-600">
<thead>
<tr>
<th v-for="header in data.headers" :key="header" class="text-left border-l-2 border-gray-600 border-b-2 border-gray-600 bg-red-400 ">{{ header }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(rows, index) in data.rows" :key="index">
<td v-for="row in rows" :key="row" class="border-l-2 border-gray-600" >{{ row }}</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
props: {
data: Object,
default: {}
}
}
</script>
<style src="../assets/tailwind.css"/>
My question:
If anyone can help me to define my problem and solve this ?
Thanks for help.
You can use your data as computed where you pass it as a prop.
<BaseTable :data='data' />
Here instead of using like this create a computed which can be filteredData.
<BaseTable :data='filteredData' />
and in your props you can simply filter it or just send the data as it is.
computed: {
filteredData() {
if(this.search) {
// filter your data as you want and return
}
else // return your main data
}
}
Here is a working simple example:
https://codesandbox.io/s/filterlist-example-vdwhg?file=/src/App.vue
And change your include to includes.
what error you are getting ? I think you are using this.search inside filteredRows function which is not a vue instance property. it should be this.data.search. The search is used with V-model as well so you should declare it outside data (JSON object).
Must the render method of a Vue component be used exclusively or can it be combined with a template? Most of my component can be rendered using a template but just need a small part of it to be rendered using code. If this is possible how can I combine the render method output with the template?
Example in component:
<template>
<table>
<tr>
// use render method here
</tr>
<tr v-for="row in rows">
// use render method here
</tr>
</table>
</template>
Need render method in spots above to loop through the array $scopedSlots.column and render each <th> and <td> based on multiple <templates v-slot:column={row}> provided by parent.
As far as I know you can either use the render function or a template - but not both. They can not be combined.
What you could do to make your example work is to use the v-html-directive, which updates the innerHTML of an element https://012.vuejs.org/api/directives.html#v-html.
new Vue({
el: '#el',
data: {
rows: ['row1', 'row2', 'row3']
},
methods: {
renderRow(row) {
return `<td>${row}</td>`;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="el">
<table>
<tr v-for="row in rows" v-html="renderRow(row)">
</tr>
</table>
</div>
I'm having this same challenge as well. Which is how I came to your question.
As others have said, it seems you can either use a template or render function but not both.
My suggestion would be to have a component handle only the render function, while the parent component can use the template method
So, Per your example
<template>
<table>
<tr>
<component-using-only-render-method />
</tr>
<tr v-for="row in rows">
<component-using-only-render-method />
</tr>
</table>
</template>
I think its the best of both worlds with minimal compromise
I am trying to add validation to custom element that gets generated in dynamic form component to support page for view. I injected Aurelia-Validation in main.ts, and DynamicForm.ts and instantiated. Below is my code.
CUSTOM ELEMENT:
TS File
import { customElement, useView, bindable, bindingMode, inject } from 'aurelia-framework';
#customElement('custom-input')
#useView('./custominput.html')
export class CustomInput {
#bindable({ defaultBindingMode: bindingMode.twoWay }) fieldValue: string;
#bindable public customClass: string;
#bindable public placeHolder: string;
#bindable public fieldName: string;
#bindable public formItem: any;
}
HTML View:
<template>
<input class="${customClass}" custom-class.bind="customClass" type="text" field-value.bind="fieldValue"
value.two-way="fieldValue & validateOnChange" placeholder="${placeHolder}" place-holder.bind="placeHolder"
id="${fieldName}" field-name.bind="fieldName" form-item.bind="formItem" />
</template>
DynamicForm
TS File:
import { bindable, bindingMode, inject } from 'aurelia-framework';
import { ValidationRules, ValidationControllerFactory } from 'aurelia-validation';
#inject(ValidationControllerFactory)
export class DynamicForm {
#bindable public formName: string;
#bindable public formTemplate: Object;
#bindable public callback;
inputItem: HTMLInputElement;
controller = null;
constructor(ValidationControllerFactory) {
this.controller = ValidationControllerFactory.createForCurrentScope();
}
public formValidator(element, field) {
//console.log(element);
}
public bind() {
if (this.formTemplate) {
this.formTemplate[this.formName].fields.forEach((item, i) => {
if (item.validation.isValidate === true) {
ValidationRules.ensure(item.name)
.displayName(item.name)
.required()
.on(this.formTemplate);
}
});
this.controller.validate();
}
console.log(this.controller);
}
}
HTML View:
<template>
<require from="../../elements/custominput/custominput"></require>
<form class="form-horizontal">
<div form-name.bind="formName" class="form-group" repeat.for="item of formTemplate[formName].fields">
<label for="${item.name}" class="col-sm-2 control-label">${item.label}</label>
<div class="col-sm-10" if.bind="item.type === 'text' && item.element === 'input'">
<custom-input router.bind="router" custom-class="${item.classes}" field-value.two-way="item.value"
place-holder="${item.placeHolder}" ref="inputItem" item.bind="formValidator(inputItem, item)"
field-name.bind="item.name" form-item.bind="item">
</custom-input>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-default pull-right" click.delegate="callback()">Submit</button>
</div>
</div>
</form>
<ul if.bind="controller.errors.length > 0">
<li repeat.for="error of controller.errors">${error}</li>
</ul>
</template>
Support page:
This page will load DynamicForm
<template>
<require from="./support.scss"></require>
<require from="../DynamicForm/dynamicform"></require>
<div class="panel panel-primary">
<div class="panel-heading">${pageTitle}</div>
<div class="panel-body">
<dynamic-form form-template.two-way="formTemplate" form-name.two-way="formName" callback.call="processForm()"></dynamic-form>
</div>
</div>
</template>
When I view the support page in browser, I do not get validation in UI. Not sure if validation is position in in nested components/elements. The view is generated like this custominput element -> DynamicForm -> support page
Plunker link for more information:
Any help is really appreciated. :)
Two major issues:
1. Rules shouldn't be stored on fields
Rules are stored on the prototype of an object and pertain to the properties of that object.
You are defining the rules on each individual property, so ultimately it's trying to validate property.property rather than object.property, which doesn't do much as you can see.
You're also declaring the rules every time the form template changes. I basically wouldn't put that logic there; put it closer to where those object come from.
If the objects are declared somewhere in your client code, declare the rules in the same module files
If the objects come from the server, declare the rules on those objects on the same place where you fetch them, right after you fetched them
Either way, those rule declarations don't belong in a change handler.
2. Bindings are missing
The ValidationController needs to know which object or properties you want to validate. It only knows in either of these cases:
Your rules are declared via controller.addObject(obj, rules).
Your rules are declared via ValidationRules.[...].on(obj) and the fields in your html template have & validate following them.
There's several pros and cons with either approach, ultimately you should go with one that gives you least resistance. I would probably go for the second approach because things get more entangled if you declare all rules on your controllers directly.
I want to create a Master/Detail page with table being the master that you click on a row and then it navigates to a detail page
Take the Aurelia ContactManager example and replace the list with a table
List example:
<li repeat.for="contact of contacts" class="list-group-item ${contact.id === $parent.selectedId ? 'active' : ''}">
<a route-href="route: contacts; params.bind: {id:contact.id}" click.delegate="$parent.select(contact)">
<h4 class="list-group-item-heading">${contact.firstName} ${contact.lastName}</h4>
<p class="list-group-item-text">${contact.email}</p>
</a>
</li>
Table example:
<table class="table" if.bind="contacts" id="myTable">
<thead>
<tr>
<th>IDs</th>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
<tr repeat.for="contact of contacts" class="clickable-row ${contact.id === $parent.selectedId ? 'active' : ''}">
<a route-href="route: contacts; params.bind: {id:contact.id}" click.delegate="$parent.select(contact)">
<td>${contact.id}</td>
<td>${contact.name}</td>
</tr>
</tbody>
</table>
I know how to get the row onlick to work using jquery without Aurelia as in
$(".class='clickable-row").click(function() {
window.location = $(this).data("href");
});
but I do not know how to use aurelia navigation with the row click.
The solution does not necessarily need to use jquery onlcick just whatever is appropriate for the Master/Detail scenario with Aurelia
If you wanted to capture click on <tr> specifically
<tr repeat.for="contact of contacts" click.delegate="onSelectContact($event, contact)">
<td style="cursor: pointer;">lalala</td>
</tr>
and in your view-model, something like..
import { autoinject } from 'aurelia-framework'
import { Router } from 'aurelia-router'
import { Contact } from '../la/la'
#autoinject
export class Contacts {
contacts: Array<Contact> = []
constructor(private router: Router) {
}
onSelectContact (event: UIEvent, contact: Contact) {
...... // do whatever
this.router.navigateToRoute('contact', {id: contact.id})
}
...
}
however, this requires your router config to contain a
{ route: 'contact/:id', name: 'contact', moduleId: PLATFORM.moduleName('path to module'), title: 'Contact' }
My template is:
<tbody id="deliveries-table">
<tr v-for="item in deliveries">
<td class="table-view-item__col"></td>
<td class="table-view-item__col" v-bind:class="{ table-view-item__col--extra-status: item.exclamation }"></td>
<td class="table-view-item__col">{{item.number}}</td>
<td class="table-view-item__col">{{item.sender_full_name}}</td>
<td class="table-view-item__col" v-if="item.courier_profile_url">{{item.courier_full_name}}</td>
<td class="table-view-item__col" v-if="item.delivery_provider_url">{{item.delivery_provider_name}}</td>
<td class="table-view-item__col">
<span style="font-weight: 900">{{item.get_status_display}}</span><br>
<span>{{item.date_state_updated}}</span>
</td>
</tr>
</tbody>
My javascript code for render a lot of prepared data is:
var monitorActiveDeliveries = new ActiveDeliveries();
monitorActiveDeliveries.fillTable(allDeliveries);
class ActiveDeliveries {
constructor() {
this.table = new Vue({
el: '#deliveries-table',
data: {
deliveries: []
}
});
}
fillTable (d) {
this.table.deliveries = d;
}
}
But after script starts any render into tbody, i have just empty place in HTML.
Where i got some wrong?
First, although you can instantiate your Vue app on the <table> tag, usually you want just a single Vue instance on the whole page, so it might be better to make the Vue instance on one main div/body tag.
Second, I think your code could work (I don't know what your deliveries objects should look like...), but your fillTable() method is probably not getting called, i.e. deliveries are empty.
I made this working example based on your code: http://jsfiddle.net/wmh29mds/
Life is easier than it seems.
I got a mistake into this directive:
v-bind:class="{ table-view-item__col--extra-status: item.exclamation }"
I just forgot single quotas into class name, next variant is working:
v-bind:class="{ 'table-view-item__col--extra-status': item.exclamation }"