Skip to content

feat(schematics): table schematic #10012

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 8, 2018
Merged

Conversation

amcdnl
Copy link
Contributor

@amcdnl amcdnl commented Feb 18, 2018

@amcdnl amcdnl self-assigned this Feb 18, 2018
@amcdnl amcdnl requested a review from jelbourn February 18, 2018 17:33
@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Feb 18, 2018
@@ -0,0 +1,5 @@
# Table
Creates a table component with sorting and pager.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"pager" -> "paginator"

image

<div class="mat-elevation-z8">
<mat-table #table [dataSource]="dataSource" matSort>

<!-- Position Column -->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just do two columns (ID and Name), since they're pretty universal. This should be less of an example of how to use the table and more of a seed to add onto, so the less someone would have to change/delete the better

@@ -0,0 +1,38 @@
<div class="mat-elevation-z8">
<mat-table #table [dataSource]="dataSource" matSort>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should ensure that the a11y for this template is really good, including placeholder aria labels where appropriate (I think the mat-table element likely needs one)


<mat-paginator #paginator
[length]="20"
[pageIndex]="0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The length should come from the data, no?

[length]="20"
[pageIndex]="0"
[pageSize]="10"
[pageSizeOptions]="[5, 10, 25, 100]">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make the sizes [25, 50, 100, 250], which feels more realistic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to encourage sometime to load 250 into view?

`,<% } else { %>
templateUrl: './<%= dasherize(name) %>.component.html',<% } if(inlineStyle) { %>
styles: []<% } else { %>
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the !! necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't write this, I copied the code from the default component template for this section.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't mean it shouldn't be corrected.

export class <%= classify(name) %>Component implements OnInit {
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
displayedColumns = ['position', 'name', 'weight', 'symbol'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a description to this like

/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */

/**
* Adds the required modules to the relative module.
*/
function addNavModulesToModule(options: Schema) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nav Modules?

*/
export default function(options: Schema): Rule {
return chain([
buildComponent({ ...options }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omit spaces in braces.

const tree = runner.runSchematic('materialTable', { ...options }, createTestApp());
const files = tree.files;

expect(files.indexOf('/src/app/foo/foo.component.css')).toBeGreaterThanOrEqual(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to use the toContain matcher:

expect(files).toContain('/src/app/foo/foo.component.css');
...

<mat-cell *matCellDef="let element">{{element.weight}}</mat-cell>
</ng-container>

<!-- Color Column -->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "color", but the column is "symbol".

it('should create', () => {
expect(component).toBeTruthy();
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a newline at the end of the file.

let component: <%= classify(name) %>Component;
let fixture: ComponentFixture<<%= classify(name) %>Component>;

beforeEach(async(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be fakeAsync, instead of async? We've moved over most of our components to be fakeAsync.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copied from the base component schematic.

fixture.detectChanges();
});

it('should create', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name sounds off. Maybe something along the lines of should compile?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copied from the base component schematic.

.compileComponents();
}));

beforeEach(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can combine these two beforeEach calls into one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copied from the base component schematic.

}));
}

disconnect() {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there should be an example of what cleaning up here would look like, or a comment that says something like "Don't forget to clean up!".


getSortedData(data) {
if (!this._sort.active || this._sort.direction === '') { return data; }
return data.sort((a, b) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing an extra space above the return. Also switching the if to multiple lines.

return data.sort((a, b) => {
const propertyA = a[this._sort.active];
const propertyB = b[this._sort.active];
const valueA = isNaN(+propertyA) ? propertyA : +propertyA;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are also supposed to serve as an example, it might be more appropriate to use something more readable like parseFloat(property) || 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to different implementation.

<mat-cell *matCellDef="let element">{{element.weight}}</mat-cell>
</ng-container>

<!-- Color Column -->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The column is actually "symbol".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to different property.

},
"flat": {
"type": "boolean",
"description": "Flag to indicate if a dir is created.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's clear what a dir means in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copied from the base component schematic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't mean we can't make it better for ourselves

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree but I wanted to keep this as close to the original as possible so when they support JSON schema inheritance, we can just do away with this file completely.

# Conflicts:
#	schematics/collection.json
#	schematics/tsconfig.json
@amcdnl
Copy link
Contributor Author

amcdnl commented Feb 28, 2018

@jelbourn - Addressed all feedback minus the client/server datasource examples. Will address that in another round.

`,<% } else { %>
templateUrl: './<%= dasherize(name) %>.component.html',<% } if(inlineStyle) { %>
styles: []<% } else { %>
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't mean it shouldn't be corrected.

},
"flat": {
"type": "boolean",
"description": "Flag to indicate if a dir is created.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't mean we can't make it better for ourselves

<mat-paginator #paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="10"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compromise on 50? 10 is just too small to ever be realistic.

import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators/map';
import { MatPaginator, MatSort } from '@angular/material';
import { DataSource } from '@angular/cdk/collections';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should be consistent with the rest of the CLI even if spaces in braces is nonsense.

import { map } from 'rxjs/operators/map';
import { MatPaginator, MatSort } from '@angular/material';
import { DataSource } from '@angular/cdk/collections';
import { of } from 'rxjs/observable/of';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import {of as observableOf} from 'rxjs/observable/of';

Since of as a function is confusing with for of

}));
}

disconnect() {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment inside the empty disconnect like

// TODO: clean up any open connections, free any held resources, etc.


disconnect() {}

private getPagedData(data) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add method description

return data.splice(startIndex, this._paginator.pageSize);
}

private getSortedData(data) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add method description

super();
}

connect(): Observable<Element[]> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return value should be Observable<DataItem>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did one better and did: <%= classify(name) %>Item[]

{id: 20, name: 'Calcium'},
];

constructor(private _paginator: MatPaginator, private _sort: MatSort) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about this, and I'm thinking it would be good to decouple the data source and the sorter/paginator. This would look something like the data source having its own sort and pagination state, and then using the outputs on the directives to update the data source state.

My main reason for wanting to do this is to not make users think they must use those components.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya, I can see that but it wouldn't be near as clean then. What about a comment instead? Best way I could think of would be to:

this.dataSource = new DataSource({
  pageIndex: this.paginator.pageIndex,
  pageSize: this.paginator.pageSize
}, {
  active: this.sort.active,
  direction: this.sort.direction
});

@amcdnl
Copy link
Contributor Author

amcdnl commented Mar 4, 2018

@jelbourn - Addressed feedback.

import { map } from 'rxjs/operators/map';
import { Observable } from 'rxjs/Observable';

/** TODO: Replace this with your own data model type */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch to // style comments to be consistent with other TODOs


/**
* Client-side page the data by slicing out the next from the data array.
* If you are using external datasource for pagination, you would connect it here.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rephrase this as:

Paginate the data (client-side). If you're using server-side pagination,
this would be replaced by requesting the appropriate data from the server.


/**
* Client-side sort the data array.
* If you are using a external datasource for sorting, you would connect it here
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rephrase this as:

Sort the data (client-side). If you're using server-side sorting,
this would be replaced by requesting the appropriate data from the server.

}
}

/** Simple sort comparator for example ID/Name columns. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/** Simple sort comparator for example ID/Name columns (for client-side sorting). */

{id: 20, name: 'Calcium'},
];

export class <%= classify(name) %>DataSource extends DataSource<<%= classify(name) %>Item> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a class description like

Data source for the xxx view. This class should encapsulate all logic for fetching and
manipulating the displayed data (including sorting, pagination, and filtering).

Copy link
Contributor

@andrewseguin andrewseguin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty cool stuff, I like that we can use schematics for this case

export class <%= classify(name) %>DataSource extends DataSource<<%= classify(name) %>Item> {
data: <%= classify(name) %>Item[] = EXAMPLE_DATA;

constructor(private _paginator: MatPaginator, private _sort: MatSort) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though we use underscores for private variables in Material, I believe in this case we should follow the standard Angular-way:

Avoid prefixing private properties and methods with an underscore.
https://angular.io/guide/styleguide#style-03-04

/**
* Connect this data source to the table. The table will only update when
* the returned stream emits new items.
* @returns A stream of the items to be rendered.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do other schematics use jsdoc format (e.g. @returns)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hansl ^^

}

disconnect() {
// TODO: clean up any open connections, free any held resources, etc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO would make me believe I must do something here. Rather than TODO, maybe leave some jsdoc for the function.

/**
 * Called when the table is being destroyed. Use this function to clean up any open connections or free
 * any held resources that were set up during connect.
 */
disconnect() { }

<!-- Id Column -->
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
<mat-cell *matCellDef="let row">{{row.id}}</mat-cell>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make it easier for users to know what can be renamed (e.g. row)? Perhaps calling the data element instead of row?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like row better because its a bit more generic. First thing I'm gonna do when I download this is rename that, if its row I'm less likely to want to do this.

@amcdnl
Copy link
Contributor Author

amcdnl commented Mar 8, 2018

@jelbourn - Ready for another review.

Copy link
Member

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jelbourn jelbourn added pr: lgtm action: merge The PR is ready for merge by the caretaker labels Mar 8, 2018
@jelbourn jelbourn merged commit e7533a5 into angular:master Mar 8, 2018
@jelbourn jelbourn added the target: major This PR is targeted for the next major release label Mar 8, 2018
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 8, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker cla: yes PR author has agreed to Google's Contributor License Agreement target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants