├── vendor └── .gitkeep ├── tests ├── helpers │ └── .gitkeep ├── integration │ ├── .gitkeep │ └── components │ │ ├── example-1 │ │ └── component-test.js │ │ └── aspect-ratio-test.js ├── test-helper.js └── index.html ├── .node-version ├── .watchmanconfig ├── public └── robots.txt ├── tailwind.config.js ├── .template-lintrc.js ├── postcss.config.js ├── app ├── components │ ├── final │ │ ├── text.hbs │ │ ├── link.hbs │ │ ├── aspect-ratio.hbs │ │ ├── aspect-ratio.js │ │ └── link.js │ └── exercises │ │ ├── exercise-13 │ │ ├── exercise.js │ │ ├── solution.js │ │ ├── solution.hbs │ │ └── exercise.hbs │ │ ├── exercise-2 │ │ ├── exercise.hbs │ │ └── solution.hbs │ │ ├── exercise-8 │ │ ├── exercise.js │ │ ├── solution.js │ │ ├── solution.hbs │ │ └── exercise.hbs │ │ ├── exercise-5 │ │ ├── exercise.hbs │ │ └── solution.hbs │ │ ├── exercise-9 │ │ ├── exercise.hbs │ │ └── solution.hbs │ │ ├── exercise-10 │ │ ├── exercise.hbs │ │ └── solution.hbs │ │ ├── exercise-3 │ │ ├── exercise.hbs │ │ └── solution.hbs │ │ ├── exercise-4 │ │ ├── exercise.hbs │ │ └── solution.hbs │ │ ├── exercise-12 │ │ ├── exercise.hbs │ │ └── solution.hbs │ │ ├── exercise-7 │ │ ├── exercise.hbs │ │ └── solution.hbs │ │ ├── exercise-1 │ │ ├── exercise.hbs │ │ └── solution.hbs │ │ ├── exercise-6 │ │ ├── solution.hbs │ │ └── exercise.hbs │ │ └── exercise-11 │ │ ├── exercise.hbs │ │ └── solution.hbs ├── routes │ ├── index.js │ └── exercise.js ├── styles │ ├── app.css │ └── tailwind.config.js ├── app.js ├── router.js ├── templates │ ├── exercise.hbs │ └── application.hbs ├── index.html └── controllers │ └── application.js ├── jsconfig.json ├── config ├── optional-features.json ├── targets.js └── environment.js ├── .ember-cli ├── .eslintignore ├── .editorconfig ├── .gitignore ├── .travis.yml ├── testem.js ├── ember-cli-build.js ├── .eslintrc.js ├── package.json └── README.md /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 12.16.0 2 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./app/styles/tailwind.config.js"); 2 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'octane' 5 | }; 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("tailwindcss"), require("autoprefixer")] 3 | }; 4 | -------------------------------------------------------------------------------- /app/components/final/text.hbs: -------------------------------------------------------------------------------- 1 |

2 | {{yield}} 3 |

-------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | {"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]} -------------------------------------------------------------------------------- /app/components/final/link.hbs: -------------------------------------------------------------------------------- 1 | 6 | {{yield}} 7 | -------------------------------------------------------------------------------- /app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class extends Route { 4 | beforeModel() { 5 | this.transitionTo("exercise", "exercise-1"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/components/final/aspect-ratio.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{yield}} 4 |
5 |
-------------------------------------------------------------------------------- /config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /app/routes/exercise.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class extends Route { 4 | model({ exercise_slug }) { 5 | return { 6 | exerciseNumber: exercise_slug.replace("exercise-", "") 7 | }; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /app/styles/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | nav.app a.active { 6 | @apply text-white bg-gray-900; 7 | } 8 | nav.app a:not(.active) { 9 | @apply text-gray-300; 10 | } 11 | nav.app a:hover:not(.active) { 12 | @apply text-white bg-gray-700; 13 | } 14 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /app/styles/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | 3 | module.exports = { 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | sans: ["Inter var", ...defaultTheme.fontFamily.sans] 8 | } 9 | } 10 | }, 11 | 12 | plugins: [require("@tailwindcss/ui")] 13 | }; 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | 17 | # ember-try 18 | /.node_modules.ember-try/ 19 | /bower.json.ember-try 20 | /package.json.ember-try 21 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Application from "@ember/application"; 2 | import Resolver from "ember-resolver"; 3 | import loadInitializers from "ember-load-initializers"; 4 | import config from "./config/environment"; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | Resolver = Resolver; 9 | } 10 | 11 | loadInitializers(App, config.modulePrefix); 12 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-13/exercise.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { action } from "@ember/object"; 3 | import { tracked } from "@glimmer/tracking"; 4 | 5 | export default class ExercisesExercise14SolutionComponent extends Component { 6 | @tracked isOpen = false; 7 | 8 | @action 9 | toggleIsOpen() { 10 | this.isOpen = !this.isOpen; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /app/components/final/aspect-ratio.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { htmlSafe } from "@ember/string"; 3 | 4 | export default class AspectRatioComponent extends Component { 5 | get style() { 6 | let paddingBottom = this.args.ratio 7 | .split(":") 8 | .map(str => +str) 9 | .reduce((prev, curr) => curr / prev); 10 | 11 | return htmlSafe(`padding-bottom: ${paddingBottom * 100}%`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-13/solution.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { action } from "@ember/object"; 3 | import { tracked } from "@glimmer/tracking"; 4 | 5 | export default class ExercisesExercise14SolutionComponent extends Component { 6 | @tracked isOpen = false; 7 | 8 | links = ["Dashboard", "Team", "Projects", "Calendar"]; 9 | 10 | @action 11 | toggleIsOpen() { 12 | this.isOpen = !this.isOpen; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from "@ember/routing/router"; 2 | import config from "./config/environment"; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function() { 10 | this.route("exercise", { path: "/:exercise_slug" }, function() { 11 | this.route("child-1"); 12 | this.route("child-2"); 13 | this.route("child-3"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /connect.lock 16 | /coverage/ 17 | /libpeerconnection.log 18 | /npm-debug.log* 19 | /testem.log 20 | /yarn-error.log 21 | 22 | # ember-try 23 | /.node_modules.ember-try/ 24 | /bower.json.ember-try 25 | /package.json.ember-try 26 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-2/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 |
8 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia odit 9 | quod ab, asperiores, praesentium veniam. 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-2/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 |
8 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia odit quod ab, asperiores, praesentium veniam. 9 |
10 |
11 |
-------------------------------------------------------------------------------- /app/components/final/link.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { inject as service } from "@ember/service"; 3 | 4 | export default class extends Component { 5 | @service router; 6 | 7 | get isActive() { 8 | let routerArgs = [this.args.route]; 9 | if (this.args.models) { 10 | routerArgs = [...routerArgs, ...this.args.models]; 11 | } 12 | 13 | let url = this.router.urlFor(routerArgs); 14 | 15 | return this.router.currentURL.startsWith(url); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/templates/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{component 5 | (concat "exercises/exercise-" @model.exerciseNumber "/exercise") 6 | }} 7 |
8 |
9 |
10 |
11 | {{component 12 | (concat "exercises/exercise-" @model.exerciseNumber "/solution") 13 | }} 14 |
15 |
16 |
-------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "10" 5 | 6 | dist: trusty 7 | 8 | addons: 9 | chrome: stable 10 | 11 | cache: 12 | yarn: true 13 | 14 | env: 15 | global: 16 | # See https://git.io/vdao3 for details. 17 | - JOBS=1 18 | 19 | branches: 20 | only: 21 | - master 22 | 23 | before_install: 24 | - curl -o- -L https://yarnpkg.com/install.sh | bash 25 | - export PATH=$HOME/.yarn/bin:$PATH 26 | 27 | install: 28 | - yarn install --non-interactive 29 | 30 | script: 31 | - yarn lint:hbs 32 | - yarn lint:js 33 | - yarn test 34 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_start_timeout: 120, 11 | browser_args: { 12 | Chrome: { 13 | ci: [ 14 | // --no-sandbox is needed when running Chrome inside a container 15 | process.env.CI ? '--no-sandbox' : null, 16 | '--headless', 17 | '--disable-dev-shm-usage', 18 | '--disable-software-rasterizer', 19 | '--mute-audio', 20 | '--remote-debugging-port=0', 21 | '--window-size=1440,900' 22 | ].filter(Boolean) 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-8/exercise.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | export default class ExercisesExercise8Component extends Component { 4 | items = [ 5 | { title: "Full name", data: "Margot Foster" }, 6 | { title: "Application for", data: "Backend Developer" }, 7 | { title: "Email address", data: "margotfoster@example.com" }, 8 | { title: "Salary expectation", data: "$120,000" }, 9 | { 10 | title: "About", 11 | data: 12 | "Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu." 13 | } 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-8/solution.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | export default class ExercisesExercise8Component extends Component { 4 | items = [ 5 | { title: "Full name", data: "Margot Foster" }, 6 | { title: "Application for", data: "Backend Developer" }, 7 | { title: "Email address", data: "margotfoster@example.com" }, 8 | { title: "Salary expectation", data: "$120,000" }, 9 | { 10 | title: "About", 11 | data: 12 | "Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu." 13 | } 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-5/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 | Woman paying for a purchase 7 |
8 |
9 | Marketing 10 |
11 | 15 | Finding customers for your new business 16 | 17 |

18 | Getting a new business off the ground is a lot of hard work. Here are five ideas you can use to find your first customers. 19 |

20 |
21 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-9/exercise.hbs: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | 20 |
21 | 26 |
27 |
-------------------------------------------------------------------------------- /tests/integration/components/example-1/component-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import { hbs } from 'ember-cli-htmlbars'; 5 | 6 | module('Integration | Component | example-1', function(hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function(assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.set('myAction', function(val) { ... }); 12 | 13 | await render(hbs``); 14 | 15 | assert.equal(this.element.textContent.trim(), ''); 16 | 17 | // Template block usage: 18 | await render(hbs` 19 | 20 | template block text 21 | 22 | `); 23 | 24 | assert.equal(this.element.textContent.trim(), 'template block text'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/integration/components/aspect-ratio-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import { hbs } from 'ember-cli-htmlbars'; 5 | 6 | module('Integration | Component | aspect-ratio', function(hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function(assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.set('myAction', function(val) { ... }); 12 | 13 | await render(hbs``); 14 | 15 | assert.equal(this.element.textContent.trim(), ''); 16 | 17 | // Template block usage: 18 | await render(hbs` 19 | 20 | template block text 21 | 22 | `); 23 | 24 | assert.equal(this.element.textContent.trim(), 'template block text'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-9/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 |
9 | 14 | 19 | 20 |
21 | 26 |
27 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-5/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Woman paying for a purchase 9 |
10 |
11 |
12 | Marketing 13 |
14 | 18 | Finding customers for your new business 19 | 20 |

21 | Getting a new business off the ground is a lot of hard work. Here are five ideas you can use to find your first customers. 22 |

23 |
24 |
25 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-10/exercise.hbs: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | 20 |
21 | 27 |
28 |

29 | Your password must be less than 4 characters. 30 |

31 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-3/exercise.hbs: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 26 |
-------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tailwind CSS Best Practices - EmberConf 2020 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 19 | 20 | {{content-for "head-footer"}} 21 | 22 | 23 | {{content-for "body"}} 24 | 25 | 26 | 27 | 28 | {{content-for "body-footer"}} 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-10/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 12 |
15 | 16 | 21 | 22 |
23 |
24 |

25 | Your password must be less than 4 characters. 26 |

27 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-4/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 | 15 | 21 |
22 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-8/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Applicant Information 6 |

7 |

8 | Personal details and application. 9 |

10 |
11 |
12 |
13 | {{#each items as |item index|}} 14 |
20 |
21 | {{item.title}} 22 |
23 |
26 | {{item.data}} 27 |
28 |
29 | {{/each}} 30 |
31 |
32 |
33 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-4/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 10 | 16 | 22 |
23 |
24 |
-------------------------------------------------------------------------------- /app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { action } from "@ember/object"; 3 | import { tracked } from "@glimmer/tracking"; 4 | import { inject as service } from "@ember/service"; 5 | 6 | export default class ApplicationController extends Controller { 7 | @service router; 8 | @tracked sidebarIsOpen; 9 | 10 | exercisesCount = 13; 11 | 12 | get currentExercise() { 13 | let slug = this.router.currentRoute.parent.params.exercise_slug; 14 | 15 | return slug.replace("exercise-", ""); 16 | } 17 | 18 | get previousExercise() { 19 | return +this.currentExercise > 1 ? +this.currentExercise - 1 : null; 20 | } 21 | 22 | get nextExercise() { 23 | return +this.currentExercise < this.exercisesCount 24 | ? +this.currentExercise + 1 25 | : null; 26 | } 27 | 28 | @action 29 | toggleSidebarIsOpen() { 30 | this.sidebarIsOpen = !this.sidebarIsOpen; 31 | } 32 | 33 | @action 34 | transitionToPreviousExercise() { 35 | this.router.transitionTo(`/exercise-${this.previousExercise}`); 36 | } 37 | 38 | @action 39 | transitionToNextExercise() { 40 | this.router.transitionTo(`/exercise-${this.nextExercise}`); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-3/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 | 28 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-8/exercise.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |
10 |

11 | Applicant Information 12 |

13 |

14 | Personal details and application. 15 |

16 |
17 |
18 |
19 | {{#each items as |item index|}} 20 |
26 |
27 | {{item.title}} 28 |
29 |
30 | {{item.data}} 31 |
32 |
33 | {{/each}} 34 |
35 |
36 |
37 |
-------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tailwind CSS Best Practices - EmberConf 2020 7 | 8 | 9 | 10 | {{content-for "head"}} {{content-for "test-head"}} 11 | 12 | 13 | 17 | 18 | 19 | {{content-for "head-footer"}} {{content-for "test-head-footer"}} 20 | 21 | 22 | {{content-for "body"}} {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} {{content-for "test-body-footer"}} 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-12/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 9 | 15 | 21 | 22 |
-------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const EmberApp = require("ember-cli/lib/broccoli/ember-app"); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberApp(defaults, { 7 | snippetPaths: ["app/exercises"], 8 | "ember-prism": { 9 | plugins: ["normalize-plugins"] 10 | }, 11 | 12 | postcssOptions: { 13 | compile: { 14 | enabled: true, 15 | plugins: [ 16 | require("tailwindcss")("./app/styles/tailwind.config.js"), 17 | require("autoprefixer") 18 | ], 19 | cacheExclude: [], 20 | cacheInclude: [/.*\.(css|scss)$/, /.tailwind\.config\.js$/] 21 | } 22 | } 23 | }); 24 | 25 | // Use `app.import` to add additional libraries to the generated 26 | // output files. 27 | app.import("node_modules/focus-visible/dist/focus-visible.min.js"); 28 | // 29 | // If you need to use different assets in different 30 | // environments, specify an object as the first parameter. That 31 | // object's keys should be the environment name and the values 32 | // should be the asset to use in that environment. 33 | // 34 | // If the library that you are including contains AMD or ES6 35 | // modules that you would like to import into your application 36 | // please specify an object with the list of modules as keys 37 | // along with the exports of each module as its value. 38 | 39 | return app.toTree(); 40 | }; 41 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-12/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 9 | 15 | 21 | 22 |
-------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: "emberconf2020-tailwindcss-best-practices", 6 | environment, 7 | rootURL: "/", 8 | locationType: "auto", 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === "development") { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === "test") { 35 | // Testem prefers this... 36 | ENV.locationType = "none"; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = "#ember-testing"; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === "production") { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-7/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 |

3 | Last 30 days 4 |

5 |
6 |
7 |
8 |
9 |
10 | Total Subscribers 11 |
12 |
13 | 71,897 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Avg. Open Rate 23 |
24 |
25 | 58.16% 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Avg. Click Rate 35 |
36 |
37 | 24.57% 38 |
39 |
40 |
41 |
42 |
43 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-1/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 |

3 | Utility-First 4 |

5 |

6 | Traditionally, whenever you need to style something on the web, you write CSS. With Tailwind, you style elements by applying pre-existing classes directly in your HTML. 7 |

8 |

9 | Once you've actually built something this way, you'll quickly notice some really important benefits: 10 |

11 |
    12 |
  • 13 | You aren't wasting energy inventing class names. 14 |
  • 15 |
  • 16 | Your CSS stops growing. 17 |
  • 18 |
  • 19 | Making changes feels safer. 20 |
  • 21 |
22 |

23 | When you realize how productive you can be working exclusively in HTML with predefined utility classes, working any other way will feel like torture. 24 |

25 |

26 | The biggest maintainability concern when using a utility-first approach is managing commonly repeated utility combinations. This is easily solved by extracting components, either as template partials/JavaScript components, or using Tailwind's @apply feature to create abstractions around common utility patterns. 27 |

28 |

29 | Aside from that, maintaining a utility-first CSS project turns out to be a lot easier than maintaining a large CSS codebase, simply because HTML is so much easier to maintain than CSS. Large companies like GitHub, Heroku, Kickstarter, Twitch, Segment, and more are using this approach with great success. 30 |

31 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-7/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 |

3 | Last 30 days 4 |

5 |
6 |
7 |
8 |
9 |
10 | Total Subscribers 11 |
12 |
13 | 71,897 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Avg. Open Rate 23 |
24 |
25 | 58.16% 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Avg. Click Rate 35 |
36 |
37 | 24.57% 38 |
39 |
40 |
41 |
42 |
43 |
-------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "babel-eslint", 4 | parserOptions: { 5 | ecmaVersion: 2018, 6 | sourceType: "module", 7 | ecmaFeatures: { 8 | legacyDecorators: true 9 | } 10 | }, 11 | plugins: ["ember"], 12 | extends: ["eslint:recommended", "plugin:ember/recommended"], 13 | env: { 14 | browser: true 15 | }, 16 | rules: { 17 | "ember/no-jquery": "error", 18 | "no-unused-vars": ["error", { args: "none" }], 19 | "no-console": "off", 20 | "ember/avoid-leaking-state-in-ember-objects": "off" 21 | }, 22 | overrides: [ 23 | // node files 24 | { 25 | files: [ 26 | ".eslintrc.js", 27 | ".template-lintrc.js", 28 | "ember-cli-build.js", 29 | "tailwind.config.js", 30 | "postcss.config.js", 31 | "testem.js", 32 | "blueprints/*/index.js", 33 | "config/**/*.js", 34 | "lib/*/index.js", 35 | "server/**/*.js" 36 | ], 37 | parserOptions: { 38 | sourceType: "script" 39 | }, 40 | env: { 41 | browser: false, 42 | node: true 43 | }, 44 | plugins: ["node"], 45 | rules: Object.assign( 46 | {}, 47 | require("eslint-plugin-node").configs.recommended.rules, 48 | { 49 | // add your custom rules and overrides for node files here 50 | 51 | // this can be removed once the following is fixed 52 | // https://github.com/mysticatea/eslint-plugin-node/issues/77 53 | "node/no-unpublished-require": "off" 54 | } 55 | ) 56 | } 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-6/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 |

3 | Lowest Common Ancestor 4 |

5 | 6 | 7 | One of the biggest challenges when writing a JavaScript application is keeping multiple parts of the interface in sync. A user interaction in one part of the interface often affects data in another. If not managed well, this data can end up in multiple places, but with inconsistent values. 8 | 9 | 10 |
11 | 15 |
16 | 17 | 18 | In the past few years, the JavaScript community has learned a lot about how to deal with this problem. The solution involves the principle of the Lowest Common Ancestor. To explain this principle, let's look at some interface elements you might build while working on a real-world application. 19 | 20 | 21 | 22 | Let's say we're building an app with a collapsible panel. If the panel isOpen, we'll show the body. Clicking the title triggers the toggleIsOpen action. 23 | 24 | 25 | 26 | Currently, our application looks like this. isOpen is the only piece of state in our application that changes. Further, the collapsible panel is the only part of the interface that needs to know about it. Because of this, it makes sense for the panel itself to "own" this piece of application state. So, we'll leave isOpen right where it is - as a simple property on the component. 27 | 28 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-6/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 |

3 | Lowest Common Ancestor 4 |

5 | 6 |

7 | One of the biggest challenges when writing a JavaScript application is keeping multiple parts of the interface in sync. A user interaction in one part of the interface often affects data in another. If not managed well, this data can end up in multiple places, but with inconsistent values. 8 |

9 | 10 |
11 | 15 |
16 | 17 |

18 | In the past few years, the JavaScript community has learned a lot about how to deal with this problem. The solution involves the principle of the Lowest Common Ancestor. To explain this principle, let's look at some interface elements you might build while working on a real-world application. 19 |

20 | 21 |

22 | Let's say we're building an app with a collapsible panel. If the panel isOpen, we'll show the body. Clicking the title triggers the toggleIsOpen action. 23 |

24 | 25 |

26 | Currently, our application looks like this. isOpen is the only piece of state in our application that changes. Further, the collapsible panel is the only part of the interface that needs to know about it. Because of this, it makes sense for the panel itself to "own" this piece of application state. So, we'll leave isOpen right where it is - as a simple property on the component. 27 |

28 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-11/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 | 8 | 9 |
10 | 11 | Account Type 12 | 13 |
14 | 25 | 36 |
37 |
38 | 39 | 58 | 59 |
60 | 69 |
70 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-11/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 | 8 | 9 |
10 | 11 | Account Type 12 | 13 |
14 | 25 | 36 |
37 |
38 | 39 | 58 | 59 |
60 | 69 |
70 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-1/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 |
5 |

6 | Utility-First 7 |

8 |

9 | Traditionally, whenever you need to style something on the web, you write CSS. With Tailwind, you style elements by applying pre-existing classes directly in your HTML. 10 |

11 |

12 | Once you've actually built something this way, you'll quickly notice some really important benefits: 13 |

14 |
    15 |
  • 16 | You aren't wasting energy inventing class names. 17 |
  • 18 |
  • 19 | Your CSS stops growing. 20 |
  • 21 |
  • 22 | Making changes feels safer. 23 |
  • 24 |
25 |

26 | When you realize how productive you can be working exclusively in HTML with predefined utility classes, working any other way will feel like torture. 27 |

28 |

29 | The biggest maintainability concern when using a utility-first approach is managing commonly repeated utility combinations. This is easily solved by extracting components, either as template partials/JavaScript components, or using Tailwind's @apply feature to create abstractions around common utility patterns. 30 |

31 |

32 | Aside from that, maintaining a utility-first CSS project turns out to be a lot easier than maintaining a large CSS codebase, simply because HTML is so much easier to maintain than CSS. Large companies like GitHub, Heroku, Kickstarter, Twitch, Segment, and more are using this approach with great success. 33 |

34 |
35 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emberconf2020-tailwindcss-best-practices", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "", 6 | "repository": "", 7 | "license": "MIT", 8 | "author": "", 9 | "directories": { 10 | "doc": "doc", 11 | "test": "tests" 12 | }, 13 | "scripts": { 14 | "build": "ember build --environment=production", 15 | "lint:hbs": "ember-template-lint .", 16 | "lint:js": "eslint .", 17 | "start": "ember serve", 18 | "test": "ember test" 19 | }, 20 | "devDependencies": { 21 | "@ember/optional-features": "^1.3.0", 22 | "@ember/render-modifiers": "^1.0.2", 23 | "@glimmer/component": "^1.0.0", 24 | "@glimmer/tracking": "^1.0.0", 25 | "@tailwindcss/custom-forms": "^0.2.1", 26 | "@tailwindcss/ui": "^0.1.3", 27 | "autoprefixer": "^9.7.5", 28 | "babel-eslint": "^10.0.3", 29 | "broccoli-asset-rev": "^3.0.0", 30 | "ember-auto-import": "^1.5.3", 31 | "ember-cli": "~3.17.0", 32 | "ember-cli-app-version": "^3.2.0", 33 | "ember-cli-babel": "^7.17.2", 34 | "ember-cli-dependency-checker": "^3.2.0", 35 | "ember-cli-eslint": "^5.1.0", 36 | "ember-cli-htmlbars": "^4.2.2", 37 | "ember-cli-inject-live-reload": "^2.0.2", 38 | "ember-cli-postcss": "^6.0.0", 39 | "ember-cli-sri": "^2.1.1", 40 | "ember-cli-string-helpers": "^4.0.6", 41 | "ember-cli-uglify": "^3.0.0", 42 | "ember-code-snippet": "^3.0.0", 43 | "ember-composable-helpers": "^3.1.1", 44 | "ember-data": "~3.17.0", 45 | "ember-export-application-global": "^2.0.1", 46 | "ember-fetch": "^7.0.0", 47 | "ember-load-initializers": "^2.1.1", 48 | "ember-math-helpers": "^2.13.0", 49 | "ember-maybe-import-regenerator": "^0.1.6", 50 | "ember-prism": "^0.6.0", 51 | "ember-qunit": "^4.6.0", 52 | "ember-render-helpers": "^0.1.1", 53 | "ember-resolver": "^7.0.0", 54 | "ember-source": "~3.17.1", 55 | "ember-truth-helpers": "^2.1.0", 56 | "eslint-plugin-ember": "^7.7.2", 57 | "eslint-plugin-node": "^11.0.0", 58 | "faker": "^4.1.0", 59 | "focus-visible": "^5.0.2", 60 | "loader.js": "^4.7.0", 61 | "postcss-cli": "^7.1.0", 62 | "qunit-dom": "^1.0.0", 63 | "tailwindcss": "^1.2.0" 64 | }, 65 | "engines": { 66 | "node": "10.* || >= 12" 67 | }, 68 | "ember": { 69 | "edition": "octane" 70 | }, 71 | "dependencies": { 72 | "graphql": "^14.6.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/components/exercises/exercise-13/solution.hbs: -------------------------------------------------------------------------------- 1 |
2 | 63 | 64 |
65 |
66 |
67 |

68 | Dashboard 69 |

70 |
71 |
72 |
73 |
74 |
75 |
78 |
79 |
80 |
81 |
82 |
-------------------------------------------------------------------------------- /app/components/exercises/exercise-13/exercise.hbs: -------------------------------------------------------------------------------- 1 |
2 | 95 | 96 |
97 |
98 |
99 |

100 | Dashboard 101 |

102 |
103 |
104 |
105 |
106 |
107 |
110 |
111 |
112 |
113 |
114 |
-------------------------------------------------------------------------------- /app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |
4 | {{! Off-canvas menu }} 5 |
6 | 13 |
19 |
20 | {{#if this.sidebarIsOpen}} 21 | 40 | {{/if}} 41 |
42 | 74 |
75 |
76 | 77 |
78 |
79 | 99 | 109 |

110 | {{this.currentExercise}} 111 |

112 | 122 |
123 |
127 | {{outlet}} 128 |
129 |
130 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EmberConf 2020: Tailwind CSS Best Practices 2 | 3 | Welcome! 4 | 5 | You can view the training on YouTube: 6 | 7 | [→ View the training on YouTube](https://www.youtube.com/watch?v=eTdYzNMPn1o) 8 | 9 | Follow along using the instructions below. 10 | 11 | ## Getting help 12 | 13 | If you have any questions, 14 | 15 | - DM us (@samselikoff or @ryanto) in the Ember Community Discord 16 | - Email us at hello@embermap.com 17 | 18 | ## Running the training app on your computer 19 | 20 | From a directory, 21 | 22 | ```sh 23 | git clone git@github.com:embermap/emberconf-2020-tailwindcss-best-practices.git 24 | cd emberconf-2020-tailwindcss-best-practices 25 | yarn install 26 | ember s 27 | ``` 28 | 29 | ## Intro 30 | 31 | Quick overview. 32 | 33 | Some useful VSCode plugins: 34 | 35 | - [Tailwind CSS IntelliSense for VS Code](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 36 | - [Headwind for VS Code](https://marketplace.visualstudio.com/items?itemName=heybourn.headwind) 37 | 38 | ## 1: Basic Tailwind 39 | 40 | - Style a blog post 41 | - Pseudo states 42 | - Responsive design 43 | 44 | ## 2: Extract components, not classes 45 | 46 | Padding trick for fixed aspect ratio. 47 | 48 | ```hbs 49 |
50 | 54 |
55 | ``` 56 | 57 | How to think about abstracting + sharing? Might reach for @apply. 58 | 59 | Problem is, you still have to duplicate html structure. You need a wrapper + a child. Another problem is that you have to go to the css file and break html-first workflow. 60 | 61 | Instead, use components. 62 | 63 | ```hbs 64 | 65 | 66 | 67 | ``` 68 | 69 | This is going to be a theme of this training. Components like this keep us in the html. That should be a goal with the abstractions you make: html-first workflow. Keeps you productive. 70 | 71 | ## 3: Tailwind-friendly Component APIs 72 | 73 | `` takes activeClass arg. Let's make it work. 74 | 75 | Our styles are stomping each other. We need to think of an API that's Tailwind-friendly. 76 | 77 | What we really want is ``. Let's make it work. 78 | 79 | ## 4: Layout with Flexbox 80 | 81 | Old school button group here. Buttons are foated left, parent is inline-block. How to center? 82 | 83 | Use text-center. 84 | 85 | This is weird - we're using `text-align: center` to lay out a component? 86 | 87 | With Tailwind + modern css you'll get very familiar with flexbox. Its great because it works in many more contexts and you usually don't need to worry about whether the child you're laying out is block or inline. The layout is kept more separate from the thing you're laying out. Also floats are super weird. Also the height of our group is different from the buttons – because of line-height. Again, inline elements are kinda weird. 88 | 89 | [ **Exercise**: Once you have it using flexbox, copy + paste the button group so there are two. Play with the justify-* classes on the parent. ] 90 | 91 | ## 5: Exercise: Practice Layout with Flexbox 92 | 93 | Match the layout on the right. Notice the behavior if you shrink the viewport. You'll need to look up the "Flex Shrink" utilities on tailwindcss.com. 94 | 95 | ## 6: More layout - measured text 96 | 97 | Try to encapsulate the measured text in a component. Notice how you can lay it out with flexbox. 98 | 99 | ## 7: Layout with Grid 100 | 101 | Build with flexbox first. Then refactor to grid. 102 | 103 | Grid is amazing. Gap is amazing. 104 | 105 | ## 8: Exercise: Practice with Grid 106 | 107 | ## 9: Working with SVG 108 | 109 | Copy svgs in, get rid of hard-coded widths and heights. Set fill or stroke to currentColor. 110 | 111 | ## 10: Exercise: Practice with SVG 112 | 113 | Copy svgs in, get rid of hard-coded widths and heights. Set fill or stroke to currentColor. 114 | 115 | ## 11: Form styling library 116 | 117 | Forms by default aren't very "utility-friendly". There's also lots of inconsistencies across browsers. 118 | 119 | The Custom forms plugin smoothes these out. Let's see how it works. 120 | 121 | - https://github.com/tailwindcss/custom-forms 122 | - Make sure you have autoprefixer 123 | 124 | ## 12: Writing a plugin for focus-visible 125 | 126 | Polyfill: https://github.com/WICG/focus-visible 127 | 128 | Download & import the polyfill 129 | 130 | Impot `plugin` from Tailwind: 131 | 132 | ```js 133 | const plugin = require("tailwindcss/plugin"); 134 | ``` 135 | 136 | Write the plugin. 137 | 138 | ```js 139 | plugin(function({ addVariant, e }) { 140 | addVariant("focus-visible", ({ modifySelectors, separator }) => { 141 | modifySelectors(({ className }) => { 142 | return `.${e( 143 | `focus-visible${separator}${className}` 144 | )}[data-focus-visible-added]`; 145 | }); 146 | }); 147 | }); 148 | ``` 149 | 150 | Add the `focus-visible` variant to the relevant plugins: 151 | 152 | ```js 153 | variants: { 154 | borderColor: ["responsive", "hover", "focus", "focus-visible"], 155 | boxShadow: ["responsive", "hover", "focus", "focus-visible"], 156 | zIndex: ["responsive", "focus", "focus-visible"] 157 | }, 158 | ``` 159 | 160 | ## 13: Responsive designs for very different layouts 161 | 162 | Finishing off with a hard lesson learned. 163 | 164 | First, if a layout is very different between two breakpoints, just split it up. 165 | 166 | Avoid JS device viewport width. Use CSS media queries. Robust to SSR. 167 | 168 | ## Resources 169 | 170 | - [Tailwind CSS](https://tailwindcss.com/) 171 | - [Tailwind CSS IntelliSense for VS Code](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 172 | - [Headwind for VS Code](https://marketplace.visualstudio.com/items?itemName=heybourn.headwind) 173 | - [Tailwind Custom forms plugin](https://github.com/tailwindcss/custom-forms) 174 | - [Heroicons: Free SVG icons](https://heroicons.dev/) 175 | - [Focus-visible polyfill](https://github.com/WICG/focus-visible) 176 | - [Tailwind UI](https://tailwindui.com) 177 | --------------------------------------------------------------------------------