Merge branch 'main' into blockless

blockless
Rich Harris 10 months ago committed by GitHub
commit 4e9cf14194
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: don't clear date input on temporarily invalid value

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: use safe-equals comparison for `@const` tags in legacy mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve proxy effect dependency tracking

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: prevent window listeners from triggering events twice

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: allow dynamic `type` attribute with `bind:value`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve event delegation with shadowed bindings

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add reactive Date object to svelte/reactivity

@ -20,6 +20,7 @@
"big-eggs-flash",
"big-eyes-carry",
"big-geese-act",
"blue-rules-juggle",
"blue-timers-film",
"brave-points-sleep",
"brave-shrimps-kiss",
@ -29,6 +30,7 @@
"bright-snakes-sing",
"brown-months-fry",
"brown-spoons-boil",
"chatty-beans-divide",
"chatty-cups-drop",
"chatty-taxis-juggle",
"chilled-pumas-invite",
@ -38,6 +40,7 @@
"clever-chefs-relate",
"clever-rockets-burn",
"cold-birds-own",
"cold-masks-learn",
"cool-ants-leave",
"cool-rabbits-tickle",
"cool-roses-trade",
@ -83,6 +86,7 @@
"forty-peaches-unite",
"forty-suns-smile",
"four-flies-hammer",
"fresh-impalas-bow",
"fresh-weeks-trade",
"friendly-candles-relate",
"friendly-lies-camp",
@ -92,6 +96,7 @@
"gentle-spies-happen",
"giant-moons-own",
"giant-roses-press",
"good-buses-reply",
"good-cars-visit",
"good-pianos-jump",
"good-rivers-yawn",
@ -136,6 +141,7 @@
"light-days-clean",
"light-humans-hang",
"light-pens-watch",
"little-pans-jog",
"long-buckets-lay",
"long-crews-return",
"long-lobsters-mate",
@ -151,12 +157,14 @@
"moody-carrots-lay",
"moody-frogs-exist",
"moody-owls-cry",
"moody-sheep-type",
"nasty-lions-double",
"nasty-yaks-peel",
"neat-boats-shake",
"neat-dingos-clap",
"nervous-spoons-relax",
"new-boats-wait",
"new-rabbits-flow",
"ninety-dingos-walk",
"odd-buckets-lie",
"odd-needles-joke",
@ -184,6 +192,7 @@
"pretty-ties-help",
"purple-dragons-peel",
"quiet-apricots-dream",
"quiet-berries-end",
"quiet-camels-mate",
"quiet-crabs-nail",
"quiet-timers-speak",
@ -202,6 +211,7 @@
"rotten-bags-type",
"rotten-buckets-develop",
"rotten-experts-relax",
"rude-ghosts-tickle",
"selfish-dragons-knock",
"selfish-tools-hide",
"serious-kids-deliver",
@ -301,6 +311,7 @@
"wicked-doors-train",
"wicked-hairs-cheer",
"wild-foxes-wonder",
"wise-apples-care",
"wise-dancers-hang",
"wise-donkeys-marry",
"wise-jobs-admire",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: correctly handle proxied signal writes before reads

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve deep_read performance

@ -1,5 +1,39 @@
# svelte
## 5.0.0-next.68
### Patch Changes
- fix: improve deep_read performance ([#10624](https://github.com/sveltejs/svelte/pull/10624))
## 5.0.0-next.67
### Patch Changes
- fix: improve event delegation with shadowed bindings ([#10620](https://github.com/sveltejs/svelte/pull/10620))
- feat: add reactive Date object to svelte/reactivity ([#10622](https://github.com/sveltejs/svelte/pull/10622))
## 5.0.0-next.66
### Patch Changes
- fix: don't clear date input on temporarily invalid value ([#10616](https://github.com/sveltejs/svelte/pull/10616))
- fix: use safe-equals comparison for `@const` tags in legacy mode ([#10606](https://github.com/sveltejs/svelte/pull/10606))
- fix: improve proxy effect dependency tracking ([#10605](https://github.com/sveltejs/svelte/pull/10605))
- fix: prevent window listeners from triggering events twice ([#10611](https://github.com/sveltejs/svelte/pull/10611))
- feat: allow dynamic `type` attribute with `bind:value` ([#10608](https://github.com/sveltejs/svelte/pull/10608))
- fix: make `bind_this` implementation more robust ([#10598](https://github.com/sveltejs/svelte/pull/10598))
- fix: tweak initial `bind:clientWidth/clientHeight/offsetWidth/offsetHeight` update timing ([#10512](https://github.com/sveltejs/svelte/pull/10512))
- fix: correctly handle proxied signal writes before reads ([#10612](https://github.com/sveltejs/svelte/pull/10612))
## 5.0.0-next.65
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.65",
"version": "5.0.0-next.68",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -62,6 +62,10 @@
"types": "./types/index.d.ts",
"default": "./src/motion/index.js"
},
"./reactivity": {
"types": "./types/index.d.ts",
"default": "./src/reactivity/index.js"
},
"./server": {
"types": "./types/index.d.ts",
"default": "./src/server/index.js"

@ -176,6 +176,12 @@ function get_delegated_event(event_name, handler, context) {
return non_hoistable;
}
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
// If we are referencing a binding that is shadowed in another scope then bail out.
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
return non_hoistable;
}
// If we have multiple references to the same store using $ prefix, bail out.
if (

@ -435,7 +435,10 @@ const validation = {
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')
);
if (type && !is_text_attribute(type)) {
error(type, 'invalid-type-attribute');
if (node.name !== 'value' || type.value === true) {
error(type, 'invalid-type-attribute');
}
return; // bind:value can handle dynamic `type` attributes
}
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {

@ -1793,7 +1793,8 @@ export const template_visitors = {
b.const(
declaration.id,
b.call(
'$.derived',
// In runes mode, we want things to be fine-grained - but not in legacy mode
state.options.runes ? '$.derived' : '$.derived_safe_equal',
b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init)))
)
)
@ -1822,7 +1823,10 @@ export const template_visitors = {
])
);
state.init.push(b.const(tmp, b.call('$.derived', fn)));
state.init.push(
// In runes mode, we want things to be fine-grained - but not in legacy mode
b.const(tmp, b.call(state.options.runes ? '$.derived' : '$.derived_safe_equal', fn))
);
for (const node of identifiers) {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.name));

@ -2,10 +2,11 @@ import { DEV } from 'esm-env';
import {
get,
set,
update,
updating_derived,
batch_inspect,
current_component_context
current_component_context,
untrack,
set_signal_value
} from './runtime.js';
import { effect_active } from './reactivity/computations.js';
import {
@ -148,6 +149,15 @@ export function unstate(value) {
);
}
/**
* @param {import('./types.js').Signal<number>} signal
* @param {1 | -1} [d]
*/
function update_version(signal, d = 1) {
const value = untrack(() => get(signal));
set_signal_value(signal, value + d);
}
/** @type {ProxyHandler<import('./types.js').ProxyStateObject<any>>} */
const state_proxy_handler = {
defineProperty(target, prop, descriptor) {
@ -182,7 +192,9 @@ const state_proxy_handler = {
}
if (s !== undefined) set(s, UNINITIALIZED);
if (boolean) update(metadata.v);
if (boolean) {
update_version(metadata.v);
}
return boolean;
},
@ -256,10 +268,21 @@ const state_proxy_handler = {
return has;
},
set(target, prop, value) {
set(target, prop, value, receiver) {
const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop);
if (s !== undefined) set(s, proxy(value, metadata.i, metadata.o));
let s = metadata.s.get(prop);
// If we haven't yet created a source for this property, we need to ensure
// we do so otherwise if we read it later, then the write won't be tracked and
// the heuristics of effects will be different vs if we had read the proxied
// object property before writing to that property.
if (s === undefined && effect_active()) {
// the read creates a signal
untrack(() => receiver[prop]);
s = metadata.s.get(prop);
}
if (s !== undefined) {
set(s, proxy(value, metadata.i, metadata.o));
}
const is_array = metadata.a;
const not_has = !(prop in target);
@ -291,8 +314,7 @@ const state_proxy_handler = {
set(ls, length);
}
}
update(metadata.v);
update_version(metadata.v);
}
return true;

@ -1008,6 +1008,12 @@ export function selected(dom) {
*/
export function bind_value(dom, get_value, update) {
dom.addEventListener('input', () => {
if (DEV && dom.type === 'checkbox') {
throw new Error(
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
);
}
/** @type {any} */
let value = dom.value;
if (is_numberlike_input(dom)) {
@ -1017,6 +1023,12 @@ export function bind_value(dom, get_value, update) {
});
render_effect(() => {
if (DEV && dom.type === 'checkbox') {
throw new Error(
'Using bind:value together with a checkbox input is not allowed. Use bind:checked instead'
);
}
const value = get_value();
// @ts-ignore
dom.__value = value;
@ -1026,6 +1038,12 @@ export function bind_value(dom, get_value, update) {
return;
}
if (dom.type === 'date' && !value && !dom.value) {
// Handles the case where a temporarily invalid date is set (while typing, for example with a leading 0 for the day)
// and prevents this state from clearing the other parts of the date input (see https://github.com/sveltejs/svelte/issues/7897)
return;
}
dom.value = stringify(value);
});
}
@ -1373,6 +1391,7 @@ export function delegate(events) {
* @returns {void}
*/
function handle_event_propagation(handler_element, event) {
const owner_document = handler_element.ownerDocument;
const event_name = event.type;
const path = event.composedPath?.() || [];
let current_target = /** @type {null | Element} */ (path[0] || event.target);
@ -1392,12 +1411,15 @@ function handle_event_propagation(handler_element, event) {
const handled_at = event.__root;
if (handled_at) {
const at_idx = path.indexOf(handled_at);
if (at_idx !== -1 && handler_element === document) {
// This is the fallback document listener but the event was already handled
// -> ignore, but set handle_at to document so that we're resetting the event
if (
at_idx !== -1 &&
(handler_element === document || handler_element === /** @type {any} */ (window))
) {
// This is the fallback document listener or a window listener, but the event was already handled
// -> ignore, but set handle_at to document/window so that we're resetting the event
// chain in case someone manually dispatches the same event object again.
// @ts-expect-error
event.__root = document;
event.__root = handler_element;
return;
}
// We're deliberately not skipping if the index is higher, because
@ -1423,8 +1445,7 @@ function handle_event_propagation(handler_element, event) {
define_property(event, 'currentTarget', {
configurable: true,
get() {
// TODO: ensure correct document?
return current_target || document;
return current_target || owner_document;
}
});
@ -1576,12 +1597,12 @@ export function out(dom, get_transition_fn, props, global = false) {
export function action(dom, action, value_fn) {
/** @type {undefined | import('./types.js').ActionPayload<P>} */
let payload = undefined;
let needs_deep_read = false;
// Action could come from a prop, therefore could be a signal, therefore untrack
// TODO we could take advantage of this and enable https://github.com/sveltejs/svelte/issues/6942
effect(() => {
if (value_fn) {
const value = value_fn();
let needs_deep_read = false;
untrack(() => {
if (payload === undefined) {
payload = action(dom, value) || {};
@ -2187,7 +2208,7 @@ export function hydrate(component, options) {
);
remove(hydration_fragment);
first_child.remove();
hydration_fragment.at(-1)?.nextSibling?.remove();
hydration_fragment[hydration_fragment.length - 1]?.nextSibling?.remove();
set_current_hydration_fragment(null);
return mount(component, options);
} else {

@ -726,6 +726,7 @@ export function get(signal) {
current_untracked_writes !== null &&
current_effect !== null &&
(current_effect.f & CLEAN) !== 0 &&
(current_effect.f & MANAGED) === 0 &&
current_untracked_writes.includes(signal)
) {
set_signal_status(current_effect, DIRTY);
@ -893,7 +894,8 @@ export function set_signal_value(signal, value) {
!ignore_mutation_validation &&
current_effect !== null &&
current_effect.c === null &&
(current_effect.f & CLEAN) !== 0
(current_effect.f & CLEAN) !== 0 &&
(current_effect.f & MANAGED) === 0
) {
if (current_dependencies !== null && current_dependencies.includes(signal)) {
set_signal_status(current_effect, DIRTY);
@ -1169,7 +1171,13 @@ export function pop(component) {
* @returns {void}
*/
export function deep_read(value, visited = new Set()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
if (
typeof value === 'object' &&
value !== null &&
// We don't want to traverse DOM elements
!(value instanceof EventTarget) &&
!visited.has(value)
) {
visited.add(value);
for (let key in value) {
try {
@ -1249,7 +1257,7 @@ export function inspect(get_value, inspect = console.log) {
pre_effect(() => {
const fn = () => {
const value = get_value().map((v) => deep_unstate(v));
const value = untrack(() => get_value().map((v) => deep_unstate(v)));
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
// eslint-disable-next-line no-console
console.warn(

@ -0,0 +1,103 @@
import { source } from '../internal/client/reactivity/sources.js';
import { get, set } from '../internal/client/runtime.js';
/** @type {Array<keyof Date>} */
const read = [
'getDate',
'getDay',
'getFullYear',
'getHours',
'getMilliseconds',
'getMinutes',
'getMonth',
'getSeconds',
'getTime',
'getTimezoneOffset',
'getUTCDate',
'getUTCDay',
'getUTCFullYear',
'getUTCHours',
'getUTCMilliseconds',
'getUTCMinutes',
'getUTCMonth',
'getUTCSeconds',
// @ts-expect-error this is deprecated
'getYear',
'toDateString',
'toISOString',
'toJSON',
'toLocaleDateString',
'toLocaleString',
'toLocaleTimeString',
'toString',
'toTimeString',
'toUTCString'
];
/** @type {Array<keyof Date>} */
const write = [
'setDate',
'setFullYear',
'setHours',
'setMilliseconds',
'setMinutes',
'setMonth',
'setSeconds',
'setTime',
'setUTCDate',
'setUTCFullYear',
'setUTCHours',
'setUTCMilliseconds',
'setUTCMinutes',
'setUTCMonth',
'setUTCSeconds',
// @ts-expect-error this is deprecated
'setYear'
];
class ReactiveDate extends Date {
#raw_time = source(super.getTime());
static #inited = false;
// We init as part of the first instance so that we can treeshake this class
#init() {
if (!ReactiveDate.#inited) {
ReactiveDate.#inited = true;
const proto = ReactiveDate.prototype;
const date_proto = Date.prototype;
for (const method of read) {
// @ts-ignore
proto[method] = function () {
get(this.#raw_time);
// @ts-ignore
return date_proto[method].call(this);
};
}
for (const method of write) {
// @ts-ignore
proto[method] = function (/** @type {any} */ ...args) {
// @ts-ignore
const v = date_proto[method].apply(this, args);
const time = date_proto.getTime.call(this);
if (time !== this.#raw_time.v) {
set(this.#raw_time, time);
}
return v;
};
}
}
}
/**
* @param {any[]} values
*/
constructor(...values) {
// @ts-ignore
super(...values);
this.#init();
}
}
export { ReactiveDate as Date };

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.65';
export const VERSION = '5.0.0-next.68';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { test } from '../../test';
// Test ensures that the `const` tag is coarse-grained in legacy mode (i.e. always fires an update when the array changes)
export default test({
html: `
<button>Show</button>
<p>0</p>
<p>1</p>
<p>2</p>
<p>3</p>
`,
async test({ target, assert }) {
const btn = target.querySelector('button');
btn?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Show</button>
<p>0 show (v_item) show (item)</p>
<p>1</p>
<p>2 show (v_item) show (item)</p>
<p>3</p>
`
);
}
});

@ -0,0 +1,21 @@
<script>
export let items = {0:{clicked:false},length:4};
</script>
<button on:click={()=>{
items[0].clicked=true;
items[2]={clicked:true};
}}>Show</button>
{#each items as item, i}
{@const v_item=item}
<p>
{i}
{#if v_item?.clicked}
show (v_item)
{/if}
{#if item?.clicked}
show (item)
{/if}
</p>
{/each}

@ -0,0 +1,72 @@
import { tick } from 'svelte';
import { test, ok } from '../../test';
export default test({
html: `
<input type=text>
<input type=text>
<p>x / y</p>
<button>change to text</button>
<button>change to number</button>
<button>change to range</button>
`,
ssrHtml: `
<input type=text value=x>
<input type=text value=y>
<p>x / y</p>
<button>change to text</button>
<button>change to number</button>
<button>change to range</button>
`,
async test({ assert, target }) {
const [in1, in2] = target.querySelectorAll('input');
const [btn1, btn2, btn3] = target.querySelectorAll('button');
const p = target.querySelector('p');
ok(p);
in1.value = '0';
in2.value = '1';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
btn2?.click();
await tick();
assert.htmlEqual(p.innerHTML, '0 / 1');
in1.stepUp();
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.stepUp();
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(p.innerHTML, '1 / 2');
btn1?.click();
await tick();
try {
in1.stepUp();
assert.fail();
} catch (e) {
// expected
}
btn3?.click();
await tick();
in1.stepUp();
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.stepUp();
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(p.innerHTML, '2 / 3');
btn1?.click();
await tick();
in1.value = 'a';
in2.value = 'b';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(p.innerHTML, 'a / b');
}
});

@ -0,0 +1,14 @@
<script>
let dynamic = $state('x');
let spread = $state('y');
let inputType = $state('text');
let props = $derived({type: inputType});
</script>
<input bind:value={dynamic} type={inputType}>
<input bind:value={spread} {...props}>
<p>{dynamic} / {spread}</p>
<button onclick={() => inputType = 'text'}>change to text</button>
<button onclick={() => inputType = 'number'}>change to number</button>
<button onclick={() => inputType = 'range'}>change to range</button>

@ -0,0 +1,37 @@
import { flushSync } from '../../../../src/main/main-client';
import { test } from '../../test';
export default test({
html: `<div>getSeconds: 0</div><div>getMinutes: 0</div><div>getHours: 15</div><div>getTime: 1708700400000</div><div>toDateString: Fri Feb 23 2024</div><button>1 second</button><button>1 minute</button><button>1 hour</button>`,
test({ assert, target }) {
const [btn, btn2, btn3] = target.querySelectorAll('button');
flushSync(() => {
btn?.click();
});
assert.htmlEqual(
target.innerHTML,
`<div>getSeconds: 1</div><div>getMinutes: 0</div><div>getHours: 15</div><div>getTime: 1708700401000</div><div>toDateString: Fri Feb 23 2024</div><button>1 second</button><button>1 minute</button><button>1 hour</button>`
);
flushSync(() => {
btn2?.click();
});
assert.htmlEqual(
target.innerHTML,
`<div>getSeconds: 1</div><div>getMinutes: 1</div><div>getHours: 15</div><div>getTime: 1708700461000</div><div>toDateString: Fri Feb 23 2024</div><button>1 second</button><button>1 minute</button><button>1 hour</button>`
);
flushSync(() => {
btn3?.click();
});
assert.htmlEqual(
target.innerHTML,
`<div>getSeconds: 1</div><div>getMinutes: 1</div><div>getHours: 16</div><div>getTime: 1708704061000</div><div>toDateString: Fri Feb 23 2024</div><button>1 second</button><button>1 minute</button><button>1 hour</button>`
);
}
});

@ -0,0 +1,21 @@
<script>
import { Date } from 'svelte/reactivity';
let date = new Date('2024/02/23 15:00:00');
</script>
<div>getSeconds: {date.getSeconds()}</div>
<div>getMinutes: {date.getMinutes()}</div>
<div>getHours: {date.getHours()}</div>
<div>getTime: {date.getTime()}</div>
<div>toDateString: {date.toDateString()}</div>
<button onclick={() => {
date.setSeconds(date.getSeconds() + 1);
}}>1 second</button>
<button onclick={() => {
date.setMinutes(date.getMinutes() + 1);
}}>1 minute</button>
<button onclick={() => {
date.setHours(date.getHours() + 1);
}}>1 hour</button>

@ -0,0 +1,16 @@
import { test } from '../../test';
import { log } from './log.js';
export default test({
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
await Promise.resolve();
assert.deepEqual(log, ['method']);
}
});

@ -0,0 +1,12 @@
<script>
import { log } from './log.js';
let method = $state('method');
function submitPay() {
log.push(method);
}
let methods = [{method:1}];
</script>
{#each methods as {method}}
<button onclick={submitPay}>{method}</button>
{/each}

@ -0,0 +1,12 @@
<script>
import { getContext } from 'svelte';
let context = getContext('container');
$effect(() => {
context.register('test');
return () => context.unregister('test');
});
</script>
<div>Item</div>

@ -0,0 +1,9 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
flushSync();
assert.htmlEqual(target.innerHTML, `<div>Item</div>`);
}
});

@ -0,0 +1,14 @@
<script>
import { setContext } from 'svelte';
import Item from './Item.svelte'
let items = $state({});
setContext('container', {
register: (id) => items[id] = true,
unregister: (id) => delete items[id]
});
</script>
<Item />

@ -8,6 +8,7 @@ import {
} from '../../src/internal/client/reactivity/computations';
import { source } from '../../src/internal/client/reactivity/sources';
import type { Computation } from '../../src/internal/client/types';
import { proxy } from '../../src/internal/client/proxy';
/**
* @param runes runes mode
@ -332,4 +333,25 @@ describe('signals', () => {
assert.equal(errored, true);
};
});
test('schedules rerun when writing to signal before reading it', (runes) => {
if (!runes) return () => {};
const value = proxy({ arr: [] });
user_effect(() => {
value.arr = [];
value.arr;
});
return () => {
let errored = false;
try {
$.flushSync();
} catch (e: any) {
assert.include(e.message, 'ERR_SVELTE_TOO_MANY_UPDATES');
errored = true;
}
assert.equal(errored, true);
};
});
});

@ -1,14 +0,0 @@
[
{
"code": "invalid-type-attribute",
"message": "'type' attribute must be a static text value if input uses two-way binding",
"start": {
"line": 6,
"column": 24
},
"end": {
"line": 6,
"column": 40
}
}
]

@ -1,6 +0,0 @@
<script>
let foo;
let inputType;
</script>
<input bind:value={foo} type={inputType}>
Loading…
Cancel
Save