Properties
Props are custom attributes/properties exposed publicly on an HTML element. They allow developers to pass data to a component to render or otherwise use.
The Prop Decorator (@Prop()
)​
Props are declared on a component using Rindo's @Prop()
decorator, like so:
// First, we import Prop from '@rindo/core'
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list',
})
export class TodoList {
// Second, we decorate a class member with @Prop()
@Prop() name: string;
render() {
// Within the component's class, its props are
// accessed via `this`. This allows us to render
// the value passed to `todo-list`
return <div>To-Do List Name: {this.name}</div>
}
}
In the example above, @Prop()
is placed before (decorates) the name
class member, which is a string. By adding
@Prop()
to name
, Rindo will expose name
as an attribute on the element, which can be set wherever the component
is used:
{/* Here we use the component in a TSX file */}
<todo-list name={"Tuesday's To-Do List"}></todo-list>
<!-- Here we use the component in an HTML file -->
<todo-list name="Tuesday's To-Do List"></todo-list>
In the example above the todo-list
component is used almost identically in TSX and HTML. The only difference between
the two is that in TSX, the value assigned to a prop (in this case, name
) is wrapped in curly braces. In some cases
however, the way props are passed to a component differs slightly between HTML and TSX.
Variable Casing​
In the JavaScript ecosystem, it's common to use 'camelCase' when naming variables. The example component below has a
class member, thingToDo
that is camelCased.
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
// thingToDo is 'camelCased'
@Prop() thingToDo: string;
render() {
return <div>{this.thingToDo}</div>;
}
}
Since thingToDo
is a prop, we can provide a value for it when we use our todo-list-item
component. Providing a
value to a camelCased prop like thingToDo
is nearly identical in TSX and HTML.
When we use our component in a TSX file, an attribute uses camelCase:
<todo-list-item thingToDo={"Learn about Rindo Props"}></todo-list-item>
In HTML, the attribute must use 'dash-case' like so:
<todo-list-item thing-to-do="Learn about Rindo Props"></todo-list-item>
Data Flow​
Props should be used to pass data down from a parent component to its child component(s).
The example below shows how a todo-list
component uses three todo-list-item
child components to render a ToDo list.
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list',
})
export class TodoList {
render() {
return (
<div>
<h1>To-Do List Name: Rindo To Do List</h1>
<ul>
{/* Below are three Rindo components that are children of `todo-list`, each representing an item on our list */}
<todo-list-item thingToDo={"Learn about Rindo Props"}></todo-list-item>
<todo-list-item thingToDo={"Write some Rindo Code with Props"}></todo-list-item>
<todo-list-item thingToDo={"Dance Party"}></todo-list-item>
</ul>
</div>
)
}
}
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
@Prop() thingToDo: string;
render() {
return <li>{this.thingToDo}</li>;
}
}
Children components should not know about or reference their parent components. This allows Rindo to efficiently re-render your components. Passing a reference to a component as a prop may cause unintended side effects.
Mutability​
A Prop is by default immutable from inside the component logic. Once a value is set by a user, the component cannot update it internally. For more advanced control over the mutability of a prop, please see the mutable option section of this document.
Types​
Props can be a boolean
, number
, string
, or even an Object
or Array
. The example below expands the
todo-list-item
to add a few more props with different types.
import { Component, Prop, h } from '@rindo/core';
// `MyHttpService` is an `Object` in this example
import { MyHttpService } from '../some/local/directory/MyHttpService';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
@Prop() isComplete: boolean;
@Prop() timesCompletedInPast: number;
@Prop() thingToDo: string;
@Prop() myHttpService: MyHttpService;
}
Boolean Props​
A property on a Rindo component that has a type of boolean
may be declared as:
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
@Prop() isComplete: boolean;
}
To use this version of todo-list-item
in HTML, we pass the string "true"
/"false"
to the component:
<!-- Set isComplete to 'true' -->
<todo-list-item is-complete="true"></todo-list-item>
<!-- Set isComplete to 'false' -->
<todo-list-item is-complete="false"></todo-list-item>
To use this version of todo-list-item
in TSX, true
/false
is used, surrounded by curly braces:
// Set isComplete to 'true'
<todo-list-item isComplete={true}></todo-list-item>
// Set isComplete to 'false'
<todo-list-item isComplete={false}></todo-list-item>
There are a few ways in which Rindo treats props that are of type boolean
that are worth noting:
- The value of a boolean prop will be
false
if provided the string"false"
in HTML
<!-- The 'todo-list-item' component will have an isComplete value of `false` -->
<todo-list-item is-complete="false"></todo-list-item>
- The value of a boolean prop will be
true
if provided a string that is not"false"
in HTML
<!-- The 'todo-list-item' component will have an isComplete value of -->
<!-- `true` for each of the following examples -->
<todo-list-item is-complete=""></todo-list-item>
<todo-list-item is-complete="0"></todo-list-item>
<todo-list-item is-complete="False"></todo-list-item>
- The value of a boolean prop will be
undefined
if it has no default value and one of the following applies:- the prop is not included when using the component
- the prop is included when using the component, but is not given a value
<!-- Both examples using the 'todo-list-item' component will have an -->
<!-- isComplete value of `undefined` -->
<todo-list-item></todo-list-item>
<todo-list-item is-complete></todo-list-item>
Number Props​
A property on a Rindo component that has a type of number
may be declared as:
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
@Prop() timesCompletedInPast: number;
}
To use this version of todo-list-item
in HTML, we pass the numeric value as a string to the component:
<!-- Set timesCompletedInPast to '0' -->
<todo-list-item times-completed-in-past="0"></todo-list-item>
<!-- Set timesCompletedInPast to '23' -->
<todo-list-item times-completed-in-past="23"></todo-list-item>
To use this version of todo-list-item
in TSX, a number surrounded by curly braces is passed to the component:
// Set timesCompletedInPast to '0'
<todo-list-item timesCompletedInPast={0}></todo-list-item>
// Set timesCompletedInPast to '23'
<todo-list-item timesCompletedInPast={23}></todo-list-item>
String Props​
A property on a Rindo component that has a type of string
may be declared as:
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
@Prop() thingToDo: string;
}
To use this version of todo-list-item
in HTML, we pass the value as a string to the component:
<!-- Set thingToDo to 'Learn about Rindo Props' -->
<todo-list-item thing-to-do="Learn about Rindo Props"></todo-list-item>
<!-- Set thingToDo to 'Write some Rindo Code with Props' -->
<todo-list-item thing-to-do="Write some Rindo Code with Props"></todo-list-item>
To use this version of todo-list-item
in TSX, we pass the value as a string to the component. Curly braces aren't
required when providing string values to props in TSX, but are permitted:
// Set thingToDo to 'Learn about Rindo Props'
<todo-list-item thingToDo="Learn about Rindo Props"></todo-list-item>
// Set thingToDo to 'Write some Rindo Code with Props'
<todo-list-item thingToDo="Write some Rindo Code with Props"></todo-list-item>
// Set thingToDo to 'Write some Rindo Code with Props' with curly braces
<todo-list-item thingToDo={"Learn about Rindo Props"}></todo-list-item>
Object Props​
A property on a Rindo component that has a type of Object
may be declared as:
// TodoListItem.tsx
import { Component, Prop, h } from '@rindo/core';
import { MyHttpService } from '../path/to/MyHttpService';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
// Use `@Prop()` to declare the `httpService` class member
@Prop() httpService: MyHttpService;
}
// MyHttpService.ts
export class MyHttpService {
// This implementation intentionally left blank
}
In TypeScript, MyHttpService
is both an Object
and a 'type'. When using user-defined types like MyHttpService
, the
type must always be exported using the export
keyword where it is declared. The reason for this is Rindo needs to
know what type the prop httpService
is when passing an instance of MyHttpService
to TodoListItem
from a parent
component.
To set httpService
in TSX, assign the property name in the custom element's tag to the desired value like so:
// TodoList.tsx
import { Component, h } from '@rindo/core';
import { MyHttpService } from '../MyHttpService';
@Component({
tag: 'todo-list',
styleUrl: 'todo-list.css',
shadow: true,
})
export class ToDoList {
private httpService = new MyHttpService();
render() {
return <todo-list-item httpService={this.httpService}></todo-list-item>;
}
}
Note that the prop name is using camelCase
, and the value is surrounded by curly braces.
It is not possible to set Object
props via an HTML attribute like so:
<!-- this will not work -->
<todo-list-item http-service="{ /* implementation omitted */ }"></todo-list-item>
The reason for this is that Rindo will not attempt to serialize object-like strings written in HTML into a JavaScript object. Similarly, Rindo does not have any support for deserializing objects from JSON. Doing either can be expensive at runtime, and runs the risk of losing references to other nested JavaScript objects.
Instead, properties may be set via <script>
tags in a project's HTML:
<script>
document.querySelector('todo-list-item').httpService = { /* implementation omitted */ };
</script>
Array Props​
A property on a Rindo component that is an Array may be declared as:
// TodoList.tsx
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
@Prop() itemLabels: string[];
}
To set itemLabels
in TSX, assign the prop name in the custom element's tag to the desired value like so:
// TodoList.tsx
import { Component, h } from '@rindo/core';
import { MyHttpService } from '../MyHttpService';
@Component({
tag: 'todo-list',
styleUrl: 'todo-list.css',
shadow: true,
})
export class ToDoList {
private labels = ['non-urgent', 'weekend-only'];
render() {
return <todo-list-item itemLabels={this.labels}></todo-list-item>;
}
}
Note that the prop name is using camelCase
, and the value is surrounded by curly braces.
It is not possible to set Array
props via an HTML attribute like so:
<!-- this will not work -->
<todo-list-item item-labels="['non-urgent', 'weekend-only']"></todo-list-item>
The reason for this is that Rindo will not attempt to serialize array-like strings written in HTML into a JavaScript object. Doing so can be expensive at runtime, and runs the risk of losing references to other nested JavaScript objects.
Instead, properties may be set via <script>
tags in a project's HTML:
<script>
document.querySelector('todo-list-item').itemLabels = ['non-urgent', 'weekend-only'];
</script>
Advanced Prop Types​
any
Type​
TypeScript's any
type is a special type
that may be used to prevent type checking of a specific value. Because any
is a valid type in TypeScript, Rindo
props can also be given a type of any
. The example below demonstrates three different ways of using props with type
any
:
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
// isComplete has an explicit type annotation
// of `any`, and no default value
@Prop() isComplete: any;
// label has an explicit type annotation of
// `any` with a default value of 'urgent',
// which is a string
@Prop() label: any = 'urgent';
// thingToDo has no type and no default value,
// and will be considered to be type `any` by
// TypeScript
@Prop() thingToDo;
render() {
return (
<ul>
<li>isComplete has a value of - {this.isComplete} - and a typeof value of "{typeof this.isComplete}"</li>
<li>label has a value of - {this.label} - and a typeof value of "{typeof this.label}"</li>
<li>thingToDo has a value of - {this.thingToDo} - and a typeof value of "{typeof this.thingToDo}"</li>
</ul>
);
}
}
When using a Rindo prop typed as any
(implicitly or explicitly), the value that is provided to a prop retains its
own type information. Neither Rindo nor TypeScript will try to change the type of the prop. To demonstrate, let's use
todo-list-item
twice, each with different prop values:
{/* Using todo-list-item in TSX using differnt values each time */}
<todo-list-item isComplete={42} label={null} thingToDo={"Learn about any-typed props"}></todo-list-item>
<todo-list-item isComplete={"42"} label={1} thingToDo={"Learn about any-typed props"}></todo-list-item>
The following will rendered from the usage example above:
- isComplete has a value of - 42 - and a typeof value of "number"
- label has a value of - - and a typeof value of "object"
- thingToDo has a value of - Learn about any-typed props - and a typeof value of "string"
- isComplete has a value of - 42 - and a typeof value of "string"
- label has a value of - 1 - and a typeof value of "number"
- thingToDo has a value of - Learn about any-typed props - and a typeof value of "string"
In the first usage of todo-list-item
, isComplete
is provided a number value of 42, whereas in the second usage it
receives a string containing "42". The types on isComplete
reflect the type of the value it was provided, 'number' and
'string', respectively.
Looking at label
, it is worth noting that although the prop has a default value, it does
not narrow the type of label
to be of type 'string'. In the first usage of todo-list-item
, label
is provided a
value of null, whereas in the second usage it receives a number value of 1. The types of the values stored in label
are correctly reported as 'object' and 'number', respectively.
Optional Types​
TypeScript allows members to be marked optional by appending a ?
at the end of the member's name. The example below
demonstrates making each a component's props optional:
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
// completeMsg is optional, has an explicit type
// annotation of `string`, and no default value
@Prop() completeMsg?: string;
// label is optional, has no explicit type
// annotation, but does have a default value
// of 'urgent'
@Prop() label? = 'urgent';
// thingToDo has no type annotation and no
// default value
@Prop() thingToDo?;
render() {
return (
<ul>
<li>completeMsg has a value of - {this.completeMsg} - and a typeof value of "{typeof this.completeMsg}"</li>
<li>label has a value of - {this.label} - and a typeof value of "{typeof this.label}"</li>
<li>thingToDo has a value of - {this.thingToDo} - and a typeof value of "{typeof this.thingToDo}"</li>
</ul>
);
}
}
When using a Rindo prop that is marked as optional, Rindo will try to infer the type of the prop if a type is not explicitly given. In the example above, Rindo is able to understand that:
completeMsg
is of type string, because it has an explicit type annotationlabel
is of type string, because it has a default value that is of type stringthingToDo
is of typeany
, because it has no explicit type annotation, nor default value
Because Rindo can infer the type of label
, the following will fail to compile due to a type mismatch:
{/* This fails to compile with the error "Type 'number' is not assignable to type 'string'" for the label prop. */}
<todo-list-item completeMsg={"true"} label={42} thingToDo={"Learn about any-typed props"}></todo-list-item>
It is worth noting that when using a component in an HTML file, such type checking is unavailable. This is a constraint on HTML, where all values provided to attributes are of type string:
<!-- using todo-list-item in HTML -->
<todo-list-item complete-msg="42" label="null" thing-to-do="Learn about any-typed props"></todo-list-item>
renders:
- completeMsg has a value of - 42 - and a typeof value of "string"
- label has a value of - null - and a typeof value of "string"
- thingToDo has a value of - Learn about any-typed props - and a typeof value of "string"
Union Types​
Rindo allows props types be union types,
which allows you as the developer to combine two or more pre-existing types to create a new one. The example below shows
a todo-list-item
who accepts a isComplete
prop that can be either a string or boolean.
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
@Prop() isComplete: string | boolean;
}
This component can be used in both HTML:
<todo-list-item is-complete="true"></todo-list-item>
<todo-list-item is-complete="false"></todo-list-item>
<todo-list-item is-complete></todo-list-item>
<todo-list-item></todo-list-item>
and TSX:
<todo-list-item isComplete={true}></todo-list-item>
<todo-list-item isComplete={false}></todo-list-item>
When using union types, the type of a component's @Prop()
value can be ambiguous at runtime.
In the provided example, under what circumstances does @Prop() isComplete
function as a string
, and when does it serve as a boolean
?
When using a component in HTML, the runtime value of a @Prop()
is a string whenever an attribute is set.
This is a result of setting the HTML attribute for the custom element.
<!-- Since this is HTML, the value of `isComplete` in `ToDoListItem` will be a string -->
<!-- Set isComplete to "true". -->
<todo-list-item is-complete="true"></todo-list-item>
<!-- Set isComplete to "false" -->
<todo-list-item is-complete="false"></todo-list-item>
<!-- Set isComplete to "" -->
<todo-list-item is-complete></todo-list-item>
However, if an attribute is not specified, the runtime value of the property will be undefined
:
<!-- Since `is-complete` is omitted, the value of `isComplete` in `ToDoListItem` will be `undefined` -->
<todo-list-item></todo-list-item>
When the attribute on a component is set using setAttribute
, the runtime value of a @Prop()
is always coerced to a string.
<script>
// both of these `setAttribute` calls set the property `isComplete` to "true" (string)
document.querySelector('todo-list-item').setAttribute('is-complete', true);
document.querySelector('todo-list-item').setAttribute('is-complete', "true");
// both of these `setAttribute` calls set the property `isComplete` to "false" (string)
document.querySelector('todo-list-item').setAttribute('is-complete', false);
document.querySelector('todo-list-item').setAttribute('is-complete', "false");
</script>
However, if the property of a custom element is directly changed, its type will match the value that was provided.
<script>
// Set the property `isComplete` to `true` (boolean)
document.querySelector('todo-list-item').isComplete = true;
// Set the property `isComplete` to "true" (string)
document.querySelector('todo-list-item').isComplete = "true";
// Set the property `isComplete` to `false` (boolean)
document.querySelector('todo-list-item').isComplete = false;
// Set the property `isComplete` to "false" (string)
document.querySelector('todo-list-item').isComplete = "false";
</script>
When using a component in TSX, a @Prop()
's type will match the value that was provided.
// Since this is TSX, the value of `isComplete` in `ToDoListItem`
// depends on the type of the value passed to the component.
//
// Set the property `isComplete` to `true` (boolean)
<todo-list-item isComplete={true}></todo-list-item>
// Set the property `isComplete` to "true" (string)
<todo-list-item isComplete={"true"}></todo-list-item>
// Set the property `isComplete` to `false` (boolean)
<todo-list-item isComplete={false}></todo-list-item>
// Set the property `isComplete` to "false" (string)
<todo-list-item isComplete={"false"}></todo-list-item>
Default Values​
Rindo props can be given a default value as a fallback in the event a prop is not provided:
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'component-with-some-props',
})
export class ComponentWithSomeProps {
@Prop() aNumber = 42;
@Prop() aString = 'defaultValue';
render() {
return <div>The number is {this.aNumber} and the string is {this.aString}</div>
}
}
Regardless of if we use this component in HTML or TSX, "The number is 42 and the string is defaultValue" is displayed when no values are passed to our component:
<component-with-some-props></component-with-some-props>
The default values on a component can be overridden by specifying a value for a prop with a default value. For the
example below, "The number is 7 and the string is defaultValue" is rendered. Note how the value provided to aNumber
overrides the default value, but the default value of aString
remains the same:
<component-with-some-props a-number="7"></component-with-some-props>
Inferring Types from Default Values​
When a default value is provided, Rindo is able to infer the type of the prop from the default value:
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'component-with-many-props',
})
export class ComponentWithManyProps {
// both props below are of type 'boolean'
@Prop() boolean1: boolean;
@Prop() boolean2 = true;
// both props below are of type 'number'
@Prop() number1: number;
@Prop() number2 = 42;
// both props below are of type 'string'
@Prop() string1: string;
@Prop() string2 = 'defaultValue';
}
Required Properties​
By placing a !
after a prop name, Rindo mark that the attribute/property as required. This ensures that when the
component is used in TSX, the property is used:
import { Component, Prop, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class ToDoListItem {
// Note the '!' after the variable name.
@Prop() thingToDo!: string;
}
Prop Validation​
To do validation of a Prop, you can use the @Watch() decorator:
import { Component, Prop, Watch, h } from '@rindo/core';
@Component({
tag: 'todo-list-item',
})
export class TodoList {
// Mark the prop as required, to make sure it is provided when we use `todo-list-item`.
// We want stricter guarantees around the contents of the string, so we'll use `@Watch` to perform additional validation.
@Prop() thingToDo!: string;
@Watch('thingToDo')
validateName(newValue: string, _oldValue: string) {
// don't allow `thingToDo` to be the empty string
const isBlank = typeof newValue !== 'string' || newValue === '';
if (isBlank) {
throw new Error('thingToDo is a required property and cannot be empty')
};
// don't allow `thingToDo` to be a string with a length of 1
const has2chars = typeof newValue === 'string' && newValue.length >= 2;
if (!has2chars) {
throw new Error('thingToDo must have a length of more than 1 character')
};
}
}