Component
The Component API consists of a single function, which is responsible for defining your components.
Most other APIs are used "inside" the component definition.
defineComponent
defineComponent
is a factory function, taking in a configuration object, and returning a function that can be used to initialize your component when receiving the right HTML element.
// definition
declare function defineComponent(options: DefineComponentOptions): ComponentFactory;
2
In short, the usage looks like this:
// usage
const MyComponent = defineComponent({ ... });
const instance = MyComponent(element);
2
3
The options you can pass to the component are the name
, props
and refs
, and the setup
function that should return a list of Binding
definitions.
// options defintion
type DefineComponentOptions<
P extends Record<string, PropTypeDefinition>,
R extends Record<string, ComponentRefItem>
> = {
name: string;
props?: P;
refs?: R;
setup: (
props: TypedProps<P>,
refs: TypedRefs<R>,
context: { element: HTMLElement },
) => undefined | null | Array<Binding>;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
The return type of the defineComponent
is the object that your application will interact with, since it's the thing that will be exported from your source file.
If you call it with an HTML element that has a data-component
attribute that matches up with the component's configured name, it will create a component instance and returns an "instance API" to interact with.
// Factory return definition
type ComponentFactory =
// the "constructor" function, returning the "instance API"
( element: HTMLElement) => {
// component name on the instance
readonly name: string;
// API to update the component props from the outside
setProps: (props: P) => void;
// object to read the component props
readonly props: P;
// reference to the component's HTML element
readonly element: HTMLElement;
// a dispose function, if you want to manually remove this component
// otherwise it will automatically get disposed when removing the HTML element from the DOM
dispose: () => void;
}
// and the display name available on the function
& { displayName: string };
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
A simple usage would look like this:
// usage
defineComponent({
name: 'my-component',
props: {
activeIndex: { type: Number, default: 0 },
},
refs: {
container: { type: 'element', ref: 'container', optional: true},
},
setup({ props, refs, element }) {
return [
bind(refs.container, { text: props.activeIndex }),
];
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
props
// props definition
type PropTypeDefinition<T = any> = {
// the property type, used to convert the property to the right datatype
type: typeof Number | typeof String | typeof Boolean | typeof Date | typeof Array | typeof Object | typeof Function;
// an optional default value for when the property is not available
default?: T extends Primitive ? T : () => T;
// an optional predicate that will reject the prop if not valid
validator?: Predicate<T>;
// when present, mark this prop as optional, so it doesn't have to be available in the HTML
isOptional?: boolean;
// when present, mark this prop as potentially having a missing value, typing it as `| undefined`
missingValue?: boolean;
// provide the shape of any prop when it's a function
shapeType?: Function;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Luckily there are helper functions available to easily define the properties without having to provide these objects yourself.
You can find more info at the Props API
refs
// refs definition
type ComponentRefItem =
// shortcut for element ref
| string
| {
// different refs have their own type, to execute slightly different logic on them
type: 'element' | 'collection' | 'component' | 'componentCollection';
// the value of the `data-ref` attribute on the html element(s)
ref: string;
// only used for element/component, and when true, it will log an error if the element
// doesn't exist in the DOM. Nothing will break, it's just that the bindings will not
// be executed
isRequired?: boolean;
// A function that will find the right HTMLElement(s) that should be used for the refs.
// When `type` is element or component, it returns a single HTMLElement or null.
queryRef: (parent: HTMLElement) => HTMLElement | null | Array<HTMLElement>;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Luckily there are helper functions available to easily define the refs without having to provide these complex objects yourself.
You can find more info at the Refs API
setup
Readonly
The props
object passed to the setup
function is readonly, so it cannot be used to communicate back to the parent component or as initial state.
lifecycle
bindings
lazy
Allows async loading of components when they are actually used, to be used in the components
option of defineComponent
.
declare function lazy(
displayName: string,
getComponent: () => Promise<{ [key: string]: ComponentFactory }>,
componentName?: string,
): () => Promise<ComponentFactory>;
2
3
4
5
See below for how to use it, and what makes it tick.
// MyLazyComponent.ts
// In the component you want to lazy load, export a component definition
import { defineComponent } from '@muban/muban';
export const MyLazyComponent = defineComponent({
name: 'my-lazy-component',
setup() {
return [];
}
});
2
3
4
5
6
7
8
9
10
// in your "parent" component
import { defineComponent, lazy } from '@muban/muban';
defineComponent({
name: 'main',
components: [
// sync
SomeSyncComponent,
// The first parameter is the display name defined in the defineComponent function
// The second parameter is the async import of the lazy component file
lazy('my-lazy-component', () => import('./MyLazyComponent'))
],
setup() {
return [];
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Usually the exported component name is equal to the pascal cased version of the name
passed to the defineComponent function, in the above example the exported const MyLazyComponent
is the pascal cased version of my-lazy-component
If your exported component name does not match the name
given to the defineComponent function you should use the optional third parameter of the lazy function:
// MyLazyComponent.ts
// In the component you want to lazy load, export a component definition
import { defineComponent } from '@muban/muban';
// Export here is `LazyComponent` instead of `MyLazyComponent` to not match convention,
// which can be explicitly provided in the example below
export const LazyComponent = defineComponent({
name: 'my-lazy-component',
setup() {
return [];
}
});
2
3
4
5
6
7
8
9
10
11
12
13
// in your "parent" component
import { defineComponent, lazy } from '@muban/muban';
defineComponent({
name: 'main',
components: [
// sync
SomeSyncComponent,
/*
lazy will try to asynchronously import the named child component by converting the
given displayName to pascal case, in this example it will look for MyLazyComponent
inside the imported file './MyLazyComponent'
If the pascal cased displayName is different than the named export you can pass
a third parameter with the name of the named export, in this case 'LazyComponent'
*/
lazy('my-lazy-component', () => import('./MyLazyComponent'), 'LazyComponent')
],
setup() {
return [];
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
`'my-lazy-component'`
To know if the component should be loaded, the first 'my-lazy-component'
is needed to detect any data-component
usages in the HTML. Only then the component is actually loaded.