summaryrefslogtreecommitdiff
path: root/docs/guide/typescript/first-widgets.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/guide/typescript/first-widgets.md')
-rw-r--r--docs/guide/typescript/first-widgets.md412
1 files changed, 412 insertions, 0 deletions
diff --git a/docs/guide/typescript/first-widgets.md b/docs/guide/typescript/first-widgets.md
new file mode 100644
index 0000000..a7372a8
--- /dev/null
+++ b/docs/guide/typescript/first-widgets.md
@@ -0,0 +1,412 @@
+# First Widgets
+
+## Getting Started
+
+Start by initializing a project
+
+```sh
+ags --init
+```
+
+then run `ags` in the terminal
+
+```sh
+ags
+```
+
+:::details Usage without AGS
+🚧 Not yet documented. 🚧
+:::
+
+That's it! You have now a custom written bar using Gtk.
+
+:::tip
+AGS will transpile every `.ts`, `.jsx` and `.tsx` files into regular JavaScript, then
+it will bundle everything into a single JavaScript file which then GJS can execute.
+:::
+
+The AGS init command will generate the following files
+
+```txt
+.
+β”œβ”€β”€ @girs/ # generated types
+β”œβ”€β”€ widget/
+β”‚ └── Bar.tsx
+β”œβ”€β”€ app.ts # entry proint
+β”œβ”€β”€ env.d.ts # additional types
+β”œβ”€β”€ style.css
+└── tsconfig.json # needed by LSPs
+```
+
+## Root of every shell component: Window
+
+Astal apps are composed of widgets. A widget is a piece of UI that has its own logic and style.
+A widget can be as small as a button or an entire bar.
+The top level widget is always a [Window](https://aylur.github.io/libastal/astal3/class.Window.html) which will hold all widgets.
+
+::: code-group
+
+```tsx [widget/Bar.tsx]
+function Bar(monitor = 0) {
+ return <window className="Bar" monitor={monitor}>
+ <box>Content of the widget</box>
+ </window>
+}
+```
+
+:::
+
+::: code-group
+
+```ts [app.ts]
+import Bar from "./widget/Bar"
+
+App.start({
+ main() {
+ Bar(0)
+ Bar(1) // instantiate for each monitor
+ },
+})
+```
+
+:::
+
+## Creating and nesting widgets
+
+Widgets are JavaScript functions which return Gtk widgets,
+either by using JSX or using a widget constructor.
+
+:::code-group
+
+```tsx [MyButton.tsx]
+function MyButton(): JSX.Element {
+ return <button onClicked="echo hello">
+ <label label="Click me!" />
+ </button>
+}
+```
+
+```ts [MyButton.ts]
+import { Widget } from "astal/gtk3"
+
+function MyButton(): Widget.Button {
+ return new Widget.Button(
+ { onClicked: "echo hello" },
+ new Widget.Label({ label: "Click me!" }),
+ )
+}
+```
+
+:::
+
+:::info
+The only difference between the two is the return type.
+Using markup the return type is always `Gtk.Widget` (globally available as `JSX.Element`),
+while using constructors the return type is the actual type of the widget.
+It is rare to need the actual return type, so most if not all of the time, you can use markup.
+:::
+
+Now that you have declared `MyButton`, you can nest it into another component.
+
+```tsx
+function MyBar() {
+ return <window>
+ <box>
+ Click The button
+ <MyButton />
+ </box>
+ </window>
+}
+```
+
+Notice that widgets you defined start with a capital letter `<MyButton />`.
+Lowercase tags are builtin widgets, while capital letter is for custom widgets.
+
+## Displaying Data
+
+JSX lets you put markup into JavaScript.
+Curly braces let you β€œescape back” into JavaScript so that you can embed some variable
+from your code and display it.
+
+```tsx
+function MyWidget() {
+ const label = "hello"
+
+ return <button>{label}</button>
+}
+```
+
+You can also pass JavaScript to markup attributes
+
+```tsx
+function MyWidget() {
+ const label = "hello"
+
+ return <button label={label} />
+}
+```
+
+## Conditional Rendering
+
+You can use the same techniques as you use when writing regular JavaScript code.
+For example, you can use an if statement to conditionally include JSX:
+
+```tsx
+function MyWidget() {
+ let content
+
+ if (condition) {
+ content = <True />
+ } else {
+ content = <False />
+ }
+
+ return <box>{content}</box>
+}
+```
+
+You can also inline a [conditional `?`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_operator) (ternary) expression.
+
+```tsx
+function MyWidget() {
+ return <box>{condition ? <True /> : <False />}</box>
+}
+```
+
+When you don’t need the `else` branch, you can also use a shorter [logical && syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND#short-circuit_evaluation):
+
+```tsx
+function MyWidget() {
+ return <box>{condition && <True />}</box>
+}
+```
+
+:::info
+As you can guess from the above snippet, [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) values are not rendered.
+:::
+
+## Rendering lists
+
+You can use [`for` loops](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for) or [array `map()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
+
+```tsx
+function MyWidget() {
+ const labels = [
+ "label1"
+ "label2"
+ "label3"
+ ]
+
+ return <box>
+ {labels.map(label => (
+ <label label={label} />
+ ))}
+ </box>
+}
+```
+
+## Widget signal handlers
+
+You can respond to events by declaring event handler functions inside your widget:
+
+```tsx
+function MyButton() {
+ function onClicked(self: Widget.Button, ...args) {
+ console.log(self, "was clicked")
+ }
+
+ return <button onClicked={onClicked} />
+}
+```
+
+The handler can also be a string, which will get executed in a subprocess asynchronously.
+
+```tsx
+function MyButton() {
+ return <button onClicked="echo hello" />
+}
+```
+
+:::info
+Attributes prefixed with `on` will connect to a `signal` of the widget.
+Their types are not generated, but written by hand, which means not all of them are typed.
+Refer to the Gtk and Astal docs to have a full list of them.
+:::
+
+## How properties are passed
+
+Using JSX, a custom widget will always have a single object as its parameter.
+
+```ts
+type Props = {
+ myprop: string
+ child?: JSX.Element // when only one child is passed
+ children?: Array<JSX.Element> // when multiple children are passed
+}
+
+function MyWidget({ myprop, child, children }: Props) {
+ //
+}
+```
+
+```tsx
+// child prop of MyWidget is the box
+return <MyWidget myprop="hello">
+ <box />
+</MyWidget>
+```
+
+```tsx
+// children prop of MyWidget is [box, box, box]
+return <MyWidget myprop="hello">
+ <box />
+ <box />
+ <box />
+</MyWidget>
+```
+
+## State management
+
+The state of widgets are handled with Bindings. A [Binding](./binding) lets you
+connect the state of an [object](./binding#subscribable-and-connectable-interface)
+to a widget so it re-renders when that state changes.
+
+Use the `bind` function to create a `Binding` object from a `Variable` or
+a regular `GObject` and one of its properties.
+
+Here is an example of a Counter widget that uses a `Variable` as its state:
+
+```tsx
+import { Variable, bind } from "astal"
+
+function Counter() {
+ const count = Variable(0)
+
+ function increment() {
+ count.set(count.get() + 1)
+ }
+
+ return <box>
+ <label label={bind(count).as(num => num.toString())} />
+ <button onClicked={increment}>
+ Click to increment
+ <button>
+ </box>
+}
+```
+
+:::info
+Bindings have an `.as()` method which lets you transform the assigned value.
+In the case of a Label, its label property expects a string, so it needs to be
+turned to a string first.
+:::
+
+:::tip
+`Variables` have a shorthand for `bind(variable).as(transform)`
+
+```tsx
+const v = Variable(0)
+const transform = (v) => v.toString()
+
+return <box>
+ {/* these two are equivalent */}
+ <label label={bind(v).as(transform)} />
+ <label label={v(transform)} />
+</box>
+```
+
+:::
+
+Here is an example of a battery percent label that binds the `percentage`
+property of the Battery object from the [Battery Library](/guide/libraries/battery):
+
+```tsx
+import Battery from "gi://AstalBattery"
+import { bind } from "astal"
+
+function BatteryPercentage() {
+ const bat = Battery.get_default()
+
+ return <label label={bind(bat, "percentage").as((p) => p * 100 + " %")} />
+}
+```
+
+## Dynamic children
+
+You can also use a `Binding` for `child` and `children` properties.
+
+```tsx
+const child = Variable(<box />)
+
+return <box>{child}</box>
+```
+
+```tsx
+const num = Variable(3)
+const range = (n) => [...Array(n).keys()]
+
+return <box>
+ {num(n => range(n).map(i => (
+ <button>
+ {i.toString()}
+ <button/>
+ )))}
+<box>
+```
+
+:::tip
+Binding children of widgets will implicitly call `.destroy()` on widgets
+that would be left without a parent. You can opt out of this behavior
+by setting `noImplicityDestroy` property on the container widget.
+:::
+
+:::info
+The above example destroys and recreates every widget in the list **every time**
+the value of the `Variable` changes. There might be cases where you would
+want to [handle child creation and deletion](/guide/typescript/faq#avoiding-unnecessary-re-rendering)
+yourself, because you don't want to lose the
+inner state of widgets that does not need to be recreated. In this case
+you can create a [custom reactive structure](./binding#example-custom-subscribable)
+:::
+
+When there is at least one `Binding` passed as a child, the `children`
+parameter will always be a flattened `Binding<Array<JSX.Element>>`.
+When there is a single `Binding` passed as a child, the `child` parameter will
+be a `Binding<JSX.Element>` or a flattened `Binding<Array<JSX.Element>>`.
+
+```tsx
+import { type Binding } from "astal"
+
+function MyContainer({ children }: {
+ children?: Binding<Array<JSX.Element>>
+}) {
+ // children is a Binding over an Array of widgets
+}
+
+return <MyContainer>
+ <box />
+ {num(n => range(n).map(i => (
+ <button>
+ {i.toString()}
+ <button/>
+ )))}
+ [
+ [
+ <button />
+ ]
+ <button />
+ ]
+</MyContainer>
+```
+
+:::info
+You can pass the followings as children:
+
+- widgets
+- deeply nested arrays of widgets
+- bindings of widgets,
+- bindings of deeply nested arrays of widgets
+
+[falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) values are not rendered and anything not from this list
+will be coerced into a string and rendered as a label
+:::