-
Notifications
You must be signed in to change notification settings - Fork 6.8k
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
Conversation
schematics/table/README.md
Outdated
@@ -0,0 +1,5 @@ | |||
# Table | |||
Creates a table component with sorting and pager. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<div class="mat-elevation-z8"> | ||
<mat-table #table [dataSource]="dataSource" matSort> | ||
|
||
<!-- Position Column --> |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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]"> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { %>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the !!
necessary?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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']; |
There was a problem hiding this comment.
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. */
schematics/table/index.ts
Outdated
/** | ||
* Adds the required modules to the relative module. | ||
*/ | ||
function addNavModulesToModule(options: Schema) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nav Modules?
schematics/table/index.ts
Outdated
*/ | ||
export default function(options: Schema): Rule { | ||
return chain([ | ||
buildComponent({ ...options }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Omit spaces in braces.
schematics/table/index_spec.ts
Outdated
const tree = runner.runSchematic('materialTable', { ...options }, createTestApp()); | ||
const files = tree.files; | ||
|
||
expect(files.indexOf('/src/app/foo/foo.component.css')).toBeGreaterThanOrEqual(0); |
There was a problem hiding this comment.
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 --> |
There was a problem hiding this comment.
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(); | ||
}); | ||
}); |
There was a problem hiding this comment.
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(() => { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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(() => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() {} |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 --> |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
@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) { %>, |
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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() {} |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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[]> { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
});
@jelbourn - Addressed feedback. |
import { map } from 'rxjs/operators/map'; | ||
import { Observable } from 'rxjs/Observable'; | ||
|
||
/** TODO: Replace this with your own data model type */ |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. */ |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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).
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
)?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
@jelbourn - Ready for another review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
This PR creates a table schematic.
Source: https://github.com/amcdnl/material-example-schematics/tree/schematic-table