svelte
Version:
Cybernetically enhanced web apps
261 lines (231 loc) • 6.85 kB
JavaScript
/** @import { CallExpression, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { binding_properties } from '../../../bindings.js';
import { build_attribute_value } from './shared/element.js';
import { build_bind_this, validate_binding } from './shared/utils.js';
/**
* @param {AST.BindDirective} node
* @param {ComponentContext} context
*/
export function BindDirective(node, context) {
const expression = /** @type {Expression} */ (context.visit(node.expression));
const property = binding_properties[node.name];
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
let get, set;
if (expression.type === 'SequenceExpression') {
[get, set] = expression.expressions;
} else {
if (
dev &&
context.state.analysis.runes &&
expression.type === 'MemberExpression' &&
(node.name !== 'this' ||
context.path.some(
({ type }) =>
type === 'IfBlock' ||
type === 'EachBlock' ||
type === 'AwaitBlock' ||
type === 'KeyBlock'
)) &&
!is_ignored(node, 'binding_property_non_reactive')
) {
validate_binding(context.state, node, expression);
}
get = b.thunk(expression);
/** @type {Expression | undefined} */
set = b.unthunk(
b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (
context.visit(
b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value'))
)
)
)
);
if (get === set) {
set = undefined;
}
}
/** @type {CallExpression} */
let call;
if (property?.event) {
call = b.call(
'$.bind_property',
b.literal(node.name),
b.literal(property.event),
context.state.node,
set ?? get,
property.bidirectional && get
);
} else {
// special cases
switch (node.name) {
// window
case 'online':
call = b.call(`$.bind_online`, set ?? get);
break;
case 'scrollX':
case 'scrollY':
call = b.call(
'$.bind_window_scroll',
b.literal(node.name === 'scrollX' ? 'x' : 'y'),
get,
set
);
break;
case 'innerWidth':
case 'innerHeight':
case 'outerWidth':
case 'outerHeight':
call = b.call('$.bind_window_size', b.literal(node.name), set ?? get);
break;
// document
case 'activeElement':
call = b.call('$.bind_active_element', set ?? get);
break;
// media
case 'muted':
call = b.call(`$.bind_muted`, context.state.node, get, set);
break;
case 'paused':
call = b.call(`$.bind_paused`, context.state.node, get, set);
break;
case 'volume':
call = b.call(`$.bind_volume`, context.state.node, get, set);
break;
case 'playbackRate':
call = b.call(`$.bind_playback_rate`, context.state.node, get, set);
break;
case 'currentTime':
call = b.call(`$.bind_current_time`, context.state.node, get, set);
break;
case 'buffered':
call = b.call(`$.bind_buffered`, context.state.node, set ?? get);
break;
case 'played':
call = b.call(`$.bind_played`, context.state.node, set ?? get);
break;
case 'seekable':
call = b.call(`$.bind_seekable`, context.state.node, set ?? get);
break;
case 'seeking':
call = b.call(`$.bind_seeking`, context.state.node, set ?? get);
break;
case 'ended':
call = b.call(`$.bind_ended`, context.state.node, set ?? get);
break;
case 'readyState':
call = b.call(`$.bind_ready_state`, context.state.node, set ?? get);
break;
// dimensions
case 'contentRect':
case 'contentBoxSize':
case 'borderBoxSize':
case 'devicePixelContentBoxSize':
call = b.call(
'$.bind_resize_observer',
context.state.node,
b.literal(node.name),
set ?? get
);
break;
case 'clientWidth':
case 'clientHeight':
case 'offsetWidth':
case 'offsetHeight':
call = b.call('$.bind_element_size', context.state.node, b.literal(node.name), set ?? get);
break;
// various
case 'value': {
if (parent?.type === 'RegularElement' && parent.name === 'select') {
call = b.call(`$.bind_select_value`, context.state.node, get, set);
} else {
call = b.call(`$.bind_value`, context.state.node, get, set);
}
break;
}
case 'files':
call = b.call(`$.bind_files`, context.state.node, get, set);
break;
case 'this':
call = build_bind_this(node.expression, context.state.node, context);
break;
case 'textContent':
case 'innerHTML':
case 'innerText':
call = b.call(
'$.bind_content_editable',
b.literal(node.name),
context.state.node,
get,
set
);
break;
// checkbox/radio
case 'checked':
call = b.call(`$.bind_checked`, context.state.node, get, set);
break;
case 'focused':
call = b.call(`$.bind_focused`, context.state.node, set ?? get);
break;
case 'group': {
const indexes = node.metadata.parent_each_blocks.map((each) => {
// if we have a keyed block with an index, the index is wrapped in a source
return each.metadata.keyed && each.index
? b.call('$.get', each.metadata.index)
: each.metadata.index;
});
// We need to additionally invoke the value attribute signal to register it as a dependency,
// so that when the value is updated, the group binding is updated
let group_getter = get;
if (parent?.type === 'RegularElement') {
const value = /** @type {any[]} */ (
/** @type {AST.Attribute} */ (
parent.attributes.find(
(a) =>
a.type === 'Attribute' &&
a.name === 'value' &&
!is_text_attribute(a) &&
a.value !== true
)
)?.value
);
if (value !== undefined) {
group_getter = b.thunk(
b.block([b.stmt(build_attribute_value(value, context).value), b.return(expression)])
);
}
}
call = b.call(
'$.bind_group',
node.metadata.binding_group_name,
b.array(indexes),
context.state.node,
group_getter,
set ?? get
);
break;
}
default:
throw new Error('unknown binding ' + node.name);
}
}
// Bindings need to happen after attribute updates, therefore after the render effect, and in order with events/actions.
// bind:this is a special case as it's one-way and could influence the render effect.
if (node.name === 'this') {
context.state.init.push(b.stmt(call));
} else {
const has_use =
parent.type === 'RegularElement' && parent.attributes.find((a) => a.type === 'UseDirective');
if (has_use) {
context.state.init.push(b.stmt(b.call('$.effect', b.thunk(call))));
} else {
context.state.after_update.push(b.stmt(call));
}
}
}