Skip to content

Commit

Permalink
Merge pull request #1152 from MehulKChaudhari/tutorial/depedent-selec…
Browse files Browse the repository at this point in the history
…t-dropdowns

New tutorial chapter: dependent select dropdowns
  • Loading branch information
NullVoxPopuli authored Oct 5, 2024
2 parents c7af8fa + b08bfd0 commit 5809c43
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 0 deletions.
6 changes: 6 additions & 0 deletions apps/tutorial/.template-lintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ module.exports = {
// We do what we want. psh
'no-forbidden-elements': 'off',
'no-inline-styles': 'off',
// false negatives due to template-lint not being able to determine that
// a calling context provides the label
//
// We'd rather disable than have template-lint-disables in the file...
// lint disables look unprofessional
'require-input-label': 'off',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
import { cell } from 'ember-resources';
import { TrackedObject } from 'tracked-built-ins';
import { RemoteData } from 'ember-resources/util/remote-data';

const DATA_SOURCES = ['people', 'planets'];
// A cell is @tracked data that can be used anywhere -- makes for smaller demos
// This could be `@tracked selectedAPI = DATA_SOURCES[0];` in a component class
const selectedAPI = cell(DATA_SOURCES[0]);
const selectedStuff = new TrackedObject();

/////////// Select
// Since we need a few of these for the demo, here is a shared Select component
function handleSelectChange(handler, options, event) {
let stringValue = event.target.value;
return handler(stringValue);
}
function isSelected(a, b) {
return a === b;
}
// for brevity, this only supports an array of strings for @options
const Select = <template>
<select {{on 'change' (fn handleSelectChange @onChange @options)}}>
{{#each @options as |item|}}
<option value={{item}} selected={{isSelected @value item}}>
{{item}}
</option>
{{/each}}
</select>
</template>;
/////////// End Select

////////////// Demo

function data() {
return JSON.stringify({ selectedAPI: selectedAPI.current, selectedStuff }, null, 3);
}

function urlForDataSource(selectedData) {
return `https://swapi.dev/api/${selectedData}/`;
}

function setSelected(propertyName, value) {
selectedStuff[propertyName] = value;
}

function names(options) {
return options.map(option => option.name);
}

<template>
<pre>{{data}}</pre>

<label>
Parent Dropdown
<Select
@options={{DATA_SOURCES}}
@onChange={{selectedAPI.set}}
@selected={{selectedAPI.current}}
/>
</label>

<label>
Select {{selectedAPI.current}}

{{!
it's important to model async behavior, and this util makes that a bit easier.
Docs here: https://ember-resources.pages.dev/funcs/util_remote_data.RemoteData }}
{{#let (RemoteData (urlForDataSource selectedAPI.current)) as |request|}}
{{#if request.isLoading}}
Loading...
{{/if}}

{{#if request.value}}
<Select
@options={{names request.value.results}}
@onChange={{fn setSelected selectedAPI.current}}
>
<:option as |item|>
{{item.name}}
</:option>
</Select>
{{/if}}
{{/let}}
</label>

<style>
label {
border: 1px solid;
padding: 1rem;
display: block;
margin: 0.5rem 0;
}
select {
padding: 0.5rem 1rem;
}
</style>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
import { cell } from 'ember-resources';
import { TrackedObject } from 'tracked-built-ins';
import { RemoteData } from 'ember-resources/util/remote-data';

const DATA_SOURCES = ['people', 'planets'];
// A cell is @tracked data that can be used anywhere -- makes for smaller demos
// This could be `@tracked selectedAPI = DATA_SOURCES[0];` in a component class
const selectedAPI = cell(DATA_SOURCES[0]);
const selectedStuff = new TrackedObject();

/////////// Select
// Since we need a few of these for the demo, here is a shared Select component
function handleSelectChange(handler, options, event) {
let stringValue = event.target.value;
return handler(stringValue);
}
function isSelected(a, b) {
return a === b;
}
// for brevity, this only supports an array of strings for @options
const Select = <template>
<select {{on 'change' (fn handleSelectChange @onChange @options)}}>
{{#each @options as |item|}}
<option value={{item}} selected={{isSelected @value item}}>
{{item}}
</option>
{{/each}}
</select>
</template>;
/////////// End Select

////////////// Demo

function data() {
return JSON.stringify({ selectedAPI: selectedAPI.current, selectedStuff }, null, 3);
}

function setSelected(propertyName, value) {
selectedStuff[propertyName] = value;
}

<template>
<pre>{{data}}</pre>

<label>
Parent Dropdown
<Select
@options={{DATA_SOURCES}}
@onChange={{selectedAPI.set}}
@selected={{selectedAPI.current}}
/>
</label>

<style>
label {
border: 1px solid;
padding: 1rem;
display: block;
margin: 0.5rem 0;
}
select {
padding: 0.5rem 1rem;
}
</style>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
You need to fetch data from the API corresponding to the selected option in the first dropdown. This requires creating a function that builds the correct API URL based on the selected value.

In this example, the fetching of data from the [Options][swapi]
should occur automatically based on changes in option selected`.

```javascript
function urlForDataSource(selectedData) {
return `https://swapi.dev/api/${selectedData}/`;
}
```

Use the `RemoteData` utility to manage the asynchronous data fetching. This will allow you to handle loading states and response values effectively.

```hbs
{{#let (RemoteData (urlForDataSource selectedAPI.current)) as |request|}}
{{/let}}
```

After fetching the data, you need to update the second dropdown with the results. Use the `names` function to extract the names from the fetched data and pass them as options to the second dropdown.

```javascript
function names(options) {
return options.map(option => option.name);
}
```

```hbs
{{#let (RemoteData (urlForDataSource selectedAPI.current)) as |request|}}
{{#if request.value}}
<Select
@options={{names request.value.results}}
@onChange={{(fn setSelected selectedAPI.current)}}
>
<:option as |item|>
{{item.name}}
</:option>
</Select>
{{/if}}
{{/let}}
```
Use conditional rendering to display a loading message while the API call is in progress. This enhances user experience by providing feedback on the data fetching process.

```hbs
{{#if request.isLoading}}
Loading...
{{/if}}
```

Docs for `RemoteData` can [be found here][docs-remote-data].

[docs-remote-data]: https://ember-resources.pages.dev/modules/util_remote_data
[swapi]: https://swapi.dev/

0 comments on commit 5809c43

Please sign in to comment.