Skip to content

Latest commit

 

History

History
331 lines (273 loc) · 11.6 KB

tutorial-and-examples.md

File metadata and controls

331 lines (273 loc) · 11.6 KB

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.

/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 = GLOB.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_states).

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:

/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.

/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.

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):

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 resizable>
      <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:

<div className={'color-' + status}>You are in {status} condition!</div>

After compiling the code above, this is what it becomes:

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.

<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.

<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.

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:

import { useBackend } from '../backend';
import { Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';

export const SampleInterface = (props, context) => {
  return (
    <Window resizable>
      <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):

/obj/copypasta/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.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
  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:

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 resizable>
      <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>
  );
};