diff --git a/examples/weather/nanoviews-nano_kit-di/src/components/Autocomplete.ts b/examples/weather/nanoviews-nano_kit-di/src/components/Autocomplete.ts index 9a827493..65b74019 100644 --- a/examples/weather/nanoviews-nano_kit-di/src/components/Autocomplete.ts +++ b/examples/weather/nanoviews-nano_kit-di/src/components/Autocomplete.ts @@ -6,7 +6,7 @@ import { signal } from 'nanoviews/store' import { - $$value, + value$, button, div, input, @@ -62,7 +62,7 @@ export function Autocomplete(props: AutocompleteProps) { 'name': props.name, 'role': 'combobox', 'type': 'text', - [$$value]: props.$value, + [value$]: props.$value, 'onBlur': () => $isOpen(false), 'onFocus': () => $isOpen(true), 'onInput': () => { diff --git a/examples/weather/nanoviews-nano_kit-di/src/components/Forecast.ts b/examples/weather/nanoviews-nano_kit-di/src/components/Forecast.ts index 85d619e8..e80aee2b 100644 --- a/examples/weather/nanoviews-nano_kit-di/src/components/Forecast.ts +++ b/examples/weather/nanoviews-nano_kit-di/src/components/Forecast.ts @@ -11,7 +11,7 @@ import { select, option, ul, - $$selected, + selected$, trackBy, inject, for_, @@ -45,7 +45,7 @@ export function Forecast() { ), select({ class: 'forecast-mode', - [$$selected]: $mode + [selected$]: $mode })( option({ value: 'hourly' diff --git a/examples/weather/nanoviews-nano_kit/src/components/Autocomplete.ts b/examples/weather/nanoviews-nano_kit/src/components/Autocomplete.ts index 9a827493..65b74019 100644 --- a/examples/weather/nanoviews-nano_kit/src/components/Autocomplete.ts +++ b/examples/weather/nanoviews-nano_kit/src/components/Autocomplete.ts @@ -6,7 +6,7 @@ import { signal } from 'nanoviews/store' import { - $$value, + value$, button, div, input, @@ -62,7 +62,7 @@ export function Autocomplete(props: AutocompleteProps) { 'name': props.name, 'role': 'combobox', 'type': 'text', - [$$value]: props.$value, + [value$]: props.$value, 'onBlur': () => $isOpen(false), 'onFocus': () => $isOpen(true), 'onInput': () => { diff --git a/examples/weather/nanoviews-nano_kit/src/components/Forecast.ts b/examples/weather/nanoviews-nano_kit/src/components/Forecast.ts index dc01a450..65ef340c 100644 --- a/examples/weather/nanoviews-nano_kit/src/components/Forecast.ts +++ b/examples/weather/nanoviews-nano_kit/src/components/Forecast.ts @@ -11,7 +11,7 @@ import { select, option, ul, - $$selected, + selected$, trackBy, for_, if_ @@ -43,7 +43,7 @@ export function Forecast() { ), select({ class: 'forecast-mode', - [$$selected]: $mode + [selected$]: $mode })( option({ value: 'hourly' diff --git a/packages/nanoviews/.size-limit.json b/packages/nanoviews/.size-limit.json index 5edb248f..d64bcd5b 100644 --- a/packages/nanoviews/.size-limit.json +++ b/packages/nanoviews/.size-limit.json @@ -8,7 +8,7 @@ { "name": "Average usage", "path": "dist/index.js", - "import": "{ fragment, div, form, input, button, label, $$classList, if_, for_, $$value, $$children, effect }", + "import": "{ fragment, div, form, input, button, label, classList$, if_, for_, value$, $$children, effect }", "limit": "4.26 kB" } ] diff --git a/packages/nanoviews/README.md b/packages/nanoviews/README.md index b6c6f539..eb6390b4 100644 --- a/packages/nanoviews/README.md +++ b/packages/nanoviews/README.md @@ -169,35 +169,35 @@ mount(App, document.querySelector('#app')) Effect attributes are special attributes that can control element's behavior. -### $$ref +### ref$ -`$$ref` is an effect attribute that can provide a reference to the DOM node. +`ref$` is an effect attribute that can provide a reference to the DOM node. ```js import { signal } from 'nanoviews/store' -import { div, $$ref } from 'nanoviews' +import { div, ref$ } from 'nanoviews' const $ref = signal(null) div({ - [$$ref]: $ref + [ref$]: $ref })( 'Target element' ) ``` -### $$style +### style$ -`$$style` is an effect attribute that manages the style of the element. +`style$` is an effect attribute that manages the style of the element. ```js import { signal } from 'nanoviews/store' -import { button, $$style } from 'nanoviews' +import { button, style$ } from 'nanoviews' const $color = signal('white') button({ - [$$style]: { + [style$]: { color: $color, backgroundColor: 'black' } @@ -206,50 +206,50 @@ button({ ) ``` -### $$autoFocus +### autoFocus$ -`$$autoFocus` is an effect attribute that sets the auto focus on the element. +`autoFocus$` is an effect attribute that sets the auto focus on the element. ```js -import { input, $$autoFocus } from 'nanoviews' +import { input, autoFocus$ } from 'nanoviews' input({ type: 'text', - [$$autoFocus]: true + [autoFocus$]: true }) ``` -### $$value +### value$ -`$$value` is an effect attribute that manages the value of text inputs. +`value$` is an effect attribute that manages the value of text inputs. ```js import { signal } from 'nanoviews/store' -import { textarea, $$value } from 'nanoviews' +import { textarea, value$ } from 'nanoviews' const $review = signal('') textarea({ name: 'review', - [$$value]: $review + [value$]: $review })( 'Write your review here' ) ``` -### $$checked +### checked$ -`$$checked` is an effect attribute that manages the checked state of checkboxes and radio buttons. +`checked$` is an effect attribute that manages the checked state of checkboxes and radio buttons. ```js import { signal } from 'nanoviews/store' -import { input, $$checked, Indeterminate } from 'nanoviews' +import { input, checked$, Indeterminate } from 'nanoviews' const $checked = signal(false) input({ type: 'checkbox', - [$$checked]: $checked + [checked$]: $checked }) ``` @@ -259,19 +259,19 @@ Also you can manage [indeterminate state of checkboxes](https://developer.mozill $checked(Indeterminate) ``` -### $$selected +### selected$ -`$$selected` is an effect attribute that manages the selected state of select's options. +`selected$` is an effect attribute that manages the selected state of select's options. ```js import { signal } from 'nanoviews/store' -import { select, option, $$selected } from 'nanoviews' +import { select, option, selected$ } from 'nanoviews' const $selected = signal('mid') select({ name: 'player-pos', - [$$selected]: $selected + [selected$]: $selected })( option({ value: 'carry' @@ -298,7 +298,7 @@ const $selected = signal(['mid', 'carry']) select({ name: 'player-pos', - [$$selected]: $selected + [selected$]: $selected })( option({ value: 'carry' @@ -318,19 +318,19 @@ select({ ) ``` -### $$files +### files$ -`$$files` is an effect attribute that can provide the files of file inputs. +`files$` is an effect attribute that can provide the files of file inputs. ```js import { signal } from 'nanoviews/store' -import { input, $$files } from 'nanoviews' +import { input, files$ } from 'nanoviews' const $files = signal([]) input({ type: 'file', - [$$files]: $files + [files$]: $files }) ``` diff --git a/packages/nanoviews/src/elements/autoFocus.spec.ts b/packages/nanoviews/src/elements/autoFocus.spec.ts index 6bfdc944..4c869a64 100644 --- a/packages/nanoviews/src/elements/autoFocus.spec.ts +++ b/packages/nanoviews/src/elements/autoFocus.spec.ts @@ -17,7 +17,7 @@ const { describe('nanoviews', () => { describe('elements', () => { - describe('$$autoFocus', () => { + describe('autoFocus$', () => { it('should focus element by static value', () => { render(StaticValue()) diff --git a/packages/nanoviews/src/elements/autoFocus.stories.ts b/packages/nanoviews/src/elements/autoFocus.stories.ts index 9bab5230..931e70bb 100644 --- a/packages/nanoviews/src/elements/autoFocus.stories.ts +++ b/packages/nanoviews/src/elements/autoFocus.stories.ts @@ -5,7 +5,7 @@ import { } from '@nanoviews/storybook' import { signal } from 'kida' import { textarea } from './elements.js' -import { $$autoFocus } from './autoFocus.js' +import { autoFocus$ } from './autoFocus.js' const meta: Meta = { title: 'Elements/Effect Attributes/Auto Focus' @@ -17,12 +17,12 @@ type Story = StoryObj export const StaticValue: Story = { render: nanoStory(() => textarea({ - [$$autoFocus]: true + [autoFocus$]: true })('Hello, world!')) } export const ReactiveValue: Story = { render: nanoStory(() => textarea({ - [$$autoFocus]: signal(true) + [autoFocus$]: signal(true) })('Hello, world!')) } diff --git a/packages/nanoviews/src/elements/autoFocus.ts b/packages/nanoviews/src/elements/autoFocus.ts index ae1841e8..abbca2b5 100644 --- a/packages/nanoviews/src/elements/autoFocus.ts +++ b/packages/nanoviews/src/elements/autoFocus.ts @@ -8,8 +8,8 @@ import { createEffectAttribute } from '../internals/index.js' /** * Effect attribute to set auto focus on element */ -export const $$autoFocus = /* @__PURE__ */ createEffectAttribute<'$$autoFocus', HTMLElement | SVGElement, ValueOrAccessor>( - '$$autoFocus', +export const autoFocus$ = /* @__PURE__ */ createEffectAttribute<'autoFocus$', HTMLElement | SVGElement, ValueOrAccessor>( + 'autoFocus$', (element, $value) => { if (isAccessor($value) && $value() || $value) { effect(() => { @@ -21,10 +21,10 @@ export const $$autoFocus = /* @__PURE__ */ createEffectAttribute<'$$autoFocus', declare module 'nanoviews' { interface EffectAttributeValues { - $$autoFocus: ValueOrAccessor + autoFocus$: ValueOrAccessor } interface EffectAttributeTargets { - $$autoFocus: HTMLElement | SVGElement + autoFocus$: HTMLElement | SVGElement } } diff --git a/packages/nanoviews/src/elements/classList.spec.ts b/packages/nanoviews/src/elements/classList.spec.ts index 390a5e95..105e25f0 100644 --- a/packages/nanoviews/src/elements/classList.spec.ts +++ b/packages/nanoviews/src/elements/classList.spec.ts @@ -15,7 +15,7 @@ const { describe('nanoviews', () => { describe('elements', () => { - describe('$$classList', () => { + describe('classList$', () => { it('should render static class list', () => { const { container } = render(StaticValue()) diff --git a/packages/nanoviews/src/elements/classList.stories.ts b/packages/nanoviews/src/elements/classList.stories.ts index d896c8ea..de61cb71 100644 --- a/packages/nanoviews/src/elements/classList.stories.ts +++ b/packages/nanoviews/src/elements/classList.stories.ts @@ -8,7 +8,7 @@ import { button, div } from './elements.js' -import { $$classList } from './classList.js' +import { classList$ } from './classList.js' const meta: Meta = { title: 'Elements/Effect Attributes/Class List' @@ -20,7 +20,7 @@ type Story = StoryObj export const StaticValue: Story = { render: nanoStory(() => div({ - [$$classList]: [ + [classList$]: [ 'class1', false, 'class3' @@ -45,7 +45,7 @@ export const ReactiveValue: StoryObj<{ rounded: false }, render: nanoStory(({ primary, rounded }) => button({ - [$$classList]: [ + [classList$]: [ 'button', when(primary, 'primary', 'regular'), when(rounded, 'rounded') diff --git a/packages/nanoviews/src/elements/classList.ts b/packages/nanoviews/src/elements/classList.ts index 5c93353f..e0f77f5b 100644 --- a/packages/nanoviews/src/elements/classList.ts +++ b/packages/nanoviews/src/elements/classList.ts @@ -29,8 +29,8 @@ function cx(parts: unknown[]) { /** * Effect attribute to set class list on element */ -export const $$classList = /* @__PURE__ */ createEffectAttribute<'$$classList', HTMLElement, ClassList>( - '$$classList', +export const classList$ = /* @__PURE__ */ createEffectAttribute<'classList$', HTMLElement, ClassList>( + 'classList$', (element, parts) => { subscribe( () => cx(parts.map($get)), @@ -42,10 +42,10 @@ export const $$classList = /* @__PURE__ */ createEffectAttribute<'$$classList', declare module 'nanoviews' { interface EffectAttributeValues { - $$classList: ClassList + classList$: ClassList } interface EffectAttributeTargets { - $$classList: HTMLElement + classList$: HTMLElement } } diff --git a/packages/nanoviews/src/elements/controls.spec.ts b/packages/nanoviews/src/elements/controls.spec.ts index 441f2d45..686acc08 100644 --- a/packages/nanoviews/src/elements/controls.spec.ts +++ b/packages/nanoviews/src/elements/controls.spec.ts @@ -26,7 +26,7 @@ const { describe('nanoviews', () => { describe('elements', () => { describe('controls', () => { - describe('$$value', () => { + describe('value$', () => { it('should handle value of text input', () => { const value = signal('Hello, world!') @@ -76,7 +76,7 @@ describe('nanoviews', () => { }) }) - describe('$$selected', () => { + describe('selected$', () => { it('should handle value of select', () => { const value = signal('green') @@ -126,7 +126,7 @@ describe('nanoviews', () => { }) }) - describe('$$checked', () => { + describe('checked$', () => { it('should handle checked state of checkbox', () => { const checked = signal(true) @@ -152,7 +152,7 @@ describe('nanoviews', () => { }) }) - describe('$$files', () => { + describe('files$', () => { it('should save files to signal', async () => { const files = signal([]) const user = userEvent.setup() diff --git a/packages/nanoviews/src/elements/controls.stories.ts b/packages/nanoviews/src/elements/controls.stories.ts index 0a276a83..40084375 100644 --- a/packages/nanoviews/src/elements/controls.stories.ts +++ b/packages/nanoviews/src/elements/controls.stories.ts @@ -13,10 +13,10 @@ import { } from './elements.js' import { Indeterminate, - $$value, - $$checked, - $$selected, - $$files + value$, + checked$, + selected$, + files$ } from './controls.js' const meta: Meta<{ @@ -52,7 +52,7 @@ export const TextInput: StoryObj<{ return input({ type: 'text', - [$$value]: value + [value$]: value }) }) } @@ -77,7 +77,7 @@ export const Textarea: StoryObj<{ } return textarea({ - [$$value]: value + [value$]: value })() }) } @@ -112,7 +112,7 @@ export const Select: StoryObj<{ } return select({ - [$$selected]: value + [selected$]: value })( option({ value: 'red' @@ -157,7 +157,7 @@ export const MultipleSelect: StoryObj<{ } return select({ - [$$selected]: values + [selected$]: values })( option({ value: 'red' @@ -203,7 +203,7 @@ export const Checkbox: StoryObj<{ return input({ type: 'checkbox', - [$$checked]: checked + [checked$]: checked }) }) } @@ -229,7 +229,7 @@ export const Files: StoryObj<{ return input({ type: 'file', - [$$files]: files + [files$]: files }) }) } diff --git a/packages/nanoviews/src/elements/controls.ts b/packages/nanoviews/src/elements/controls.ts index 9fca9ba8..69d57f29 100644 --- a/packages/nanoviews/src/elements/controls.ts +++ b/packages/nanoviews/src/elements/controls.ts @@ -77,8 +77,8 @@ function getValue(control: TextboxElement) { /** * Effect attribute to set and read text value of input element */ -export const $$value = /* @__PURE__ */ createEffectAttribute<'$$value', TextboxElement, Value>( - '$$value', +export const value$ = /* @__PURE__ */ createEffectAttribute<'value$', TextboxElement, Value>( + 'value$', createElementPropertySetter( onInputEvent, getValue, @@ -105,8 +105,8 @@ function getChecked(control: CheckboxElement): CheckedPrimitive { /** * Effect attribute to set and read checked value of checkbox or radio button element */ -export const $$checked = /* @__PURE__ */ createEffectAttribute<'$$checked', CheckboxElement, WritableSignal>( - '$$checked', +export const checked$ = /* @__PURE__ */ createEffectAttribute<'checked$', CheckboxElement, WritableSignal>( + 'checked$', createElementPropertySetter( onChangeEvent, getChecked, @@ -161,8 +161,8 @@ function getSelected(control: ComboboxElement): SelectedPrimitive { /** * Effect attribute to set and read selected value of combobox element */ -export const $$selected = /* @__PURE__ */ createEffectAttribute<'$$selected', ComboboxElement, WritableSignal>( - '$$selected', +export const selected$ = /* @__PURE__ */ createEffectAttribute<'selected$', ComboboxElement, WritableSignal>( + 'selected$', createElementPropertySetter( onChangeEvent, getSelected, @@ -186,23 +186,23 @@ function filesEffectAttribute( /** * Effect attribute to read files of file input element */ -export const $$files = /* @__PURE__ */ createEffectAttribute<'$$files', FileElement, Files>( - '$$files', +export const files$ = /* @__PURE__ */ createEffectAttribute<'files$', FileElement, Files>( + 'files$', filesEffectAttribute ) declare module 'nanoviews' { interface EffectAttributeValues { - $$value: Value - $$checked: Checked - $$selected: Selected - $$files: Files + value$: Value + checked$: Checked + selected$: Selected + files$: Files } interface EffectAttributeTargets { - $$value: TextboxElement - $$checked: CheckboxElement - $$selected: ComboboxElement - $$files: FileElement + value$: TextboxElement + checked$: CheckboxElement + selected$: ComboboxElement + files$: FileElement } } diff --git a/packages/nanoviews/src/elements/ref.spec.ts b/packages/nanoviews/src/elements/ref.spec.ts index 3e86c12a..975a958d 100644 --- a/packages/nanoviews/src/elements/ref.spec.ts +++ b/packages/nanoviews/src/elements/ref.spec.ts @@ -6,16 +6,16 @@ import { import { render } from '@nanoviews/testing-library' import { signal } from 'kida' import { button } from './elements.js' -import { $$ref } from './ref.js' +import { ref$ } from './ref.js' describe('nanoviews', () => { describe('elements', () => { - describe('$$ref', () => { + describe('ref$', () => { it('should set ref', () => { const ref = signal(null) render(() => button({ - [$$ref]: ref + [ref$]: ref })('Click me!')) expect(ref()).toBeInstanceOf(HTMLButtonElement) diff --git a/packages/nanoviews/src/elements/ref.ts b/packages/nanoviews/src/elements/ref.ts index 0a0ee740..f6f223b1 100644 --- a/packages/nanoviews/src/elements/ref.ts +++ b/packages/nanoviews/src/elements/ref.ts @@ -7,8 +7,8 @@ import { createEffectAttribute } from '../internals/index.js' /** * Effect attribute to get element reference */ -export const $$ref = /* @__PURE__ */ createEffectAttribute<'$$ref', Element, WritableSignal>( - '$$ref', +export const ref$ = /* @__PURE__ */ createEffectAttribute<'ref$', Element, WritableSignal>( + 'ref$', (element, $ref) => { $ref(element) @@ -18,10 +18,10 @@ export const $$ref = /* @__PURE__ */ createEffectAttribute<'$$ref', Element, Wri declare module 'nanoviews' { interface EffectAttributeValues { - $$ref: WritableSignal + ref$: WritableSignal } interface EffectAttributeTargets { - $$ref: Element + ref$: Element } } diff --git a/packages/nanoviews/src/elements/style.spec.ts b/packages/nanoviews/src/elements/style.spec.ts index 3c1eab4e..fbab0c62 100644 --- a/packages/nanoviews/src/elements/style.spec.ts +++ b/packages/nanoviews/src/elements/style.spec.ts @@ -15,7 +15,7 @@ const { describe('nanoviews', () => { describe('elements', () => { - describe('$$style', () => { + describe('style$', () => { it('should render static value', () => { const { container } = render(StaticValue()) diff --git a/packages/nanoviews/src/elements/style.stories.ts b/packages/nanoviews/src/elements/style.stories.ts index 7dc906b0..30293106 100644 --- a/packages/nanoviews/src/elements/style.stories.ts +++ b/packages/nanoviews/src/elements/style.stories.ts @@ -4,7 +4,7 @@ import { nanoStory } from '@nanoviews/storybook' import { div } from './elements.js' -import { $$style } from './style.js' +import { style$ } from './style.js' const meta: Meta<{ color: string @@ -18,7 +18,7 @@ type Story = StoryObj export const StaticValue: Story = { render: nanoStory(() => div({ - [$$style]: { + [style$]: { color: 'green' } })('Hello, world!')) @@ -29,7 +29,7 @@ export const ReactiveValue: Story = { color: 'green' }, render: nanoStory(({ color }) => div({ - [$$style]: { + [style$]: { color } })('Hello, world!')) diff --git a/packages/nanoviews/src/elements/style.ts b/packages/nanoviews/src/elements/style.ts index a6c7b959..a5391b5b 100644 --- a/packages/nanoviews/src/elements/style.ts +++ b/packages/nanoviews/src/elements/style.ts @@ -23,8 +23,8 @@ function setStyle( /** * Effect attribute to set style properties on element */ -export const $$style = /* @__PURE__ */ createEffectAttribute<'$$style', HTMLElement | SVGAElement, StyleProps>( - '$$style', +export const style$ = /* @__PURE__ */ createEffectAttribute<'style$', HTMLElement | SVGAElement, StyleProps>( + 'style$', (element, style) => { const keys = Object.keys(style) const len = keys.length @@ -41,10 +41,10 @@ export const $$style = /* @__PURE__ */ createEffectAttribute<'$$style', HTMLElem declare module 'nanoviews' { interface EffectAttributeValues { - $$style: StyleProps + style$: StyleProps } interface EffectAttributeTargets { - $$style: HTMLElement | SVGAElement + style$: HTMLElement | SVGAElement } } diff --git a/packages/nanoviews/src/flow/if.spec.ts b/packages/nanoviews/src/flow/if.spec.ts index 032d741a..2acbfb16 100644 --- a/packages/nanoviews/src/flow/if.spec.ts +++ b/packages/nanoviews/src/flow/if.spec.ts @@ -1,12 +1,18 @@ import { describe, it, - expect + expect, + expectTypeOf } from 'vitest' import { composeStories } from '@nanoviews/storybook' import { render } from '@nanoviews/testing-library' -import { signal } from 'kida' +import { + type WritableSignal, + type ReadableSignal, + signal +} from 'kida' import * as Stories from './if.stories.js' +import { if_ } from './if.js' const { StaticValue, @@ -48,6 +54,67 @@ describe('nanoviews', () => { expect(container.innerHTML).toBe('
') }) + + it('should keep signal type in branches', () => { + const $value = signal('truthy') + + if_($value)( + ($truthy) => { + expectTypeOf($truthy).toExtend>() + return null + }, + ($falsy) => { + expectTypeOf($falsy).toExtend>() + return null + } + ) + }) + + it('should narrow value type of union signal in branches', () => { + const $value = signal('truthy') + + if_($value)( + ($truthy) => { + expectTypeOf($truthy()).toEqualTypeOf() + expectTypeOf($truthy).toExtend>() + return null + }, + ($falsy) => { + expectTypeOf($falsy()).toEqualTypeOf() + return null + } + ) + }) + + it('should narrow boolean signal value to literals in branches', () => { + const $value = signal(true) + + if_($value)( + ($truthy) => { + expectTypeOf($truthy()).toEqualTypeOf() + return null + }, + ($falsy) => { + expectTypeOf($falsy()).toEqualTypeOf() + return null + } + ) + }) + + it('should narrow static union value in branches', () => { + const value = 'truthy' as string | null + + if_(value)( + (truthy) => { + expectTypeOf(truthy).toEqualTypeOf() + return null + }, + (falsy) => { + expectTypeOf(falsy).toEqualTypeOf() + return null + } + ) + }) }) }) }) diff --git a/todo.txt b/todo.txt index cdb0d2e7..4f4b9b7c 100644 --- a/todo.txt +++ b/todo.txt @@ -43,8 +43,6 @@ ## Nanoviews -- fix TruthyValueOrSignal type, return signal if signal, not accessor -- restore effect attributes naming to name$ ?) - hooks/effects naming convention - check old version -> defer effects wroked bottom->top, now it works top -> bottom -> its ok for stores, not ok for views