Skip to content
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

[WIP] Revamped octane tutorial #1002

Closed
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions guides/release/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,61 @@
# pages:
# - title: "Overview"
# url: "index"
- title: Tutorial
url: tutorial
pages:
- title: Part One
url: part-one
- title: Zero
url: 0
- title: One
url: 1
- title: Two
url: 2
- title: Three
url: 3
- title: Four
url: 4
- title: Five
url: 5
- title: Six
url: 6
- title: Seven
url: 7
- title: Eight
url: 8
- title: Nine
url: 9
- title: Ten
url: 10
- title: Eleven
url: 11
- title: Twelve
url: 12
- title: Thirteen
url: 13
- title: Fourteen
url: 14
- title: Fifteen
url: 15
- title: Sixteen
url: 16
- title: Seventeen
url: 17
- title: Eighteen
url: 18
- title: Nineteen
url: 19
- title: Twenty
url: 20
- title: Twenty-one
url: 21
- title: Part Two
url: part-two
- title: Twenty-two
url: 22
- title: Twenty-three
url: 23
- title: "Templating"
url: "templates"
pages:
Expand Down
3 changes: 3 additions & 0 deletions guides/release/tutorial/0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
TODO: fill this in!

`ember new ember-quickstart -b @ember/octane-app-blueprint`
5 changes: 5 additions & 0 deletions guides/release/tutorial/1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- TODO: explain ember server, etc. -->

When you are done experimenting, go ahead and delete the `app/templates/application.hbs` file. We won't be needing this for a while, and can start afresh and add it back later when there we have a need for it.

If you still have your browser tab open, your tab should automatically reload into a blank page as you delete the file. This reflects the fact that we no longer have an application template in our app.
124 changes: 124 additions & 0 deletions guides/release/tutorial/10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
We've been refactoring our existing code for a while, so let's implement a new feature for a change. Let's work on the site-wide navigation bar next.
vaidehijoshi marked this conversation as resolved.
Show resolved Hide resolved

We can create a `<NavBar>` component at `app/components/nav-bar.hbs`:

```handlebars {data-filename=app/components/nav-bar.hbs}
<nav class="menu">
<LinkTo @route="index" class="menu-index">
<h1>SuperRentals</h1>
</LinkTo>
<div class="links">
<LinkTo @route="about" class="menu-about">
About
</LinkTo>
<LinkTo @route="contact" class="menu-contact">
Contact
</LinkTo>
</div>
</nav>
```

Next, we will add our `<NavBar>` component to the top of each page:

```handlebars {data-filename="app/components/index.hbs" data-diff="+1"}
<NavBar />
<Jumbo>
<h2>Welcome to Super Rentals!</h2>
<!-- ...snip... -->
</Jumbo>
```

```handlebars {data-filename="app/components/about.hbs" data-diff="+1"}
<NavBar />
<Jumbo>
<h2>About Super Rentals</h2>
<!-- ...snip... -->
</Jumbo>
```

```handlebars {data-filename="app/components/contact.hbs" data-diff="+1"}
<NavBar />
<Jumbo>
<h2>Contact Us</h2>
<!-- ...snip... -->
</Jumbo>
```

Voilà, we made another component!

<div class="cta">
<div class="cta-note">
<div class="cta-note-body">
<div class="cta-note-heading">Zoey says...</div>
<div class="cta-note-message">
`<NavBar />` is a shorthand for `<NavBar></NavBar>`. Component tags must always be closed properly, even when you are not passing any content to them, as in this case. Since this is pretty common, Ember provides the alternative self-closing shorthand to save you some typing!
</div>
</div>
<img src="/images/mascots/zoey.png" role="presentation" alt="Ember Mascot">
</div>
</div>

Everything looks great in the browser, but as we know, you can never be too sure. So let's write some tests!
vaidehijoshi marked this conversation as resolved.
Show resolved Hide resolved

But what kind of test? We _could_ write a component test for the `<NavBar>` by itself, like we just did for the `<Jumbo>` component. However, since the job of `<NavBar>` is to _navigate_ us around the app, it would not make a lot of sense to test this particular component in isolation. So, let's go back to writing some acceptance tests!

```handlebars {data-filename="tests/acceptance/super-rentals-test.js" data-diff="+8,+9,+19,+20,+30,+31,+36,+37,+38,+39,+40,+41,+42,+43,+44,+45,+46,+47,+48,+49,+50,+51,+52,+53"}
module('Acceptance | super rentals', function(hooks) {
setupApplicationTest(hooks);

test('visiting /', async function(assert) {
await visit('/');

assert.equal(currentURL(), '/');
assert.dom('nav').exists();
assert.dom('h1').containsText('SuperRentals');
assert.dom('h2').containsText('Welcome to Super Rentals!');

// ...snip...
});

test('visiting /about', async function(assert) {
await visit('/about');

assert.equal(currentURL(), '/about');
assert.dom('nav').exists();
assert.dom('h1').containsText('SuperRentals');
assert.dom('h2').containsText('About Super Rentals');

// ...snip...
});

test('visiting /getting-in-touch', async function(assert) {
await visit('/getting-in-touch');

assert.equal(currentURL(), '/getting-in-touch');
assert.dom('nav').exists();
assert.dom('h1').containsText('SuperRentals');
assert.dom('h2').containsText('Contact Us');

// ...snip...
});

test('navigating using the nav-bar', async function(assert) {
await visit('/');

assert.dom('nav').exists();
assert.dom('nav a.menu-index').containsText('SuperRentals')
assert.dom('nav a.menu-about').containsText('About');
assert.dom('nav a.menu-contact').containsText('Contact');

await click('nav a.menu-about');
assert.equal(currentURL(), '/about');

await click('nav a.menu-contact');
assert.equal(currentURL(), '/getting-in-touch');

await click('nav a.menu-index');
assert.equal(currentURL(), '/');
});
});
```

We updated the existing tests to assert that a `<nav>` element exists on each page. This is important for accessibility since screen readers will use that element to provide navigation. Then, we added a new test that verifies the behavior of the `<NavBar>` links.

All tests should pass at this point.
44 changes: 44 additions & 0 deletions guides/release/tutorial/11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Before we move on to the next feature, there is one more thing we could clean up.

Since the `<NavBar>` is used for site-wide navigation, it really needs to be displayed on _every_ page in the app. So far, we have been adding the component on each page manually. This is is a bit error-prone, as we could easily forget to do this the next time that we add a new page.

We can solve this problem by moving the nav-bar into a special template called `application.hbs`. You may remember that it was generated for us when we first created the app but we deleted it. Now, it's time for us to bring it back!

This template is special in that it does not have its own URL and cannot be navigated to on its own. Rather, it is used to specify a common layout that is shared by every page in your app. This is a great place to put site-wide UI elements, like a nav-bar and a site footer.

While we are at it, we will also add a container element that wraps around the whole page, as requested by our designer for styling purposes.

```handlebars {data-filename="app/templates/application.hbs"}
<div class="container">
<NavBar />
<div class="body">
{{outlet}}
</div>
</div>
```

```handlebars {data-filename="app/components/index.hbs" data-diff="-1"}
<NavBar />
<Jumbo>
<h2>Welcome to Super Rentals!</h2>
<!-- ...snip... -->
</Jumbo>
```

```handlebars {data-filename="app/components/about.hbs" data-diff="-1"}
<NavBar />
<Jumbo>
<h2>About Super Rentals</h2>
<!-- ...snip... -->
</Jumbo>
```

```handlebars {data-filename="app/components/contact.hbs" data-diff="-1"}
<NavBar />
<Jumbo>
<h2>Contact Us</h2>
<!-- ...snip... -->
</Jumbo>
```

Much nicer! We can run our test suite which confirms that everything still works after our refactor. We are ready to move on to the next feature!
47 changes: 47 additions & 0 deletions guides/release/tutorial/12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
It's time to finally work on the rentals listing!

Let's start by creating a `<Rental>` component. This time, we will use the component generator to create the template and test file for us.

```
ember generate component rental
```

We will *[hard-code tags](TODO: link to hard-code tags)* the rental property's details for now, and replace it with the real data from the server later on. We will write a test to ensure all of the details are present.

<!-- TODO: format diff -->
```
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Component | rental', function(hooks) {
setupRenderingTest(hooks);

test('it renders information about a rental property', async function(assert) {
await render(hbs`<Rental />`);

assert.dom('article').hasClass('rental');
assert.dom('article h3').containsText('Grand Old Mansion');
assert.dom('article .detail.owner').containsText('Veruca Salt');
assert.dom('article .detail.type').containsText('Standalone');
assert.dom('article .detail.location').containsText('San Francisco');
assert.dom('article .detail.bedrooms').containsText('15');
});
});
```

Finally, let's invoke this a couple of times from our index template to populate the page.

<!-- TODO: format diff -->
```
<div class="list-filter">
<ul class="results">
<li><Rental /></li>
<li><Rental /></li>
<li><Rental /></li>
</ul>
</div>
```

Things are looking pretty convincing already; not bad for just a little bit of work!
93 changes: 93 additions & 0 deletions guides/release/tutorial/13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Next, let's add the image for the rental property. We will use the component generator for this again.

```
ember generate component rental/image
```

This time, we had a slash in the component's name. This resulted in the component being created at `app/components/rental/image.hbs` instead of at the root of `app/components`. This component can be invoked as `<Rental::Image>`.

Components like these are known as *[namespaced](TODO: link to namespaced)* components. They allow us to organize our components by folders according to their purpose. This is completely optional – namespaced components are not special in any way.
vaidehijoshi marked this conversation as resolved.
Show resolved Hide resolved

Let's edit the component's template:

```handlebars {data-filename="app/components/rental/image.hbs"}
<div class="image">
<img ...attributes>
</div>
```

Instead of hard-coding specific `src` and `alt` attributes on the `<img>` tag, we opted for the `...attributes` keyword instead, which is also sometimes referred to as *["splattributes"](TODO: link to splattributes)* syntax. This allows arbitrary HTML attributes to be passed in when invoking this component:

```handlebars {data-filename="app/components/rental.hbs" data-diff="+2,+3,+4,+5,+6"}
<article class="rental">
<Rental::Image
src="https://upload.wikimedia.org/wikipedia/commons/c/cb/Crane_estate_(5).jpg"
alt="A picture of Grand Old Mansion"
/>

<div class="details">
<!-- ...snip... -->
</div>
</article>
```

We specified a `src` and an `alt` HTML attribute here, which will be passed along to the component and attached to the element where `...attributes` is specified. You can think of this as being similar to `{{yield}}`, but for HTML attributes specifically, instead of for any content. In fact, we used this feature earlier when we passed a `class` attribute to `<LinkTo>`.
vaidehijoshi marked this conversation as resolved.
Show resolved Hide resolved

This way, our `<Rental::Image>` component is not coupled to any specific rental property on the site. Of course, the problem still exists, we simply moved it to the `<Rental>` component, but we will deal with that soon. We can try to limit all the hard-coding to the `<Rental>` component, so that we will have an easier time cleaning it up when we switch to fetching real data.
vaidehijoshi marked this conversation as resolved.
Show resolved Hide resolved

In general, it is a good idea to add `...attributes` to the primary element in your component. This will allow for maximum flexibility as the invoker may need to pass along classes for styling, or ARIA attributes to improve accessibility.
vaidehijoshi marked this conversation as resolved.
Show resolved Hide resolved

Let's write a test for our component!

<!-- TODO: format diff -->
```
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Component | rental/image', function(hooks) {
setupRenderingTest(hooks);

test('it renders the given image', async function(assert) {
await render(hbs`
<Rental::Image
src="/assets/images/teaching-tomster.png"
alt="Teaching Tomster"
/>
`);

assert.dom('.image').exists();
assert.dom('.image img').hasAttribute('src', '/assets/images/teaching-tomster.png');
assert.dom('.image img').hasAttribute('alt', 'Teaching Tomster');
});
});
```

Finally, we should also update the tests for the `<Rental>` component to confirm that we successfully invoked `<Rental::Image>`.

<!-- TODO: format diff -->
```
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Component | rental', function(hooks) {
setupRenderingTest(hooks);

test('it renders information about a rental property', async function(assert) {
await render(hbs`<Rental />`);

assert.dom('article').hasClass('rental');
assert.dom('article h3').containsText('Grand Old Mansion');
assert.dom('article .detail.owner').containsText('Veruca Salt');
assert.dom('article .detail.type').containsText('Standalone');
assert.dom('article .detail.location').containsText('San Francisco');
assert.dom('article .detail.bedrooms').containsText('15');
assert.dom('article .image').exists();
});
});
```

Because we already tested `<Rental::Image>` extensively on its own, we can omit the details here and keep our assertion to the bare minimum. That way, we won't have to _also_ update the `<Rental>` tests whenever we make any changes to `<Rental::Image>`.
vaidehijoshi marked this conversation as resolved.
Show resolved Hide resolved
Loading