-
File names for Components, Classes and Stylesheets should be in PascalCase. Examples:
App.svelte CancellablePromise.ts App.scss
-
Typescript file names should be in lowerCamelCase. Examples:
index.ts utilityFunctions.ts
-
All variables and constants should be in lowerCamelCase. Examples:
export let randomVariable; let aNewVariable = 'new variable'; const someValue = 'constant value';
-
All function names should be in lowerCamelCase. Examples:
function someFunction() { /* ... */ } let someOtherFn = () => { /* ... */ }; const someConstFn = () => { /* ... */ };
-
All CSS class names should be in kebab-case. Examples:
<div class="cell-fabric"></div> <span class="editable-cell"></span>
-
All directory names should be in kebab-case (hyphen-delimited). Examples:
/components/text-input/ /components/combo-boxes/multi-select/
-
Acronyms within PascalCase and camelCase should be treated as words. Examples:
UrlInput.svelte
function getApiUrl() { /* ... */ } let currentDbName;
- discussion
- Not all code conforms to this standard yet, and bringing existing code into conformance is a low priority.
-
Use American English spelling instead of British English spelling. Examples:
LabeledInput.svelte ColorSelector.svelte
-
If a TypeScript file contains only type definitions (without any values or implementation), then use the file extension
.d.tsinstead of.ts. If you useenumorconstyou'll need make the file a.tsfile. If you only usetypeandinterface, then make the file a.d.tsfile. -
Prefer the term "delete" in code and UI over similar terms like "remove" and "drop".
When working inside a component that might be placed where phrasing content is required, be sure to only use phrasing content elements (like span) instead of not-phrasing content elements (like div).
-
✅ Good
FancyCheckbox.svelte<span class="make-it-fancy"> <input type="checkbox" /> </span>
-
❌ Bad because it uses
divinstead ofspanFancyCheckbox.svelte<div class="make-it-fancy"> <input type="checkbox" /> </div>
Rationale:
-
For example, let's build on the "Bad" example above and write the following Svelte code:
<label> <FancyCheckbox /> Foo </label>
That Svelte code will render:
<label> <div class="make-it-fancy"> <input type="checkbox" /> </div> Foo </label>
That markup has invalid DOM nesting because a
labelelement can only contain phrasing content -- but adivis not phrasing content.
Tips:
- You can make a
spanact like adivby settingdisplay: blockif needed.
Notes:
- Not all of our components adhere to this guideline yet.
-
Don't use
px— useremoreminstead.Exceptional cases where
pxis okay:- when setting the root
font-size
- when setting the root
Note: some of our older code still does not conform to this standard.
- Don't use
vhorvw— usesvhandsvwinstead. See #4752 to understand why.
To preserve modularity and encapsulation, components should not define their own layout-related behavior — instead the consuming component should be responsible for layout and spacing. Components should not set any space around their outer-most visual edges or define their own z-index.
-
margin: The component's root element should not set any margin.
-
padding: If the component's root element has border, it's fine to set padding because the border will serve as the outer-most visual edge. But if there's no border, then there should be no padding.
-
z-index:
-
The component's root element should not set any z-index.
-
It's fine for child elements within the component to set a z-index, but in such cases the component's root element must establish its own stacking context. It's best to use
isolation: isolate;to establish the stacking context because it clearly communicates intent and works without setting z-index. Once your component has its own stacking context, then you're free to set the z-index values within the component without thinking about anything outside the component. You can use simple values like1and2within the component because everything is encapsulated. -
If you want to render a child component with a specific z-index, then prefer to nest the child component inside a DOM element, setting the z-index on the DOM element (not the component).
-
If you absolutely must pass a z-index value into a component, then do so using CSS variables as follows:
-
Within the parent component (that establishes a stacking context), define one CSS variable for each "layer" within that stacking context.
-
Follow this naming convention to scope your CSS variables:
.record-selector-window { --z-index__record_selector__row-header: 1; --z-index__record_selector__thead: 2; --z-index__record_selector__thead-row-header: 3; --z-index__record_selector__shadow-inset: 4; --z-index__record_selector__overlay: 5; --z-index__record_selector__above-overlay: 6; }
Here, the name of each variable begins with
z-index, then has a name to represent the stacking context (in this caserecord-selector), then has a name to represent the layer within that stacking context (e.g.row-header). Double underscores delimit those three pieces of the variable.
-
-
Prefer await over .then when possible.
-
✅ Good
async function handleSave() { isLoading = true; try { await save(value); } catch (e: unknown) { error = getErrorMessage(e); } finally { isLoading = false; } }
-
❌ Bad
function handleSave() { isLoading = true; save(value) .then(() => { isLoading = false; return true; }) .catch((e: unknown) => { error = getErrorMessage(e); isLoading = false; }); }
Prefer function over const
-
✅ Good
function withFoo(s: string) { return `${s} foo`; }
-
❌ Bad
const withFoo = (s: string) => { return `${s} foo`; };
Rationale:
-
The
functionsyntax is more concise, with less likelihood of line wrapping. -
With TypeScript, adding explicit return type annotations becomes significantly more verbose when using the
constapproach. For example,export const withFoo: (s: string) => string = (s: string) => { return `${s} foo`; };
We don't require explicit return types everywhere, but we do use the explicit-module-boundary-types linting rule to require them for exported functions. This makes TS errors appear closer to the code that should be fixed and also provides some small performance gains with
tsc.
- Prefer
interfacewhen possible. - Use
typewhen necessary.
Prefer using undefined over null where possible.
-
✅ Good
const name = writable<string | undefined>(undefined);
-
❌ Bad because it uses
nullwhen it could useundefinedconst name = writable<string | null>(null);
-
❌ Bad because it uses an empty string to represent an empty value when it probably should be using
undefinedfor greater code clarity.const name = writable<string>('');
-
❌ Bad because it mixes
nullandundefinedinto the same type.const name = writable<string | undefined | null>(null);
-
✅ Acceptable use of
nullbecause it's necessary for data that will be serialized to JSON. (Usingundefinedhere would result in that key/value pair being removed from the JSON string).await patchApi(url, { name: null });
-
✅ Acceptable use of
nullbecause theCheckboxcomponent is designed to accept anullvalue to place the checkbox into an indeterminate state.<Checkbox value={null} />
Considerations:
-
In some cases you may need to coalesce a
nullvalue toundefined. For example:function firstCapitalLetter(s: string): string | undefined { return s.match(/[A-Z]/)?.[0] ?? undefined; }
Additional context:
TypeScript types (including interfaces) which describe API requests and responses (and properties therein) should be separated from other front end code and placed in the mathesar_ui/src/api directory. This structure serves to communicate that, unlike other TypeScript types, the front end does not have control over the API types.
-
✅ Good because only one
coststore is created.function getCost(toppings: Readable<string[]>) { return derived(this.toppings, (t) => 10 + t.length * 2); } class PizzaOrder { toppings: string[]; cost: Readable<number>; constructor() { this.toppings = []; this.cost = getCost(this.toppings); } }
-
❌ Bad because separate calls to
costwill create separate stores which may lead to more subscribe and unsubscribe events in some cases, risking performance problems.class PizzaOrder { toppings: string[]; constructor() { this.toppings = []; } get cost(): Readable<boolean> { return derived(this.toppings, (t) => 10 + t.length * 2); } }
Additional context:
-
Example:
-
Child.svelte<script lang="ts> export let word: string; </script> <span class="child">{word}</span>
Childexplicitly accepts awordprop. -
Parent.svelte<script lang="ts> import type { ComponentProps } from 'svelte'; import Child from './Child.svelte`; type $$Props = ComponentProps<MessageBox>; </script> <Child {...$$restProps} />
TypeScript knows that
Parentaccepts awordprop too because we have defined$$Propsas such. -
Grandparent.svelte<script lang="ts> import Parent from './Parent.svelte`; </script> <Parent word="foo" />
Passing
"foo"to thewordprop here is type-safe.
-
-
If you want to alter the props, you can define
$$Propslike this:interface $$Props extends Omit<ComponentProps<Child>, 'notThatOne'> { addThisOne: string; }
You can search our codebase for
$$Propsto see the various ways we're using it.