Files
fulpstation/tgui/docs/tutorial-and-examples.md
Aleksej Komarov baf9bd2f95 REMOVES TGUI (#49330)
* Removes tgui

* Make tgui script executable again

* Set version of packages to 2.0.0

* Update copypasta docs to match the current best practice

* Rebuild tgui

* Update .github/CONTRIBUTING.md

Co-Authored-By: BadSS13Player <58376109+BadSS13Player@users.noreply.github.com>

* Remove holy bible by Arcane

* rebuild

Co-authored-by: BadSS13Player <58376109+BadSS13Player@users.noreply.github.com>
Co-authored-by: Rob Bailey <actioninja@gmail.com>
2020-02-17 04:51:56 -08:00

243 lines
7.5 KiB
Markdown

# Tutorial and Examples
## Main concepts
Basic tgui backend code consists of the following vars and procs:
```
ui_interact(mob/user, ui_key, datum/tgui/ui, force_open,
datum/tgui/master_ui, datum/ui_state/state)
ui_data(mob/user)
ui_act(action, params)
```
- `src_object` - The atom, which UI corresponds to in the game world.
- `ui_interact` - The proc where you will handle a request to open an
interface. Typically, you would update an existing UI (if it exists),
or set up a new instance of UI by calling the `SStgui` subsystem.
- `ui_data` - In this proc you munges whatever complex data your `src_object`
has into an associative list, which will then be sent to UI as a JSON string.
- `ui_act` - This proc receives user actions and reacts to them by changing
the state of the game.
- `ui_state` (set in `ui_interact`) - This var dictates under what conditions
a UI may be interacted with. This may be the standard checks that check if
you are in range and conscious, or more.
Once backend is complete, you create an new interface component on the
frontend, which will receive this JSON data and render it on screen.
States are easy to write and extend, and what make tgui interactions so
powerful. Because states can be overridden from other procs, you can build
powerful interactions for embedded objects or remote access.
## Using It
### Backend
Let's start with a very basic hello world.
```dm
/obj/machinery/my_machine/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = default_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
if(!ui)
ui = new(user, src, ui_key, "my_machine", name, 300, 300, master_ui, state)
ui.open()
```
This is the proc that defines our interface. There's a bit going on here, so
let's break it down. First, we override the ui_interact proc on our object. This
will be called by `interact` for you, which is in turn called by `attack_hand`
(or `attack_self` for items). `ui_interact` is also called to update a UI (hence
the `try_update_ui`), so we accept an existing UI to update. The `state` is a
default argument so that a caller can overload it with named arguments
(`ui_interact(state = overloaded_state)`) if needed.
Inside the `if(!ui)` block (which means we are creating a new UI), we choose our
template, title, and size; we can also set various options like `style` (for
themes), or autoupdate. These options will be elaborated on later (as will
`ui_state`s).
After `ui_interact`, we need to define `ui_data`. This just returns a list of
data for our object to use. Let's imagine our object has a few vars:
```dm
/obj/machinery/my_machine/ui_data(mob/user)
var/list/data = list()
data["health"] = health
data["color"] = color
return data
```
The `ui_data` proc is what people often find the hardest about tgui, but its
really quite simple! You just need to represent your object as numbers, strings,
and lists, instead of atoms and datums.
Finally, the `ui_act` proc is called by the interface whenever the user used an
input. The input's `action` and `params` are passed to the proc.
```dm
/obj/machinery/my_machine/ui_act(action, params)
if(..())
return
if(action == "change_color")
var/new_color = params["color"]
if(!(color in allowed_coors))
return FALSE
color = new_color
return TRUE
update_icon()
```
The `..()` (parent call) is very important here, as it is how we check that the
user is allowed to use this interface (to avoid so-called href exploits). It is
also very important to clamp and sanitize all input here. Always assume the user
is attempting to exploit the game.
Also note the use of `. = TRUE` (or `FALSE`), which is used to notify the UI
that this input caused an update. This is especially important for UIs that do
not auto-update, as otherwise the user will never see their change.
### Frontend
Finally, you have to make a UI component. This is also a source of
confusion for many new users. If you got some basic javascript and HTML
knowledge, that should ease the learning process, although we recommend
getting yourself introduced to
[React and JSX](https://reactjs.org/docs/introducing-jsx.html).
A component is not a regular HTML. A component is a pure function, which
accepts a `props` object (it contains properties passed to a component),
and outputs an HTML-like structure consisting of regular HTML elements and
other UI components.
Interface component will always receive 1 prop which is called `state`.
This object contains a few special values:
- `config` is always the same and is part of core tgui
(it will be explained later),
- `data` is the data returned from `ui_data`
```jsx
import { useBackend } from '../backend';
import { Section, LabeledList } from '../components';
export const SampleInterface = props => {
const { act, data } = useBackend(props);
return (
<Section title="Health status">
<LabeledList>
<LabeledList.Item label="Health">
{data.health}
</LabeledList.Item>
<LabeledList.Item label="Color">
{data.color}
</LabeledList.Item>
</LabeledList>
</Section>
);
};
```
This syntax can be very confusing at first, but it is very important to
realize that this is just a natural extension of javascript. Here's a few
examples of this syntax:
Return a different element based on a condition:
```jsx
if (condition) {
return <Foo />;
}
return <Bar />;
```
Conditionally render a element inside of another element:
```jsx
<Box>
{showProgress && (
<ProgressBar value={progress} />
)}
</Box>
```
Looping over the array to make an element for each item:
```jsx
<LabeledList>
{items.map(item => (
<LabeledList.Item key={item.id} label={item.label}>
{item.content}
</LabeledList.Item>
))}
</LabeledList>
```
### Routing table
Once you finished creating your interface, you must add a route entry to
the large `ROUTES` object, otherwise tgui won't know when and how to render
your interface. Key of this `ROUTES` object corresponds to the interface
name you use in DM code.
```js
import { SampleInterface } from './interfaces/SampleInterface';
const ROUTES = {
sample_interface: {
component: () => SampleInterface,
scrollable: true,
},
};
```
## Copypasta
We all do it, even the best of us. If you just want to make a tgui **fast**,
here's what you need (note that you'll probably be forced to clean your shit up
upon code review):
```dm
/obj/copypasta/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = default_state) // Remember to use the appropriate state.
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
if(!ui)
ui = new(user, src, ui_key, "copypasta", name, 300, 300, master_ui, state)
ui.open()
/obj/copypasta/ui_data(mob/user)
var/list/data = list()
data["var"] = var
return data
/obj/copypasta/ui_act(action, params)
if(..())
return
if(action == "copypasta")
var/newvar = params["var"]
// A demo of proper input sanitation.
var = CLAMP(newvar, min_val, max_val)
return TRUE
update_icon() // Not applicable to all objects.
```
And the template:
```jsx
import { useBackend } from '../backend';
import { Section, LabeledList } from '../components';
export const SampleInterface = props => {
const { act, data } = useBackend(props);
return (
<Section title="Section name">
<LabeledList>
<LabeledList.Item label="Variable">
{data.var}
</LabeledList.Item>
</LabeledList>
</Section>
);
};
```