- Overview
- Definition
- Props comparison
- Binding function props
- Dynamic Props
- Default Props
- Props validation
- Good Practices
In Owl, props
(short for properties) is an object which contains every piece
of data given to a component by its parent.
class Child extends Component {
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
}
class Parent extends Component {
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
In this example, the Child
component receives two props from its parent: a
and b
. They are collected into a props
object by Owl, with each value being
evaluated in the context of the parent. So, props.a
is equal to 'fromparent'
and
props.b
is equal to 'string'
.
Note that props
is an object that only makes sense from the perspective of the
child component.
The props
object is made of every attributes defined on the template, with the
following exceptions:
- every attribute starting with
t-
are not props (they are QWeb directives),
In the following example:
<div>
<ComponentA a="state.a" b="'string'"/>
<ComponentB t-if="state.flag" model="model"/>
</div>
the props
object contains the following keys:
- for
ComponentA
:a
andb
, - for
ComponentB
:model
,
Whenever Owl encounters a subcomponent in a template, it performs a shallow comparison of all props. If they are all referentially equal, then the subcomponent will not even be updated. Otherwise, if at least one props has changed, then Owl will update it.
However, in some cases, we know that two values are different, but they have the same effect, and should not be considered different by Owl. For example, anonymous functions in a template are always different, but most of them should not be considered different:
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<Todo todo="todo" onDelete="() => deleteTodo(todo.id)" />
</t>
In that case, one can use the .alike
suffix:
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<Todo todo="todo" onDelete.alike="() => deleteTodo(todo.id)" />
</t>
This tells Owl that this specific prop should always be considered equivalent (or, in other words, should be removed from the list of comparable props).
Note that even if most anonymous functions should probably be considered alike
,
it is not necessarily true in all cases. It depends on what values are captured
by the anonymous function. The following example shows a case where it is probably
wrong to use .alike
.
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<!-- Probably wrong! todo.isCompleted may change -->
<Todo todo="todo" toggle.alike="() => toggleTodo(todo.isCompleted)" />
</t>
It is common to have the need to pass a callback as a prop. Since Owl components are class based, the callback frequently needs to be bound to its owner component. So, one can do this:
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback="doSomething"/>
</div>`;
setup() {
this.doSomething = this.doSomething.bind(this);
}
doSomething() {
// ...
}
}
However, this is such a common use case that Owl provides a special suffix to do
just that: .bind
. This looks like this:
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback.bind="doSomething"/>
</div>`;
doSomething() {
// ...
}
}
The .bind
suffix also implies .alike
, so these props will not cause additional
renderings.
When you need to pass a user-facing string to a subcomponent, you likely want it
to be translated. Unfortunately, because props are arbitrary expressions, it wouldn't
be practical for Owl to find out which parts of the expression are strings and translate
them, and it also makes it difficult for tooling to extract these strings to generate
terms to translate. While you can work around this issue by doing the translation in
JavaScript, or by using t-set
with a body (the body of t-set
is translated),
and passing the variable as a prop, this is a sufficiently common use case that Owl
provides a suffix for this purpose: .translate
.
<t t-name="ParentComponent">
<Child someProp.translate="some message"/>
</t>
Note that the content of this attribute is NOT treated as a JavaScript expression: it is treated as a string, as if it was an attribute on an HTML element, and translated before being passed to the component. If you need to interpolate some data into the string, you will still have to do this in JavaScript.
The t-props
directive can be used to specify totally dynamic props:
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
If the static defaultProps
property is defined, it will be used to complete
props received by the parent, if missing.
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0,
};
...
}
In the example above, the initialValue
props is now by default set to 0.
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
- hard to tell how a component should be used, by looking at its code.
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
A props type system solves both issues, by describing the types and shapes of the props. Here is how it works in Owl:
props
key is a static key (so, different fromthis.props
in a component instance)- it is optional: it is ok for a component to not define a
props
key. - props are validated whenever a component is created/updated
- props are only validated in
dev
mode (see how to configure an app) - if a key does not match the description, an error is thrown
- it validates keys defined in (static)
props
. Additional keys given by the parent will cause an error (unless the special prop*
is present). - it is an object or a list of strings
- a list of strings is a simplified props definition, which only lists the name
of the props. Also, if the name ends with
?
, it is considered optional. - all props are by default required, unless they are defined with
optional: true
(in that case, it is only done if there is a value) - valid types are:
Number, String, Boolean, Object, Array, Date, Function
, and all constructor functions (so, if you have aPerson
class, it can be used as a type) - arrays are homogeneous (all elements have the same type/shape)
For each key, a prop
definition is either a boolean, a constructor, a list of constructors, or an object:
- a boolean: indicate that the props exists, and is mandatory.
- a constructor: this should describe the type, for example:
id: Number
describe the propsid
as a number - an object describing a value as type. This is done by using the
value
key. For example,{value: false}
specifies that the corresponding value should be equal to false. - a list of constructors. In that case, this means that we allow more than one
type. For example,
id: [Number, String]
means thatid
can be either a string or a number. - an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
type
: the main type of the prop being validatedelement
: if the type wasArray
, then theelement
key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,shape
: if the type wasObject
, then theshape
key describes the interface of the object. If it is not set, then we only validate the object, not its elements,values
: if the type wasObject
, then thevalues
key describes the interface of values in the object, this allows validating objects that are used as mappings, where keys are not known in advance but the shape of the values is.validate
: this is a function which should return a boolean to determine if the value is valid or not. Useful for custom validation logic.optional
: if true, the prop is not mandatory
There is a special *
prop that means that additional prop are allowed. This is
sometimes useful for generic components that will propagate some or all their
props to their child components.
Note that default values cannot be defined for a mandatory props. Doing so will result in a prop validation error.
Examples:
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: String }}
},
date: Date,
combinedVal: [Number, Boolean],
optionalProp: { type: Number, optional: true }
};
...
}
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
// only the existence of those 3 keys is documented. any other key is allowed.
static props = ['message', 'id', 'date', '*'];
// size is optional
static props = ['message', 'size?'];
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
}
}, // object, with keys id (number), name (string, optional) and url (string)
someObj3: {
type: Object,
values: { type: Array, element: String },
}, // object with arbitary keys where values are arrays of strings
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
kindofsmallnumber: {
type: Number,
validate: n => (0 <= n && n <= 10)
},
size: {
validate: e => ["small", "medium", "large"].includes(e)
},
someId: [Number, {value: false}], // either a number or false
};
Note: the props validation code is done by using the validate utility function.
A props
object is a collection of values that come from the parent. As such,
they are owned by the parent, and should never be modified by the child:
class MyComponent extends Component {
constructor(parent, props) {
super(parent, props);
props.a.b = 43; // Never do that!!!
}
}
Props should be considered readonly, from the perspective of the child component. If there is a need to modify them, then the request to update them should be sent to the parent (for example, with an event).
Any value can go in a props. Strings, objects, classes, or even callbacks could be given to a child component (but then, in the case of callbacks, communicating with events seems more appropriate).