diff --git a/src/examples/src/config.tsx b/src/examples/src/config.tsx index 62ff93e6a7..9022a80adb 100644 --- a/src/examples/src/config.tsx +++ b/src/examples/src/config.tsx @@ -110,6 +110,7 @@ import ListItemRenderer from './widgets/list/ListItemRenderer'; import FetchedResource from './widgets/list/FetchedResource'; import DisabledList from './widgets/list/Disabled'; import DraggableList from './widgets/list/Draggable'; +import StaticOptionList from './widgets/list/StaticOption'; import CustomThemeList from './widgets/list/CustomTheme'; import Menu from './widgets/list/Menu'; import CustomTransformer from './widgets/list/CustomTransformer'; @@ -161,6 +162,7 @@ import RequiredRangeSlider from './widgets/range-slider/Required'; import LabelledRangeSlider from './widgets/range-slider/Labelled'; import ControlledRangeSlider from './widgets/range-slider/Controlled'; import AdditionalText from './widgets/select/AdditionalText'; +import PlaceholderSelect from './widgets/select/Placeholder'; import BasicSelect from './widgets/select/Basic'; import ControlledSelect from './widgets/select/Controlled'; import CustomRenderer from './widgets/select/CustomRenderer'; @@ -254,6 +256,7 @@ import PaginationPageSizeSelector from './widgets/pagination/PageSizeSelector'; import PaginationControlled from './widgets/pagination/Controlled'; import PaginationSiblingCount from './widgets/pagination/SiblingCount'; import BasicTypeahead from './widgets/typeahead/Basic'; +import PlaceholderTypeahead from './widgets/typeahead/Placeholder'; import RemoteTypeahead from './widgets/typeahead/RemoteSource'; import ValidatedTypeahead from './widgets/typeahead/Validation'; import FreeTextTypeahead from './widgets/typeahead/FreeText'; @@ -1147,6 +1150,11 @@ export const config = { module: FillList, title: 'Fill' }, + { + filename: 'StaticOption', + module: StaticOptionList, + title: 'Static Option' + }, { description: 'This example shows how list items can be easily themed', filename: 'CustomTheme', @@ -1574,6 +1582,13 @@ export const config = { sandbox: true, size: 'medium' }, + { + filename: 'Placeholder', + module: PlaceholderSelect, + title: 'Placeholder', + sandbox: true, + size: 'medium' + }, { filename: 'DisabledSelect', module: DisabledSelect, @@ -2090,6 +2105,11 @@ export const config = { sandbox: true, size: 'large' }, + { + filename: 'Placeholder', + module: PlaceholderTypeahead, + title: 'Placeholder' + }, { filename: 'Controlled', module: ControlledTypeahead, diff --git a/src/examples/src/widgets/list/StaticOption.tsx b/src/examples/src/widgets/list/StaticOption.tsx new file mode 100644 index 0000000000..fc4375681b --- /dev/null +++ b/src/examples/src/widgets/list/StaticOption.tsx @@ -0,0 +1,22 @@ +import { create, tsx } from '@dojo/framework/core/vdom'; +import List from '@dojo/widgets/list'; +import icache from '@dojo/framework/core/middleware/icache'; +import Example from '../../Example'; +import { listOptionTemplate } from '../../template'; + +const factory = create({ icache }); + +export default factory(function StaticOption({ middleware: { icache } }) { + return ( + + { + icache.set('value', value); + }} + staticOption={{ value: 'static', label: 'This is a static option' }} + /> +

{`Clicked on: ${JSON.stringify(icache.getOrSet('value', ''))}`}

+
+ ); +}); diff --git a/src/examples/src/widgets/select/AdditionalText.tsx b/src/examples/src/widgets/select/AdditionalText.tsx index 79e59eebf4..7ed6197146 100644 --- a/src/examples/src/widgets/select/AdditionalText.tsx +++ b/src/examples/src/widgets/select/AdditionalText.tsx @@ -1,12 +1,12 @@ -import { create, tsx } from '@dojo/framework/core/vdom'; -import Select from '@dojo/widgets/select'; import icache from '@dojo/framework/core/middleware/icache'; -import Example from '../../Example'; import { - createResourceTemplate, - createResourceMiddleware + createResourceMiddleware, + createResourceTemplate } from '@dojo/framework/core/middleware/resources'; +import { create, tsx } from '@dojo/framework/core/vdom'; import { ListOption } from '@dojo/widgets/list'; +import Select from '@dojo/widgets/select'; +import Example from '../../Example'; const resource = createResourceMiddleware(); const factory = create({ icache, resource }); @@ -27,7 +27,6 @@ export default factory(function AdditionalText({ id, middleware: { icache, resou icache.set('value', value); }} helperText="I am the helper text" - placeholder="I am a placeholder" > {{ label: 'Additional Text' diff --git a/src/examples/src/widgets/select/Placeholder.tsx b/src/examples/src/widgets/select/Placeholder.tsx new file mode 100644 index 0000000000..0bdd77bf1c --- /dev/null +++ b/src/examples/src/widgets/select/Placeholder.tsx @@ -0,0 +1,41 @@ +import icache from '@dojo/framework/core/middleware/icache'; +import { + createResourceMiddleware, + createResourceTemplate +} from '@dojo/framework/core/middleware/resources'; +import { create, tsx } from '@dojo/framework/core/vdom'; +import { ListOption } from '@dojo/widgets/list'; +import Select from '@dojo/widgets/select'; +import Example from '../../Example'; + +const resource = createResourceMiddleware(); +const factory = create({ icache, resource }); +const options = [ + { value: '1', label: 'cat' }, + { value: '2', label: 'dog' }, + { value: '3', label: 'fish' } +]; + +const template = createResourceTemplate('value'); + +export default factory(function Placeholder({ id, middleware: { icache, resource } }) { + return ( + + +
{JSON.stringify(icache.getOrSet('value', ''))}
+
+ ); +}); diff --git a/src/examples/src/widgets/typeahead/Placeholder.tsx b/src/examples/src/widgets/typeahead/Placeholder.tsx new file mode 100644 index 0000000000..6fbe3e7be1 --- /dev/null +++ b/src/examples/src/widgets/typeahead/Placeholder.tsx @@ -0,0 +1,60 @@ +import { create, tsx } from '@dojo/framework/core/vdom'; +import icache from '@dojo/framework/core/middleware/icache'; +import Typeahead from '@dojo/widgets/typeahead'; +import Example from '../../Example'; +import { + createResourceTemplate, + createResourceMiddleware, + defaultFilter +} from '@dojo/framework/core/middleware/resources'; +import { largeListOptions } from '../../data'; +import { ListOption } from '@dojo/widgets/list'; + +const resource = createResourceMiddleware(); +const factory = create({ icache, resource }); + +const dataWithDisabled = largeListOptions.map((item) => ({ + ...item, + disabled: Math.random() < 0.1 +})); + +export const listOptionTemplate = createResourceTemplate({ + idKey: 'value', + read: async (req, { put }) => { + // emulate an async request + await new Promise((res) => setTimeout(res, 1000)); + const { offset, size, query } = req; + const filteredData = dataWithDisabled.filter((item) => defaultFilter(query, item)); + put({ data: filteredData.slice(offset, offset + size), total: filteredData.length }, req); + } +}); + +export default factory(function Placeholder({ middleware: { icache, resource } }) { + const strict = icache.getOrSet('strict', true); + return ( + + { + icache.set('value', value); + }} + placeholder={{ + value: 'placeholder', + label: 'This is a placeholder' + }} + required + > + {{ + label: 'Placeholder Typeahead' + }} + + +
{JSON.stringify(icache.getOrSet('value', ''))}
+
+ ); +}); diff --git a/src/list/index.tsx b/src/list/index.tsx index 32a3f22ee8..db95930065 100644 --- a/src/list/index.tsx +++ b/src/list/index.tsx @@ -1,19 +1,19 @@ import { RenderResult } from '@dojo/framework/core/interfaces'; -import { focus } from '@dojo/framework/core/middleware/focus'; import dimensions from '@dojo/framework/core/middleware/dimensions'; +import { focus } from '@dojo/framework/core/middleware/focus'; import { createICacheMiddleware } from '@dojo/framework/core/middleware/icache'; +import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources'; +import { throttle } from '@dojo/framework/core/util'; import { create, tsx } from '@dojo/framework/core/vdom'; -import { Keys, isRenderResult } from '../common/util'; -import theme, { ThemeProperties } from '../middleware/theme'; +import { isRenderResult, Keys } from '../common/util'; +import Icon from '../icon'; +import LoadingIndicator from '../loading-indicator'; import offscreen from '../middleware/offscreen'; +import theme, { ThemeProperties } from '../middleware/theme'; import * as listItemCss from '../theme/default/list-item.m.css'; -import * as menuItemCss from '../theme/default/menu-item.m.css'; import * as css from '../theme/default/list.m.css'; +import * as menuItemCss from '../theme/default/menu-item.m.css'; import * as fixedCss from './list.m.css'; -import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources'; -import LoadingIndicator from '../loading-indicator'; -import { throttle } from '@dojo/framework/core/util'; -import Icon from '../icon'; export interface MenuItemProperties { /** Callback used when the item is clicked */ @@ -243,6 +243,8 @@ export interface ListProperties { disabled?: (item: ListOption) => boolean; /** Specifies if the list height should by fixed to the height of the items in view */ height?: 'auto' | 'fixed'; + /** Static option to always show */ + staticOption?: ListOption; } export interface ListChildren { @@ -411,6 +413,7 @@ export const List = factory(function List({ } function renderItems(start: number, count: number) { + const { staticOption } = properties(); const renderedItems = []; const { size: resourceRequestSize } = options(); const { @@ -434,6 +437,11 @@ export const List = factory(function List({ } return get({ ...options(), offset: (page - 1) * options().size }, { read }); }); + if (staticOption !== undefined) { + const { value, label, disabled, divider } = staticOption; + renderedItems[0] = renderItem({ value, label, disabled, divider }, -1); + } + const offset = renderedItems.length; for (let i = 0; i < Math.min(total - start, count); i++) { const index = i + startNode; const page = Math.floor(index / resourceRequestSize) + 1; @@ -442,16 +450,19 @@ export const List = factory(function List({ const items = pageItems[pageIndex]; if (items && items[indexWithinPage]) { const { value, label, disabled, divider } = items[indexWithinPage]; - renderedItems[i] = renderItem({ value, label, disabled, divider }, index); + renderedItems[i + offset] = renderItem( + { value, label, disabled, divider }, + index + ); } else if (!items) { - renderedItems[i] = renderPlaceholder(index); + renderedItems[i + offset] = renderLoading(index); } } } return renderedItems; } - function renderPlaceholder(index: number) { + function renderLoading(index: number) { const itemProps = { widgetId: `${idBase}-item-${index}`, key: `item-${index}`, @@ -646,7 +657,8 @@ export const List = factory(function List({ return divider ? [item,
] : item; } - let { value: selectedValue, draggable, onMove } = properties(); + const { draggable, onMove, staticOption } = properties(); + let { value: selectedValue } = properties(); if (selectedValue === undefined) { if (initialValue !== undefined && initialValue !== icache.get('initial')) { @@ -734,7 +746,7 @@ export const List = factory(function List({ const renderedItemsCount = calculatedItemsInView + 2 * nodePadding; let computedActiveIndex = activeIndex === undefined ? icache.get('activeIndex') : activeIndex; const inputText = icache.get('inputText'); - const { + let { meta: { total = 0 } } = get(options(), { meta: true, read }); if (inputText && inputText !== icache.get('previousInputText') && total) { @@ -791,6 +803,10 @@ export const List = factory(function List({ const offsetY = startNode * itemHeight; const items = renderItems(startNode, renderedItemsCount); + + if (staticOption !== undefined) { + total++; + } const totalContentHeight = total * itemHeight; return (
{ r.expect(listWithListItemsAssertion); }); + it('should render list with static option', () => { + const r = renderer( + () => ( + + ), + { middleware: [[getRegistry, mockGetRegistry]] } + ); + r.expect( + listWithListItemsAssertion + .setProperty(WrappedItemWrapper, 'styles', { + height: '180px' + }) + .prepend(WrappedItemContainer, () => [ + + This is a static option + + ]) + ); + }); + it('should render list with auto height', () => { const r = renderer( () => ( diff --git a/src/select/index.tsx b/src/select/index.tsx index af37471cb5..6957760c1b 100644 --- a/src/select/index.tsx +++ b/src/select/index.tsx @@ -5,6 +5,7 @@ import { createICacheMiddleware } from '@dojo/framework/core/middleware/icache'; import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources'; import { uuid } from '@dojo/framework/core/util'; import { create, tsx } from '@dojo/framework/core/vdom'; +import { find } from '@dojo/framework/shim/array'; import { Keys } from '../common/util'; import HelperText from '../helper-text'; import Icon from '../icon'; @@ -12,20 +13,19 @@ import Label from '../label'; import { ItemRendererProperties, List, - ListOption, ListItemProperties, + ListOption, MenuItemProperties } from '../list'; +import LoadingIndicator from '../loading-indicator'; import theme from '../middleware/theme'; import { PopupPosition } from '../popup'; -import TriggerPopup from '../trigger-popup'; -import * as listCss from '../theme/default/list.m.css'; -import * as labelCss from '../theme/default/label.m.css'; import * as iconCss from '../theme/default/icon.m.css'; +import * as labelCss from '../theme/default/label.m.css'; +import * as listCss from '../theme/default/list.m.css'; import * as css from '../theme/default/select.m.css'; +import TriggerPopup from '../trigger-popup'; import bundle from './nls/Select'; -import LoadingIndicator from '../loading-indicator'; -import { find } from '@dojo/framework/shim/array'; export interface SelectProperties { /** Callback called when user selects a value */ @@ -39,7 +39,7 @@ export interface SelectProperties { /** placement of the select menu; 'above' or 'below' */ position?: PopupPosition; /** Placeholder value to show when nothing has been selected */ - placeholder?: string; + placeholder?: ListOption; /** Property to determine if the input is disabled */ disabled?: boolean; /** Sets the helper text of the input */ @@ -100,7 +100,6 @@ export const Select = factory(function Select({ itemsInView = 6, onValidate, onValue, - placeholder = '', position, required, name, @@ -115,7 +114,11 @@ export const Select = factory(function Select({ } = resource.template(template); const [{ items, label } = { items: undefined, label: undefined }] = children(); - let { value } = properties(); + let { value, placeholder } = properties(); + + if (!required && placeholder === undefined) { + placeholder = { value: '', label: '' }; + } if (value === undefined) { if (initialValue !== undefined && initialValue !== icache.get('initial')) { @@ -123,6 +126,11 @@ export const Select = factory(function Select({ icache.set('value', initialValue); } value = icache.get('value'); + if (value === undefined && placeholder !== undefined) { + icache.set('value', placeholder.value); + value = icache.get('value'); + onValue(placeholder); + } } const menuId = icache.getOrSet('menuId', uuid()); @@ -222,6 +230,21 @@ export const Select = factory(function Select({ } } + let valueOption: ListOption | undefined; + if (value && data && (!placeholder || placeholder.value !== value)) { + let found = find(data, (item) => { + return Boolean(item.value && item.value.value === value); + }); + if (found) { + valueOption = found.value; + } else { + const items = get(options({ query: { value } }), { read }); + if (items) { + valueOption = items[0]; + } + } + } + return (