github.com/thomasobenaus/nomad@v0.11.1/ui/stories/components/table.stories.js (about) 1 import hbs from 'htmlbars-inline-precompile'; 2 import productMetadata from '../../app/utils/styleguide/product-metadata'; 3 4 import EmberObject, { computed } from '@ember/object'; 5 6 import { getOwner } from '@ember/application'; 7 import { on } from '@ember/object/evented'; 8 import Controller from '@ember/controller'; 9 10 export default { 11 title: 'Components|Table', 12 }; 13 14 /** 15 * The Ember integration for Storybook renders a container component with no routing, 16 * which means things that need query parameters, like sorting and pagination, won’t work. 17 18 * This initialiser turns on routing and accepts a controller definition that gets wired up 19 * to a generated `storybook` route. The controller is attached to the Storybook component 20 * as the `controller` property so its query parameters are accessible from the template. 21 */ 22 function injectRoutedController(controllerClass) { 23 return on('init', function() { 24 let container = getOwner(this); 25 container.register('controller:storybook', controllerClass); 26 27 let routerFactory = container.factoryFor('router:main'); 28 routerFactory.class.map(function() { 29 this.route('storybook'); 30 }); 31 32 let router = container.lookup('router:main'); 33 router.initialURL = 'storybook'; 34 router.startRouting(true); 35 36 this.set('controller', container.lookup('controller:storybook')); 37 }); 38 } 39 40 let longList = [ 41 { city: 'New York', growth: 0.048, population: '8405837', rank: '1', state: 'New York' }, 42 { city: 'Los Angeles', growth: 0.048, population: '3884307', rank: '2', state: 'California' }, 43 { city: 'Chicago', growth: -0.061, population: '2718782', rank: '3', state: 'Illinois' }, 44 { city: 'Houston', growth: 0.11, population: '2195914', rank: '4', state: 'Texas' }, 45 { 46 city: 'Philadelphia', 47 growth: 0.026, 48 population: '1553165', 49 rank: '5', 50 state: 'Pennsylvania', 51 }, 52 { city: 'Phoenix', growth: 0.14, population: '1513367', rank: '6', state: 'Arizona' }, 53 { city: 'San Antonio', growth: 0.21, population: '1409019', rank: '7', state: 'Texas' }, 54 { city: 'San Diego', growth: 0.105, population: '1355896', rank: '8', state: 'California' }, 55 { city: 'Dallas', growth: 0.056, population: '1257676', rank: '9', state: 'Texas' }, 56 { city: 'San Jose', growth: 0.105, population: '998537', rank: '10', state: 'California' }, 57 { city: 'Austin', growth: 0.317, population: '885400', rank: '11', state: 'Texas' }, 58 { city: 'Indianapolis', growth: 0.078, population: '843393', rank: '12', state: 'Indiana' }, 59 { city: 'Jacksonville', growth: 0.143, population: '842583', rank: '13', state: 'Florida' }, 60 { 61 city: 'San Francisco', 62 growth: 0.077, 63 population: '837442', 64 rank: '14', 65 state: 'California', 66 }, 67 { city: 'Columbus', growth: 0.148, population: '822553', rank: '15', state: 'Ohio' }, 68 { 69 city: 'Charlotte', 70 growth: 0.391, 71 population: '792862', 72 rank: '16', 73 state: 'North Carolina', 74 }, 75 { city: 'Fort Worth', growth: 0.451, population: '792727', rank: '17', state: 'Texas' }, 76 { city: 'Detroit', growth: -0.271, population: '688701', rank: '18', state: 'Michigan' }, 77 { city: 'El Paso', growth: 0.194, population: '674433', rank: '19', state: 'Texas' }, 78 { city: 'Memphis', growth: -0.053, population: '653450', rank: '20', state: 'Tennessee' }, 79 { city: 'Seattle', growth: 0.156, population: '652405', rank: '21', state: 'Washington' }, 80 { city: 'Denver', growth: 0.167, population: '649495', rank: '22', state: 'Colorado' }, 81 { 82 city: 'Washington', 83 growth: 0.13, 84 population: '646449', 85 rank: '23', 86 state: 'District of Columbia', 87 }, 88 { city: 'Boston', growth: 0.094, population: '645966', rank: '24', state: 'Massachusetts' }, 89 { 90 city: 'Nashville-Davidson', 91 growth: 0.162, 92 population: '634464', 93 rank: '25', 94 state: 'Tennessee', 95 }, 96 { city: 'Baltimore', growth: -0.04, population: '622104', rank: '26', state: 'Maryland' }, 97 { city: 'Oklahoma City', growth: 0.202, population: '610613', rank: '27', state: 'Oklahoma' }, 98 { 99 city: 'Louisville/Jefferson County', 100 growth: 0.1, 101 population: '609893', 102 rank: '28', 103 state: 'Kentucky', 104 }, 105 { city: 'Portland', growth: 0.15, population: '609456', rank: '29', state: 'Oregon' }, 106 { city: 'Las Vegas', growth: 0.245, population: '603488', rank: '30', state: 'Nevada' }, 107 { city: 'Milwaukee', growth: 0.003, population: '599164', rank: '31', state: 'Wisconsin' }, 108 { city: 'Albuquerque', growth: 0.235, population: '556495', rank: '32', state: 'New Mexico' }, 109 { city: 'Tucson', growth: 0.075, population: '526116', rank: '33', state: 'Arizona' }, 110 { city: 'Fresno', growth: 0.183, population: '509924', rank: '34', state: 'California' }, 111 { city: 'Sacramento', growth: 0.172, population: '479686', rank: '35', state: 'California' }, 112 { city: 'Long Beach', growth: 0.015, population: '469428', rank: '36', state: 'California' }, 113 { city: 'Kansas City', growth: 0.055, population: '467007', rank: '37', state: 'Missouri' }, 114 { city: 'Mesa', growth: 0.135, population: '457587', rank: '38', state: 'Arizona' }, 115 { city: 'Virginia Beach', growth: 0.051, population: '448479', rank: '39', state: 'Virginia' }, 116 { city: 'Atlanta', growth: 0.062, population: '447841', rank: '40', state: 'Georgia' }, 117 { 118 city: 'Colorado Springs', 119 growth: 0.214, 120 population: '439886', 121 rank: '41', 122 state: 'Colorado', 123 }, 124 { city: 'Omaha', growth: 0.059, population: '434353', rank: '42', state: 'Nebraska' }, 125 { city: 'Raleigh', growth: 0.487, population: '431746', rank: '43', state: 'North Carolina' }, 126 { city: 'Miami', growth: 0.149, population: '417650', rank: '44', state: 'Florida' }, 127 { city: 'Oakland', growth: 0.013, population: '406253', rank: '45', state: 'California' }, 128 { city: 'Minneapolis', growth: 0.045, population: '400070', rank: '46', state: 'Minnesota' }, 129 { city: 'Tulsa', growth: 0.013, population: '398121', rank: '47', state: 'Oklahoma' }, 130 { city: 'Cleveland', growth: -0.181, population: '390113', rank: '48', state: 'Ohio' }, 131 { city: 'Wichita', growth: 0.097, population: '386552', rank: '49', state: 'Kansas' }, 132 { city: 'Arlington', growth: 0.133, population: '379577', rank: '50', state: 'Texas' }, 133 ]; 134 135 export let Standard = () => { 136 return { 137 template: hbs` 138 <h5 class="title is-5">Table</h5> 139 <ListTable @source={{shortList}} as |t|> 140 <t.head> 141 <th>Name</th> 142 <th>Language</th> 143 <th>Description</th> 144 </t.head> 145 <t.body @key="model.name" as |row|> 146 <tr> 147 <td>{{row.model.name}}</td> 148 <td>{{row.model.lang}}</td> 149 <td>{{row.model.desc}}</td> 150 </tr> 151 </t.body> 152 </ListTable> 153 <p class="annotation">Tables have airy designs with a minimal amount of borders. This maximizes their utility.</p> 154 `, 155 context: { 156 shortList: productMetadata, 157 }, 158 }; 159 }; 160 161 export let Search = () => { 162 return { 163 template: hbs` 164 <h5 class="title is-5">Table search</h5> 165 <div class="boxed-section"> 166 <div class="boxed-section-head"> 167 Table Name 168 <SearchBox 169 @searchTerm={{mut controller.searchTerm}} 170 @placeholder="Search..." 171 @class="is-inline pull-right" 172 @inputClass="is-compact" /> 173 </div> 174 <div class="boxed-section-body {{if controller.filteredShortList.length "is-full-bleed"}}"> 175 {{#if controller.filteredShortList.length}} 176 <ListTable @source={{controller.filteredShortList}} as |t|> 177 <t.head> 178 <th>Name</th> 179 <th>Language</th> 180 <th>Description</th> 181 </t.head> 182 <t.body @key="model.name" as |row|> 183 <tr> 184 <td>{{row.model.name}}</td> 185 <td>{{row.model.lang}}</td> 186 <td>{{row.model.desc}}</td> 187 </tr> 188 </t.body> 189 </ListTable> 190 {{else}} 191 <div class="empty-message"> 192 <h3 class="empty-message-headline">No Matches</h3> 193 <p class="empty-message-body">No products match your query.</p> 194 </div> 195 {{/if}} 196 </div> 197 </div> 198 <p class="annotation">Tables compose with boxed-section and boxed-section composes with search box.</p> 199 `, 200 context: { 201 controller: EmberObject.extend({ 202 searchTerm: '', 203 204 filteredShortList: computed('searchTerm', function() { 205 let term = this.searchTerm.toLowerCase(); 206 return productMetadata.filter(product => product.name.toLowerCase().includes(term)); 207 }), 208 }).create(), 209 }, 210 }; 211 }; 212 213 export let SortableColumns = () => { 214 return { 215 template: hbs` 216 <h5 class="title is-5">Table with sortable columns</h5> 217 <ListTable @source={{sortedShortList}} @sortProperty={{controller.sortProperty}} @sortDescending={{controller.sortDescending}} as |t|> 218 <t.head> 219 <t.sort-by @prop="name">Name</t.sort-by> 220 <t.sort-by @prop="lang" @class="is-2">Language</t.sort-by> 221 <th>Description</th> 222 </t.head> 223 <t.body @key="model.name" as |row|> 224 <tr> 225 <td>{{row.model.name}}</td> 226 <td>{{row.model.lang}}</td> 227 <td>{{row.model.desc}}</td> 228 </tr> 229 </t.body> 230 </ListTable> 231 <p class="annotation">The list-table component provides a <code>sort-by</code> contextual component for building <code>link-to</code> components with the appropriate query params.</p> 232 <p class="annotation">This leaves the component stateless, relying on data to be passed down and sending actions back up via the router (via link-to).</p> 233 `, 234 context: { 235 injectRoutedController: injectRoutedController( 236 Controller.extend({ 237 queryParams: ['sortProperty', 'sortDescending'], 238 sortProperty: 'name', 239 sortDescending: false, 240 }) 241 ), 242 243 sortedShortList: computed('controller.sortProperty', 'controller.sortDescending', function() { 244 let sorted = productMetadata.sortBy(this.get('controller.sortProperty') || 'name'); 245 return this.get('controller.sortDescending') ? sorted.reverse() : sorted; 246 }), 247 }, 248 }; 249 }; 250 251 export let MultiRow = () => { 252 return { 253 template: hbs` 254 <h5 class="title is-5">Multi-row Table</h5> 255 <ListTable @source={{sortedShortList}} @sortProperty={{controller.sortProperty}} @sortDescending={{controller.sortDescending}} @class="is-striped" as |t|> 256 <t.head> 257 <t.sort-by @prop="name">Name</t.sort-by> 258 <t.sort-by @prop="lang">Language</t.sort-by> 259 </t.head> 260 <t.body @key="model.name" as |row|> 261 <tr> 262 <td>{{row.model.name}}</td> 263 <td>{{row.model.lang}}</td> 264 </tr> 265 <tr> 266 <td colspan="2">{{row.model.desc}}</td> 267 </tr> 268 </t.body> 269 </ListTable> 270 <p class="annotation">The list-table component attempts to be as flexible as possible. For this reason, <code>t.body</code> does not provide the typical <code>tr</code> element. It's sometimes desired to have multiple elements per record.</p> 271 `, 272 context: { 273 injectRoutedController: injectRoutedController( 274 Controller.extend({ 275 queryParams: ['sortProperty', 'sortDescending'], 276 sortProperty: 'name', 277 sortDescending: false, 278 }) 279 ), 280 281 sortedShortList: computed('controller.sortProperty', 'controller.sortDescending', function() { 282 let sorted = productMetadata.sortBy(this.get('controller.sortProperty') || 'name'); 283 return this.get('controller.sortDescending') ? sorted.reverse() : sorted; 284 }), 285 }, 286 }; 287 }; 288 289 export let Pagination = () => { 290 return { 291 template: hbs` 292 <h5 class="title is-5">Table pagination</h5> 293 <ListPagination @source={{longList}} @size={{5}} @page={{controller.currentPage}} as |p|> 294 <ListTable @source={{p.list}} @class="with-foot" as |t|> 295 <t.head> 296 <th class="is-1">Rank</th> 297 <th>City</th> 298 <th>State</th> 299 <th>Population</th> 300 <th>Growth</th> 301 </t.head> 302 <t.body @key="model.rank" as |row|> 303 <tr> 304 <td>{{row.model.rank}}</td> 305 <td>{{row.model.city}}</td> 306 <td>{{row.model.state}}</td> 307 <td>{{row.model.population}}</td> 308 <td>{{format-percentage row.model.growth total=1}}</td> 309 </tr> 310 </t.body> 311 </ListTable> 312 <div class="table-foot"> 313 <nav class="pagination"> 314 <span class="bumper-left">U.S. City population and growth from 2000-2013</span> 315 <div class="pagination-numbers"> 316 {{p.startsAt}}–{{p.endsAt}} of {{longList.length}} 317 </div> 318 <p.prev @class="pagination-previous"> < </p.prev> 319 <p.next @class="pagination-next"> > </p.next> 320 <ul class="pagination-list"></ul> 321 </nav> 322 </div> 323 </ListPagination> 324 <p class="annotation">Pagination works like sorting: using <code>link-to</code>s to set a query param.</p> 325 <p class="annotation">Pagination, like Table, is a minimal design. Only a next and previous button are available. The current place in the set of pages is tracked by showing which slice of items is currently shown.</p> 326 <p class="annotation">The pagination component exposes first and last components (for jumping to the beginning and end of a list) as well as pageLinks for generating links around the current page.</p> 327 `, 328 context: { 329 injectRoutedController: injectRoutedController( 330 Controller.extend({ 331 queryParams: ['currentPage'], 332 currentPage: 1, 333 }) 334 ), 335 longList, 336 }, 337 }; 338 }; 339 340 export let RowLinks = () => { 341 return { 342 template: hbs` 343 <h5 class="title is-5">Table row links</h5> 344 <ListTable @source={{shortList}} as |t|> 345 <t.head> 346 <th>Name</th> 347 <th>Language</th> 348 <th>Description</th> 349 </t.head> 350 <t.body @key="model.name" as |row|> 351 <tr class="is-interactive"> 352 <td><a href="javascript:;" class="is-primary">{{row.model.name}}</a></td> 353 <td>{{row.model.lang}}</td> 354 <td>{{row.model.desc}}</td> 355 </tr> 356 </t.body> 357 </ListTable> 358 <p class="annotation">It is common for tables to act as lists of links, (e.g., clients list all allocations, each row links to the allocation detail). The helper class <code>is-interactive</code> on the <code>tr</code> makes table rows have a pointer cursor. The helper class <code>is-primary</code> on the <code>a</code> element in a table row makes the link bold and black instead of blue. This makes the link stand out less, since the entire row is a link.</p> 359 <p class="annotation"> 360 A few rules for using table row links: 361 <ol> 362 <li>The <code>is-primary</code> cell should always be the first cell</li> 363 <li>The <code>is-primary</code> cell should always contain a link to the destination in the form of an <code>a</code> element. This is to support opening a link in a new tab.</li> 364 <li>The full row should transition to the destination on click. This is to improve the usability of a table by creating a larger click area.</li> 365 </ol> 366 </p> 367 `, 368 context: { 369 shortList: productMetadata, 370 }, 371 }; 372 }; 373 374 export let CellLinks = () => { 375 return { 376 template: hbs` 377 <h5 class="title is-5">Table cell links</h5> 378 <ListTable @source={{shortList}} as |t|> 379 <t.head> 380 <th>Name</th> 381 <th>Language</th> 382 <th>Description</th> 383 </t.head> 384 <t.body @key="model.name" as |row|> 385 <tr> 386 <td><a href={{row.model.link}} target="_parent">{{row.model.name}}</a></td> 387 <td>{{row.model.lang}}</td> 388 <td>{{row.model.desc}}</td> 389 </tr> 390 </t.body> 391 </ListTable> 392 <p class="annotation">Links in table cells are just links.</p> 393 `, 394 context: { 395 shortList: productMetadata, 396 }, 397 }; 398 }; 399 400 export let CellDecorations = () => { 401 return { 402 template: hbs` 403 <h5 class="title is-5">Table cell decorations</h5> 404 <ListTable @source={{shortList}} as |t|> 405 <t.head> 406 <th>Name</th> 407 <th>Language</th> 408 <th>Description</th> 409 </t.head> 410 <t.body @key="model.name" as |row|> 411 <tr> 412 <td><a href={{row.model.link}}>{{row.model.name}}</a></td> 413 <td class="nowrap"> 414 <span class="color-swatch 415 {{if (eq row.model.lang "ruby") "swatch-6"}} 416 {{if (eq row.model.lang "golang") "swatch-5"}}" /> 417 {{row.model.lang}} 418 </td> 419 <td>{{row.model.desc}}</td> 420 </tr> 421 </t.body> 422 </ListTable> 423 <p class="annotation">Small icons and accents of color make tables easier to scan.</p> 424 `, 425 context: { 426 shortList: productMetadata, 427 }, 428 }; 429 }; 430 431 export let CellIcons = () => { 432 return { 433 template: hbs` 434 <h5 class="title is-5">Table cell icons</h5> 435 <ListPagination @source={{longList}} @size={{5}} @page={{controller.currentPage}} as |p|> 436 <ListTable @source={{p.list}} @class="with-foot" as |t|> 437 <t.head> 438 <th class="is-narrow"></th> 439 <th class="is-1">Rank</th> 440 <th>City</th> 441 <th>State</th> 442 <th>Population</th> 443 <th>Growth</th> 444 </t.head> 445 <t.body @key="model.rank" as |row|> 446 <tr> 447 <td class="is-narrow"> 448 {{#if (lt row.model.growth 0)}} 449 {{x-icon "warning" class="is-warning"}} 450 {{/if}} 451 </td> 452 <td>{{row.model.rank}}</td> 453 <td>{{row.model.city}}</td> 454 <td>{{row.model.state}}</td> 455 <td>{{row.model.population}}</td> 456 <td>{{format-percentage row.model.growth total=1}}</td> 457 </tr> 458 </t.body> 459 </ListTable> 460 <div class="table-foot"> 461 <nav class="pagination"> 462 <span class="bumper-left">U.S. City population and growth from 2000-2013. Cities with negative growth denoted.</span> 463 <div class="pagination-numbers"> 464 {{p.startsAt}}–{{p.endsAt}} of {{longList.length}} 465 </div> 466 <p.prev @class="pagination-previous"> < </p.prev> 467 <p.next @class="pagination-next"> > </p.next> 468 <ul class="pagination-list"></ul> 469 </nav> 470 </div> 471 </ListPagination> 472 `, 473 context: { 474 injectRoutedController: injectRoutedController( 475 Controller.extend({ 476 queryParams: ['currentPage'], 477 currentPage: 1, 478 }) 479 ), 480 longList, 481 }, 482 }; 483 };