Skip to content

Commit

Permalink
Merge pull request #41 from f3d0t/select-component
Browse files Browse the repository at this point in the history
Pull request for issue #36
  • Loading branch information
zonzujiro authored Jun 24, 2021
2 parents c624a3a + ce13248 commit 24e9546
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Out of scope:

## Development

Make sure you have [Node.js](https://nodejs.org/en/) installed on your machine.

`npm install` to install dependencies.
Ignore npm audit warnings.
If any changes appear on `package-lock.json` just commit those.
Expand Down
33 changes: 33 additions & 0 deletions src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,44 @@ import { createElement, createFragment } from '../framework/element';
import SearchByCity from './SearchByCity';
import WeatherResults from './WeatherResults';
import Checkbox from './Checkbox';
import Select from './Select';

export default function App() {
const selectOptions = [
{ value: 1, label: 'one' },
{ value: 2, label: 'two' },
{ value: 3, label: 'three' },
{
value: 'group',
label: 'optgtoup',
options: [
{ value: 4 },
{ value: 5, label: 'five' },
{ value: 10, label: 'disabled', disabled: true },
],
},
{
label: 'disabled',
disabled: true,
options: [{ value: 6 }, { value: 7, label: 'seven' }],
},
{ value: 8, label: 'eight', disabled: false },
{ value: 89, label: 'nine', disabled: true },
];

return (
<>
<SearchByCity />
<Select
label="Select example"
id="test"
isMultiple
size={5}
name="example"
isRequired
selectedOption={5}
options={selectOptions}
/>
<Checkbox
label="next days forecast - at noon only"
onChange={e => setForecastPeriodicity(e.target.value)}
Expand Down
121 changes: 121 additions & 0 deletions src/components/Select/Select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/** @jsx createElement */
/** @jsxFrag createFragment */

import { createElement, createFragment } from '../../framework/element';

/**
* Creates select element.
* @param {string} label - optional, default is null
* @param {string} id - must have if label not null, default is null
* @param {boolean} isMultiple: optional, default is false; ; shouldn't be rendered if false
* @param {string} name: optional, default is null; shouldn't be rendered if null
* @param {boolean} isRequired: optional, default is false; shouldn't be rendered if null
* @param {number} size: optional, default is 0
* @param {array} options: array of option objects (see option object spec below) or array of values, index is used as an option id (option value attribute) and array element itself as an option UI representation visible to a user
* @param {number} selectedOption: option index, default is null
* @param {function} onChange: optional, that expects selected option id (value) as an argument
* @param {boolean} disabled: optional, default is false; ; shouldn't be rendered if false
*/

/*
Each option object has the following properties:
value: string; used as text if label property is not defined or falsy, or as optgroup label if object has options property defined
label: optional, string; if not defined or null or undefined then value is used as an option UI representation visible to a user; ignored when the object represents an optgroup
options: optional, array of option objects; if defined then current object is used to render an optgroup
disabled: optional, boolean; if defined then current object will be disabled
*/

export default function Select({
label = null,
id = null,
isMultiple = false,
name = null,
isRequired = false,
size = 0,
options = [],
selectedOption = null,
onChange = null,
disabled = false,
}) {
if (options.length === 0) return null;
options = options.map((option, index) => {
if (typeof option === 'string') {
return <Option value={index} label={option} selected={index === selectedOption} />;
} else if (typeof option === 'object') {
if (option.options) {
return (
<Optgroup
label={option.label ? option.label : option.value}
disabled={option.disabled}
selectedOption={selectedOption}
options={option.options}
/>
);
} else {
return (
<Option
value={option.value}
label={option.label ? option.label : option.value}
selected={option.value === selectedOption}
disabled={option.disabled}
/>
);
}
}
});
return (
<>
{label ? <label for={id}>{label}: </label> : ''}
<select
id={id}
multiple={isMultiple}
name={name}
required={isRequired}
size={size}
onChange={onChange}
disabled={disabled}
>
{options}
</select>
</>
);
}

/**
* Creates option element.
* @param {string} value - string; used as text if label property is not defined or falsy, or as optgroup label if object has options property defined
* @param {string} label - optional, string; if not defined or null or undefined then value is used as an option UI representation visible to a user;
* @param {boolean} disabled: optional, boolean; if defined then current object will be disabled
* @param {boolean} selected: optional, boolean; if defined then current object will be selected
*/

export function Option({ value, label = null, disabled = null, selected = null }) {
return (
<option value={value} selected={selected} disabled={disabled}>
{label ? label : value}
</option>
);
}

/**
* Creates optgroup element.
* @param {string} label - string; value is used as an option UI representation visible to a user;
* @param {boolean} disabled: optional, boolean; if defined then current object will be disabled
* @param {boolean} selectedOption: optional, boolean; if defined then current object will be selected
* @param {array} options: array of option objects or array of values, index is used as an option id (option value attribute) and array element itself as an option UI representation visible to a user
*/

export function Optgroup({ label, disabled = null, selectedOption = null, options }) {
return (
<optgroup label={label} disabled={disabled}>
{options.map(groupOption => (
<Option
value={groupOption.value}
label={groupOption.label ? groupOption.label : groupOption.value}
selected={groupOption.value === selectedOption}
disabled={groupOption.disabled}
/>
))}
</optgroup>
);
}
1 change: 1 addition & 0 deletions src/components/Select/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Select';
9 changes: 6 additions & 3 deletions src/framework/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@ export const createElement = (tag, props, ...children) => {
}
const element = tag === '' ? new DocumentFragment() : document.createElement(tag);
Object.entries(props || {}).forEach(([name, value]) => {
if (name.startsWith('on') && name.toLowerCase() in window) {
if (name.startsWith('on') && name.toLowerCase() in window && value !== null) {
element.addEventListener(
name.toLowerCase().substr(2),
/** @type {Function} */
value,
);
} else {
try {
if (!(element instanceof DocumentFragment)) {
if (!(element instanceof DocumentFragment) && value !== null) {
// Boolean attributes are considered to be true if they're present on the element at all, regardless of their actual value
// https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute#example
if (['disabled', 'checked'].includes(name) && !value) {
if (
['disabled', 'checked', 'multiple', 'selected', 'required'].includes(name) &&
!value
) {
element.removeAttribute(name);
} else if (name.toLowerCase() === 'classname') {
// We want to treat both strings and arrays in a similar manner
Expand Down

0 comments on commit 24e9546

Please sign in to comment.