Using slots to render slot content for child component

I’m working with aurelia-mdc-web, the data-table component.

I have a child component that looks like this:

<template>
    <mdc-data-table class="admin">
        <mdc-data-table-header>
            <mdc-data-table-header-cell>
                <mdc-checkbox></mdc-checkbox>
            </mdc-data-table-header-cell>

            <mdc-data-table-header-cell>Edit</mdc-data-table-header-cell>
            <slot name="header"></slot><!-- SLOT HEADER -->
        </mdc-data-table-header>
        <mdc-data-table-content>
            <mdc-data-table-row repeat.for="row of rows">

                <mdc-data-table-cell>
                    <mdc-checkbox checked.bind="row.__checked"></mdc-checkbox>
                </mdc-data-table-cell>
                <mdc-data-table-cell>
                    <button click.delegate="edit(row)" mdc-icon-button icon="edit"></button>
                </mdc-data-table-cell>
                <slot name="row"></slot><!-- SLOT ROW -->
            </mdc-data-table-row>
        </mdc-data-table-content>
    </mdc-data-table>
</template>

My goal with this was to be able to pass in header slot, and row slot so that the headers and rows could be customized while being able to reuse the bulk of the viewmodel functionality.

However, after using this in a parent component, neither the header, or row slots get rendered.

        <admin-table row-type.bind="UserType" service.bind="usersService" dialog-view-model.bind="UserEditViewModel">
            <div slot="header">
                <mdc-data-table-header-cell>Name</mdc-data-table-header-cell>
                <mdc-data-table-header-cell>Username</mdc-data-table-header-cell>
                <mdc-data-table-header-cell>Email</mdc-data-table-header-cell>
                <mdc-data-table-header-cell>Roles</mdc-data-table-header-cell>
            </div>
            <div slot="row">
                <mdc-data-table-cell>${row.fullname}</mdc-data-table-cell>
                <mdc-data-table-cell>${row.username}</mdc-data-table-cell>
                <mdc-data-table-cell>${row.email}</mdc-data-table-cell>
                <mdc-data-table-cell>
                    <mdc-form-field repeat.for="role of roles">
                        <mdc-checkbox checked.to-view="userHasRole(user, role)"
                            change.delegate="updateUserRole(user, role, $event.target.checked)"></mdc-checkbox>
                        <label>${role.role_name}</label>
                    </mdc-form-field>
                </mdc-data-table-cell>
            </div>
        </admin-table>

Is this what would be considered “dynamic” slot content IE a limitation of the slots? Is there a better way to accomplish this?

Okay - I have an update. Instead of trying to render the header and rows as slots - I’m wrapping the entire data table in a slot and passing that instead.

My next challenge is to give the slot access to the viewModel properties, like rows. Is there a way to say something like <slot rows.bind="rows"> so the slot can render the rows?

Similar to vue’s functionality of scoped slots

The issue you’re seeing here is the use of slots inside the repeaters which wont work with bindables. Instead try replaceable parts https://gist.github.com/martonsagi/dcffe2afcb1eee1777e9b0d9f7366d28

1 Like

Cool! I’ll give that a try! THANKS!

1 Like

Okay - so the interesting thing is that the replaceable template works outside of the mdc-data-table, but not inside it. I think this is because of how mdc components likely work, they appear to be doing the dom manipulation in the component and don’t expect the <template> tag inside the component, that’s my guess anyways.

I was able to get what I wanted working by using the compose tag - and just copying/pasting the template with modifications but its less DRY.

Hey @zewa666 - the replace-part tag is now working for me! Thank you much!

Basically I wrapped my whole mdc-data-table in the replaceable template. This lets me replace the table, but preserve my pagination and filtering functionality. So what I have now is:


        <admin-table row-type.bind="Role"
            service.bind="rolesService"
            dialog-view-model="RoleEditViewModel"
            title="Role">
            <template replace-part="table">
            
            <mdc-data-table class="admin">
                <mdc-data-table-header>
                    <mdc-data-table-header-cell>
                        <mdc-checkbox></mdc-checkbox>
                    </mdc-data-table-header-cell>

                    <mdc-data-table-header-cell>Edit</mdc-data-table-header-cell>
                    <mdc-data-table-header-cell>Name</mdc-data-table-header-cell>
                    <mdc-data-table-header-cell>Username</mdc-data-table-header-cell>
                </mdc-data-table-header>

                <mdc-data-table-content>
                    <mdc-data-table-row repeat.for="row of rows">
                        <mdc-data-table-cell>
                            <mdc-checkbox checked.bind="row.__checked"></mdc-checkbox>
                        </mdc-data-table-cell>
                        <mdc-data-table-cell>
                            <button click.delegate="edit(row)" mdc-icon-button icon="edit"></button>
                        </mdc-data-table-cell>
                        <mdc-data-table-cell>${row.role_name}</mdc-data-table-cell>
                        <mdc-data-table-cell>${row.description}</mdc-data-table-cell>
                    </mdc-data-table-row>
                </mdc-data-table-content>
            </mdc-data-table>
            </template>
        </admin-table>

Awesome, hey could you perhaps Post an image so that we can see what this looks like? Also have you already checked out aurelia-slickgrid from @ghiscoding, perhaps that might be another nice fit if you’re looking for a powerful datagrid

For sure - its still very early in development, so its not super usable (yet) but here’s the full details:

import { BaseModel } from 'types/BaseModel';
import { MdcSnackbarService } from '@aurelia-mdc-web/snackbar';
import { BaseService } from 'services/api/BaseService';
import { DialogService } from 'aurelia-dialog';
import { bindable, inject } from 'aurelia-framework';
import { ValidationController } from 'aurelia-validation';
import { Field } from 'types/Field';

import debug from 'debug';
const logger = debug('app/components/admin/admin-table');

function titleize(key: string) {
    const split = key
        .replace(/_/g, ' ')
        .replace(/([A-Z])/g, " $1");
    return split.charAt(0).toUpperCase() + split.slice(1);
}

@inject(
    DialogService,
    MdcSnackbarService,
)
export class AdminTable {
    @bindable _fields: Field[]
    @bindable RowType: any;
    @bindable service: BaseService;
    @bindable DialogViewModel: any;
    @bindable controller: ValidationController;

    @bindable rows?: BaseModel[];    
    @bindable params = {};
    @bindable perPage = 10;
    @bindable page = 1;
    @bindable total = 0;

    @bindable title: string;

    constructor(
        protected dialogService: DialogService,
        protected snackbarService: MdcSnackbarService,
    ) {
        
    }

    attached() {
        this.fetchRows();
    }

    perPageChanged(val) {
        this.params = {
            ...this.params,
            $limit: val,
        }
    }

    pageChanged(val) {
        this.params = {
            ...this.params,
            $skip: (val-1) * this.perPage,
        }
    }

    paramsChanged() {
        this.fetchRows();
    }

    fetchRows() {
        if (!this.service) {
            logger('Missing required property: service')
            return;
        }
        return this.service.service.find({ query: this.params })
            .then(result => {
                Object.assign(this, {
                    total: result.total,
                    rows: result.data.map(row => new this.RowType(row)),
                })

                if (!this.fields && result.data.length) {
                    this.fields = this.getFieldsFromRow(result.data[0])
                }
            });
    }

    set fields(fields: Field[]) {
        this._fields = fields;
    }
    get fields() : Field[] {
        if (!this._fields && this.rows?.length) {
            return this.getFieldsFromRow(this.rows[0])
        }
        return this._fields || [];
    }

    getFieldsFromRow(row: BaseModel) {
        return Object.keys(row).map(key => ({
            label: titleize(key),
            name: key,
        }))
    }

    create() {
        return this.edit({} as BaseModel)
    }

    edit(item: BaseModel) {
        return this.dialogService.open({
            viewModel: this.DialogViewModel,
            model: {...item},
        }).whenClosed(result => {
            if (result.wasCancelled) {
                return;
            }
            if (item.id) {
                for (let i = 0; i < this.rows.length; i++){
                    if (this.rows[i].id === item.id) {
                        this.rows = this.rows.map(row => {
                            if (row.id === item.id) {
                                return result.output;
                            }
                            return row;
                        })
                        break;
                    }
                }
            } else {
                this.rows = [
                    ...this.rows,
                    new this.RowType(result.output),
                ]
            }
        })
    }

    getSelectedRows() : BaseModel[] {
        return this.rows.filter(row => !!row.__checked);
    }

    deleteSelected() {
        const selected = this.getSelectedRows()
        if (!selected.length) {
            this.snackbarService.open(`No ${this.title}(s) selected`)
            return;
        }
        return this.delete(selected)
    }

    delete(users: BaseModel[]) {
        return Promise.all(users.map(user => {
            return this.service.service.remove(user.id)
                .then(() => this.params = {...this.params})
        })).then(() => this.snackbarService.open(`${this.title}(s) deleted`))
            .catch(e => {
                logger(e)
                this.snackbarService.open(e);
            })
    }

}
<template>
    <template replaceable part="table">
        <mdc-data-table class="admin">
            <mdc-data-table-header>
                    <mdc-data-table-header-cell>
                        <mdc-checkbox></mdc-checkbox>
                    </mdc-data-table-header-cell>
                    <mdc-data-table-header-cell>Edit</mdc-data-table-header-cell>
                    <mdc-data-table-header-cell repeat.for="field of fields">${field.label}</mdc-data-table-header-cell>
            </mdc-data-table-header>
            <mdc-data-table-content>
                <mdc-data-table-row repeat.for="row of rows">
                        <mdc-data-table-cell>
                            <mdc-checkbox checked.bind="row.__checked"></mdc-checkbox>
                        </mdc-data-table-cell>
                        <mdc-data-table-cell>
                            <button click.delegate="edit(row)" mdc-icon-button icon="edit"></button>
                        </mdc-data-table-cell>
                        <mdc-data-table-cell repeat.for="field of fields">${row[field.name]}</mdc-data-table-cell>
                </mdc-data-table-row>
            </mdc-data-table-content>
        </mdc-data-table>
    </template>
    <div class="admin-actions">
        <button mdc-button type="button" click.delegate="create()">
            <mdc-icon>add</mdc-icon> Create ${title}
        </button>
        <button mdc-button type="button" click.delegate="deleteSelected()">
            <mdc-icon>delete</mdc-icon> Delete Selected
        </button>
    </div>
    <mdc-pagination per-page.two-way="perPage" page.two-way="page" total.bind="total">
    </mdc-pagination>
</template>

Example usage:

<mdc-expandable accordion="admin">
        <div slot="caption">Manage Users</div>

        <admin-table row-type.bind="User" service.bind="usersService" dialog-view-model.bind="UserEditViewModel"
            title="User">
            <template replace-part="table">
                <mdc-data-table class="admin">
                    <mdc-data-table-header>
                        <mdc-data-table-header-cell>
                            <mdc-checkbox></mdc-checkbox>
                        </mdc-data-table-header-cell>

                        <mdc-data-table-header-cell>Edit</mdc-data-table-header-cell>
                        <mdc-data-table-header-cell>Name</mdc-data-table-header-cell>
                        <mdc-data-table-header-cell>Username</mdc-data-table-header-cell>
                        <mdc-data-table-header-cell>Email</mdc-data-table-header-cell>
                        <mdc-data-table-header-cell>Roles</mdc-data-table-header-cell>
                    </mdc-data-table-header>
                    <mdc-data-table-content>
                        <mdc-data-table-row repeat.for="row of rows">

                            <mdc-data-table-cell>
                                <mdc-checkbox checked.bind="row.__checked"></mdc-checkbox>
                            </mdc-data-table-cell>
                            <mdc-data-table-cell>
                                <button click.delegate="edit(row)" mdc-icon-button icon="edit"></button>
                            </mdc-data-table-cell>
                            <mdc-data-table-cell>${row.fullname}</mdc-data-table-cell>
                            <mdc-data-table-cell>${row.username}</mdc-data-table-cell>
                            <mdc-data-table-cell>${row.email}</mdc-data-table-cell>
                            <mdc-data-table-cell>
                                <mdc-form-field repeat.for="role of roles">
                                    <mdc-checkbox checked.to-view="row.hasRole(role)"
                                        change.delegate="updateUserRole(row, role, $event.target.checked)">
                                    </mdc-checkbox>
                                    <label>${role.role_name}</label>
                                </mdc-form-field>
                            </mdc-data-table-cell>
                        </mdc-data-table-row>
                    </mdc-data-table-content>
                </mdc-data-table>
            </template>
        </admin-table>
    </mdc-expandable>
1 Like

That does seem like a lot of code, you should definitely take a look at my lib Aurelia-Slickgrid, the main difference in comparison to your approach is that I always prefer to have as small code as possible in the View and rather have that in the ViewModel, for example defining the Column Definitions, Grid Options (editing, filtering, exporting, …) is all declared in the ViewModel. Lastly, Aurelia-Slickgrid was originally built with Bootstrap styling but I now also have other SASS Themes like Material Design (with plenty of variables to customize everything).

I often see other grid libraries filling up their Views with lot of html code and tags, it’s not necessarily bad but it’s a lot harder to unit test, so you might want to keep that in mind.

Good luck and continue sharing :wink:

1 Like

Hey! Thanks for the feedback. I’m quite new to Aurelia - and every piece of advice I can gather helps me get a better idea of what I could/should be doing so your feedback is much appreciated!

I will definitely take a look into slick grid paired with some sass from the material design - that’s the goal anyways :slight_smile:

And YES - I do want to have solid unit tests when I get this dialed in so that is definitely something to consider.

2 Likes

You could read the Aurelia-Slickgrid blog post I wrote a few months back, it will give you a better overview of what the lib is about.

1 Like

Awesome - will do. Looks like a good resource to get started. I’ll post an update when I get it implemented.

1 Like

To give you an idea of the Material Design Theme that I created, you can take a look at this Example from my other lib. Also, like I said earlier, it’s totally stylable, I have added over 800+ SASS variables over time, so you can change it quite a lot. I have been working on the lib for over 3 years, it’s also fully tested.

If you want to get started with Aurelia-Slickgrid, then follow the Wiki - Step by Step or even easier than that, simply clone the Aurelia-Slickgrid-Demos.

Good luck.

Side Note, I have started working on the next major version 3.x, it shouldn’t be too much hard to switch from current version to the future. More info in this WIP PR, I’m expecting a release before year end and is totally unrelated to Aurelia2.