JSX
Syntactic sugar for creating objects declaratively. In GJS building UIs connecting signals, binding properties between objects is done mostly imperatively.
WARNING
This is not React.js This works nothing like React.js and has nothing in common with React.js other than the XML syntax.
Consider the following example:
function Box() {
const button = new Gtk.Button()
const icon = new Gtk.Image({ iconName: "system-search-symbolic" })
const label = new Gtk.Label({ label: "hello world" })
const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL })
button.set_child(icon)
box.append(button)
box.append(label)
button.connect("clicked", () => console.log("clicked"))
return box
}
Can be written as
function Box() {
return (
<Gtk.Box orientation={Gtk.Orientation.VERTICAL}>
<Gtk.Button $clicked={() => console.log("clicked")}>
<Gtk.Image iconName="system-search-symbolic" />
</Gtk.Button>
<Gtk.Label label="hello world" />
</Gtk.Box>
)
}
JSX expressions and jsx
function
A JSX expression transpiles to a jsx
function call. A JSX expression's type however is always the base GObject.Object
type while the jsx
return type is the instance type of the class or the return type of the function you pass to it. If you need the actual type of an object either use the jsx
function directly or type assert the JSX expression.
import { jsx } from "gjsx/gtk4"
const menubutton = new Gtk.MenuButton()
menubutton.popover = <Gtk.Popover /> // can not assign Object to Popover
menubutton.popover = jsx(Gtk.Popover, {}) // works as expected
function MyPopover(): Gtk.Popover
menubutton.popover = <MyPopover /> // can not assign Object to Popover
menubutton.popover = jsx(MyPopover, {}) // works as expected
Class components
When defining custom components choosing between using classes vs functions is mostly down to preference. There are cases when one or the other is more convenient to use, but you are mostly be using class components that come from libraries such as Gtk and you will be defining function components for custom components.
Using classes in JSX expressions let's you define some additional properties.
Constructor function
By default classes are instantiated with the new
keyword and initial values are passed in. In cases where you need to use a static constructor function instead you can define it with _constructor
.
WARNING
Initial values this way can not be passed to the constructor and are set after. This means construct only properties like css-name
can not be set.
<Gtk.DropDown
_constructor={() => Gtk.DropDown.new_from_strings(themes)}
/>
Type string
Under the hood the jsx
function uses the Gtk.Buildable interface which lets you define a type string to specify the type of child
it is meant to be.
NOTE
When using Gjsx with Gnome extensions, this has no effect.
<Gtk.CenterBox>
<Gtk.Box _type="start" />
<Gtk.Box _type="center" />
<Gtk.Box _type="end" />
</Gtk.CenterBox>
Signal handlers
Signal handlers can be defined with a $
prefix and notify::
signal handlers can be defined with a $$
prefix.
NOTE
Passed arguments by signals are not typed because of TypeScript limitations. Both properties and signals can be in either camelCase
, kebab-case
or snake_case
.
<Gtk.Revealer
$$childRevealed={(self) => print(self, "child-revealed")}
$destroy={(self) => print(self, "destroyed")}
/>
Setup function
It is possible to define an arbitrary function to do something with the instance imperatively. It is run after properties are set, signals are connected and children are appended and before the jsx
function returns.
<Gtk.Stack
$={self => print(self, "is about to be returned")}
/>
Bindings
Properties can be set as a static value or can be passed a Binding.
const opened = new State(false)
return (
<Gtk.Button $clicked={() => opened.set(!opened.get())}>
<Gtk.Revealer revealChild={bind(opened)}>
<Gtk.Label label="content" />
</Gtk.Revealer>
</Gtk.Button>
)
How children are passed to class components
Class components can only take GObject.Object
instances as children. They are set through the Gtk.Buildable.add_child
.
NOTE
In Gnome extensions they are set with Clutter.Actor.add_child
@register({ Implements: [Gtk.Buildable] })
class MyContainer extends Gtk.Widget {
vfunc_add_child(builder: Gtk.Builder, child: GObject.Object, type: string | null) {
if (child instanceof Gtk.Widget) {
// set children here
} else {
super.vfunc_add_child(builder, child, type)
// or you can also throw an error
}
}
}
Function components
Function components don't really benefit from JSX, they are just called as is.
Setup function
Just like class components, function components can also have a setup function.
import { FCProps } from "gjsx/gtk4"
function MyComponent(props: FCProps<Gtk.Button, {}>) {
return (
<Gtk.Button>
<Gtk.Label />
</Gtk.Button>
)
}
<MyComponent $={self => print(self, "is a Button")} />
NOTE
FCProps
is required for TypeScript to be aware of the $
function.
How children are passed to function components
They are passed in as children
property. They can be of any type and is statically checked by TypeScript.
interface MyButtonProps {
children: string
}
function MyButton({ children }: MyButtonProps) {
return (
<Gtk.Button label={children} />
)
}
<MyButton>Click Me</MyButton>
When multiple children are passed in children
is an Array
.
interface MyBoxProps {
children: Array<GObject.Object | string>
}
function MyBox({ children }: MyBoxProps) {
return (
<Gtk.Box>
{children.map(item => item instanceof Gtk.Widget ? (
item
) : (
<Gtk.Label label={item.toString()} />
))}
</Gtk.Box>
)
}
<MyBox>
Some Content
<Gtk.Button />
</MyBox>
Everything has to be handled explicitly in function components
There is no builtin way to define signal handlers or bindings automatically with function components, they have to be explicitly declared and handled.
interface MyWidgetProps {
label: Binding<string> | string
onClicked: (self: Gtk.Button) => void
}
function MyWidget({ label, onClicked }: MyWidgetProps) {
return (
<Gtk.Button
$clicked={onClicked}
label={label}
/>
)
}
Control flow
Dynamic rendering
When you want to render based on a value, you can use the <With>
component.
import { For } from "gjsx/gtk4"
import { State } from "gjsx/state"
const value = new State<{ member: string } | null>({
member: "hello"
})
return (
<With value={value()}>
{value => value && (
<Gtk.Label label={value.member} />
)}
</With>
)
TIP
In a lot of cases it is better to always render the component and set its visible
property instead because <With>
will destroy/recreate the widget each time the passed value
changes.
WARNING
When the value changes and the widget is re-rendered the previous one is removed from the parent component and the new one is appended. Order of widgets are not kept so make sure to wrap <With>
in a container to avoid this.
List rendering
The <For>
component let's you render based on an array dynamically. Each time the array is changed it is compared with its previous state and every widget associated with an item is removed and for every new item a new widget is inserted.
import { For } from "gjsx/gtk4"
let list: Binding<Array<object>>
return <For each={list()}>
{(item, index: Binding<number>) => (
<Gtk.Label label={index(i => `${i}. ${item}`)} />
)}
</For>
WARNING
Similarly to <With>
, when the list changes and a new item is added it is simply appended to the parent. Order of widgets are not kept so make sure to wrap <For>
in a container to avoid this.
Fragments
Both <When>
and <For>
are Fragment
s. A Fragment
is a collection of children. Whenever the children array changes it is reflected on the parent widget the Fragment
was assigned to. When implementing custom widgets you need to take into consideration the API being used for child insertion and removing.
- Both Gtk3 and Gtk4 uses the
Gtk.Buildable
interface to append children. - Gtk3 uses the
Gtk.Container
interface to remove children. - Gtk4 checks for a method called
remove
. - Clutter uses
Clutter.Actor.add_child
andClutter.Actor.remove_child
.
Intrinsic Elements
Intrinsic elements are globally available components which in web frameworks are usually HTMLElements such as <div>
<span>
<p>
. There are no intrinsic elements by default, but they can be set.
TIP
It should always be preferred to just export/import components.
Function components
tsximport { FCProps } from "gjsx/gtk4" import { intrinsicElements } from "gjsx/gtk4/jsx-runtime" type MyLabelProps = FCProps<Gtk.Label, { someProp: string }> function MyLabel({ someProp }: MyLabelProps) { return <Gtk.Label label={someProp} /> } intrinsicElements["my-label"] = MyLabel declare global { namespace JSX { interface IntrinsicElements { "my-label": MyLabelProps } } } <my-label someProps="hello" />
Class components
tsximport { CCProps } from "gjsx/gtk4" import { intrinsicElements } from "gjsx/gtk4/jsx-runtime" import { property, register } from "gjsx/gobject" interface MyWidgetProps extends Gtk.Widget.ConstructorProps { someProp: string } @register() class MyWidget extends Gtk.Widget { @property(String) declare someProp: string constructor(props: Partial<MyWidgetProps>) { super(props) } } intrinsicElements["my-widget"] = MyWidget declare global { namespace JSX { interface IntrinsicElements { "my-widget": CCProps<MyWidget, MyWidgetProps> } } } <my-widget someProps="hello" />