Files
Yogstation/tgui/docs/tutorial-and-examples.md
TheGamerdk ca57f72a78 jamie stinks (#16349)
* Modernizes TGUI (#4219)

* Ports the first two PRs. CBT works local

* tchussi oops

* linttobuild

* Update Dockerfile for CBT (#56175)

Follow-up to fix the failing Docker CI on master.

- Reorganize the entire Dockerfile to be more readable
- Inline the tgstation/byond Dockerfile into our own, so we can change the base distro at will
- Also allows us to trash the dependencies.sh<->Dockerfile hack
- Use 32-bit libs on a 64-bit distro so that we can download and run recent 64-bit Node binaries
- Call tools/build/build rather than DreamMaker directly

* TGUI 43 plus hotfix 1

* https://github.com/tgstation/tgstation/pull/58701

* https://github.com/tgstation/tgstation/pull/56223

* https://github.com/tgstation/tgstation/pull/56229

* https://github.com/tgstation/tgstation/pull/56713

* https://github.com/tgstation/tgstation/pull/56797

* https://github.com/tgstation/tgstation/pull/57081/

* https://github.com/tgstation/tgstation/pull/57154

* https://github.com/tgstation/tgstation/pull/57251

* https://github.com/tgstation/tgstation/pull/56269

* https://github.com/tgstation/tgstation/pull/57277

* https://github.com/tgstation/tgstation/pull/57287

* https://github.com/tgstation/tgstation/pull/57326

* QF

* GimmeHopeUpdateExtra_Pr_Labels

* Flush queue in preloading part of tgui_panel (#57432)

* Flush queue in preloading part of tgui_panel

* Update tgui_panel.dm

* Compiles

* https://github.com/tgstation/tgstation/pull/57461

* tgui: Upgrade to Yarn 2.4.1 and TypeScript 4.2 (#57465)

* Fix tgui reloading (#57499)

* Add note about rust-g for building on Linux (#57622)

This PR adds a minor note about needing to compile rust-g to Linux building instructions. I'm not sure if this is the appropriate place for it, but I don't see a better place to put it.

* Fixes build script failing properly on windows. (#57623)

* Adds easy to extend custom svg font. (#57717)

Just throw in svgs into tgui/packages/tgfont/icons directory and you can use them in tgui with tg- prefix.

Co-authored-by: Style Mistake <stylemistake@gmail.com>
Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>

* Typescript fixups. (#57746)

* Improve tgui routing to allow interface subdirectories (#57825)

* Improve tgui routing to allow interface subdirectories

* Reverse order of interface file resolution, to be consistent with common expectation (top-level .tsx, top-level .js, directory .tsx, directory.js) and the way Webpack checks

* Fixes TGS compiles on windows. (#57834)

Batch quotes strike again.

* tgui: ESLint and VSCode settings improvements (#57905)

    Added column 80 rulers to all javascript and scss files.
    Added a "prettier" config to have sensible defaults for those who prefer to use it.
    Added RadarJS (fork of SonarQube's SonarJS). Launchable only via bin/tgui --lint-harder for now.
    Disabled ESLint rule for parens around arrow function arguments, because in TypeScript they're pretty much always required, and I don't want to replace it throughout the codebase.
    Removed unused javascript extensions from tooling (jsx, mjs).

* https://github.com/tgstation/tgstation/pull/57931

* https://github.com/tgstation/tgstation/pull/58061

* https://github.com/tgstation/tgstation/pull/58081

* https://github.com/tgstation/tgstation/pull/58212

* https://github.com/tgstation/tgstation/pull/58215

* https://github.com/tgstation/tgstation/pull/58219

* Add "Except DM" build mode to build.js (#58245)

* https://github.com/tgstation/tgstation/pull/58484

* FixBaconFabandPodLauncher

* haharunlintmanuallyfirstkids

* One last fab change then its perf

* f5willwork

* This May Do it

* prune

* Fix Docker build failing on a clean checkout (#56190)

Follow-up to #56175 which turned out to be accidentally relying on Node already being downloaded.

* Huh? Rebuild you better be a fluke

* getinthere

* Syncretize

* Revert "Syncretize"

This reverts commit 41749b68868d2af1b82de9ba6de39cf8052dd1ee.

* zzzz

* quick path change

* another quick

* Correct Dockerfile, bloats it with GCC lol.

* small

* back2oldfile I HATE DOCKER I HATE EXTOOLS

* firstthingnotdonelol

* yuabzn

* v

* reverttoclassic

* Difffffffffff

* prec1s

* ccc

* ok

* bitte

* jesus im dum

* z

* Nuke Window resizable and improve drag code (#56727)

* comp

Co-authored-by: Tad Hardesty <tad@platymuus.com>
Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
Co-authored-by: Cyprex <35031555+Cyprex@users.noreply.github.com>
Co-authored-by: AnturK <AnturK@users.noreply.github.com>
Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
Co-authored-by: Mordent <62817778+mordent-goonstation@users.noreply.github.com>
Co-authored-by: Jonathan Rubenstein <jrubcop@gmail.com>

* thanks yarn!

* Update Panel.js

* compiles

* restores exosuit

* chmod

* Update travis_config.txt

* honk

* honk2

* Update turdis.yml

* Update ExosuitFabricator.js

* Revert "Update turdis.yml"

This reverts commit 7517df56e5.

* Revert "Update ExosuitFabricator.js"

This reverts commit ac31fa4543.

* wtf is this

* lol

* Update tgui.dm

Co-authored-by: Sinestia <40812746+Sinestia@users.noreply.github.com>
Co-authored-by: Tad Hardesty <tad@platymuus.com>
Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
Co-authored-by: Cyprex <35031555+Cyprex@users.noreply.github.com>
Co-authored-by: AnturK <AnturK@users.noreply.github.com>
Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
Co-authored-by: Mordent <62817778+mordent-goonstation@users.noreply.github.com>
Co-authored-by: Jonathan Rubenstein <jrubcop@gmail.com>
Co-authored-by: Jamie D <993128+JamieD1@users.noreply.github.com>
2023-01-02 14:03:15 +00:00

355 lines
11 KiB
Markdown

# Tutorial and Examples
## Main concepts
Basic tgui backend code consists of the following vars and procs:
```
ui_interact(mob/user, datum/tgui/ui)
ui_data(mob/user)
ui_act(action, params)
ui_state()
```
- `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` - This proc 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, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "MyMachine")
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.
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
. = 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, let's make a React Component for your interface. This is also
a source of confusion for new developers. 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 React component is not a regular HTML template. A component is a
javascript function, which accepts a `props` object (that contains
properties passed to a component) and a `context` object (which is
necessary to access UI data) as arguments, and outputs an HTML-like
structure.
So let's create our first React Component. Create a file with a name
`SampleInterface.js` (or any other name you want), and copy this code
snippet (make sure component name matches the file name):
```jsx
import { useBackend } from '../backend';
import { Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
export const SampleInterface = (props, context) => {
const { act, data } = useBackend(context);
// Extract `health` and `color` variables from the `data` object.
const {
health,
color,
} = data;
return (
<Window>
<Window.Content scrollable>
<Section title="Health status">
<LabeledList>
<LabeledList.Item label="Health">
{health}
</LabeledList.Item>
<LabeledList.Item label="Color">
{color}
</LabeledList.Item>
<LabeledList.Item label="Button">
<Button
content="Dispatch a 'test' action"
onClick={() => act('test')} />
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
```
Here are the key variables you get from a `useBackend(context)` function:
- `config` is part of core tgui. It contains meta-information about the
interface and who uses it, BYOND refs to various objects, and so forth.
You are rarely going to use it, but sometimes it can be used to your
advantage when doing complex UIs.
- `data` is the data returned from `ui_data` and `ui_static_data` procs in
your DM code. Pretty straight forward.
- Note, that javascript doesn't have associative arrays, so when you
return an associative list from DM, it will be available in `data` as a
javascript object instead of an array. You can use it normally
like so: `object.key`, so it's not a problem if it's representing a
data structure, but common `Array` methods, such as `array.map(item => ...)`,
are not available on it. Always prefer returning clean arrays from your
code, since arrays are easier to work with in javascript!
- `act(name, params)` is a function, which you can call to dispatch an action
to your DM code. It will be processed in `ui_act` proc. Action name will be
available in `params["action"]`, mixed together with the rest of parameters
you have passed in `params` object.
**Let's talk about the syntax.**
The syntax you're seeing here is called JSX - a very simple extension of the
core javascript language. It's basically a pre-processor, that takes
expressions that look like html, and turns them into function calls.
Take a look at this example:
```jsx
<div className={'color-' + status}>
You are in {status} condition!
</div>
```
After compiling the code above, this is what it becomes:
```js
createElement('div',
{ className: 'color-' + status },
'You are in ', status, ' condition!');
```
It is very important to remember, that JSX is just a javascript expression
made out of `createElement` function calls. Naturally, this allows doing
all sorts of stuff on these expressions, just like you would with anything
else in javascript.
Take a look at these examples:
**Render an element inside of another element if `showProgress` is true.**
This example uses the `&&` operator (the logical AND). It returns
the first operand if it evaluates to `false`, and returns the second operand
if it evaluates to `true`.
If `showProgress` is `true`, the whole expression evaluates
to a `<ProgressBar />` element. If `showProgress` is `false`, the whole
expression evaluates to `false`, and `false` is not rendered by React.
```jsx
<Box>
{showProgress && (
<ProgressBar value={progress} />
)}
</Box>
```
You can also use the `||` operator (the logical OR), which works the same way,
except it will return the second operand on `false` instead of `true`.
**Loop over the array to map every item to a corresponding React element.**
`Array.map()` is a method, that calls a function on every item in the array,
and builds a new array based on what was returned by that function.
```jsx
<LabeledList>
{items.map(item => (
<LabeledList.Item
key={item.id}
label={item.label}>
{item.content}
</LabeledList.Item>
))}
</LabeledList>
```
If you need more examples of what you can do with React, see the
[interface conversion guide](docs/converting-old-tgui-interfaces.md).
#### Splitting UIs into smaller, modular components
You interface will eventually get really, really big. The easiest thing
you can do in this situation, is divide and conquer. Grab a chunk of your
JSX code, and wrap it into a second, smaller React component:
```jsx
import { useBackend } from '../backend';
import { Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
export const SampleInterface = (props, context) => {
return (
<Window>
<Window.Content scrollable>
<HealthStatus user="Jerry" />
</Window.Content>
</Window>
);
};
const HealthStatus = (props, context) => {
const { act, data } = useBackend(context);
const {
user,
} = props;
const {
health,
color,
} = data;
return (
<Section title={"Health status of: " + user}>
<LabeledList>
<LabeledList.Item label="Health">
{health}
</LabeledList.Item>
<LabeledList.Item label="Color">
{color}
</LabeledList.Item>
</LabeledList>
</Section>
);
};
```
## 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, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "copypasta")
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
switch(action)
if("copypasta")
var/newvar = params["var"]
// A demo of proper input sanitation.
var = CLAMP(newvar, min_val, max_val)
. = TRUE
update_icon() // Not applicable to all objects.
```
And the template:
```jsx
import { useBackend } from '../backend';
import { Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
export const SampleInterface = (props, context) => {
const { act, data } = useBackend(context);
// Extract `health` and `color` variables from the `data` object.
const {
health,
color,
} = data;
return (
<Window>
<Window.Content scrollable>
<Section title="Health status">
<LabeledList>
<LabeledList.Item label="Health">
{health}
</LabeledList.Item>
<LabeledList.Item label="Color">
{color}
</LabeledList.Item>
<LabeledList.Item label="Button">
<Button
content="Dispatch a 'test' action"
onClick={() => act('test')} />
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
```