Why the fuck did the rest of the stuff dissapear
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
# Converting old tgui interfaces to tgui-next
|
||||
|
||||
This guide is going to assume you already know roughly how tgui-next works, how to make new uis, etc. It's mostly aimed at helping translate concepts between tgui and tgui-next, and clarify some confusing parts of the transition.
|
||||
|
||||
## Backend
|
||||
|
||||
Backend in almost every case does not require any changes. In particularly heavy ui cases, something to be aware of is the new `ui_static_data()` proc. This proc allows you to split some data sent to the interface off into data that will only be sent on ui initialize and when manually updated by elsewhere in the code. Useful for things like cargo where you have a very large set of mostly identical code.
|
||||
|
||||
Keep in mind that for uis where *all* data doesn't need to be live updating, you can just toggle off autoupdate for the ui instead of messing with static data.
|
||||
|
||||
## Frontend
|
||||
|
||||
The very first thing to note is the name of the `ract` file containing the old interface. Whatever the name is (minus the extension) is going to be what the route key is going to be.
|
||||
|
||||
One thing I like to do before starting work on a conversion is screenshot what the old interface looks like so I have something to reference to make sure that the styling can line up as well.
|
||||
|
||||
## General syntax changes
|
||||
|
||||
Ractive has a fairly different templating syntax from React.
|
||||
|
||||
### `data`
|
||||
|
||||
You likely already know that React data inserts look like this
|
||||
|
||||
```jsx
|
||||
{data.example_data}
|
||||
```
|
||||
|
||||
Ractive looks very similar, the only real difference is that React uses one paranthesis instead of two.
|
||||
|
||||
```ractive
|
||||
{{data.example_data}}
|
||||
```
|
||||
|
||||
However, you may occasionally come across data inserts that instead of referencing the `data` var or things contained within it instead reference `adata`. `adata` was short for animated data, and was used for smooth number animations in interfaces. instead of having a seperate data structure for this. tgui-next instead uses a component, which is `AnimatedNumber`.
|
||||
|
||||
`AnimatedNumber` is used like this
|
||||
|
||||
```jsx
|
||||
<AnimatedNumber value={data.example_data}/>
|
||||
```
|
||||
|
||||
Make sure you don't forget to import it.
|
||||
|
||||
### Conditionals
|
||||
|
||||
Ractive conditionals look very different from React conditionals.
|
||||
|
||||
A ractive `if` (only render if result of expression is true) looks like this
|
||||
|
||||
```ractive
|
||||
{{#if data.condition}}
|
||||
<span>Example Render</span>
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
The equivalent React would be
|
||||
|
||||
```jsx
|
||||
{!!data.condition && (
|
||||
<Fragment>Example Render</Fragment>
|
||||
)}
|
||||
```
|
||||
|
||||
This might look a bit intimidating compared to the reactive part but it's not as complicated as it seems:
|
||||
|
||||
1. A new jsx context is opened with `{}`
|
||||
2. jsx contexts like this always render whatever the return value is, so we can use `&&` to return a value we want. `&&` returns the last true value (or not "falsey" because this is js).
|
||||
3. jsx tags are never "falsey", so a conditioned paired with a jsx tag will mean the condition being true will continue on and return the tag. `()` is just used to contain the tag
|
||||
4. The `!!` is not a special operator, it is a literal double negation. This is because most `false` values coming from byond are going to actually be `0`, which would be rendered if the condition is false. Negating `0` returns `true`, negating `true` returns `false`, which isn't rendered.
|
||||
5. `Fragment` is actually a true "dead tag". It's similar to `span` in that it just contains things without providing functionality, but it's unwrapped before the final render and children of it are injected into its parent. In a case where you only need to render text without any styling, it's probably better to just return a string literal (`"Example Render"`), but this was just to illustrate that you can put any tag in this expression.
|
||||
|
||||
You don't really need to know all this to understand how to use it, but I find it helps with understanding when things go wrong.
|
||||
|
||||
Ractive conditionals can have an `else` as well
|
||||
```ractive
|
||||
{{#if data.condition}}
|
||||
value
|
||||
{{else}}
|
||||
other value
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
Similarly to the previous example, just add a `||` operator to handle the
|
||||
"falsy" condition:
|
||||
|
||||
```jsx
|
||||
{!!data.condition && (
|
||||
<Fragment>value</Fragment>
|
||||
) || (
|
||||
<Fragment>other value</Fragment>
|
||||
)}
|
||||
```
|
||||
|
||||
There's also our good old friend - the ternary:
|
||||
|
||||
```jsx
|
||||
{data.condition ? 'value' : 'other value'}
|
||||
```
|
||||
|
||||
Keep in mind you can also use tags here like the conditional example,
|
||||
and you can mix string literals, values, and tags as well.
|
||||
|
||||
```jsx
|
||||
{data.is_robot ? (
|
||||
<Button content="Robot Button"/>
|
||||
) : 'Not a robot'}
|
||||
```
|
||||
|
||||
### Loops
|
||||
|
||||
Ractive has loops for iterating over data and inserting something for each
|
||||
member of an array or object
|
||||
|
||||
```
|
||||
{{#each data.list_of_foo}}
|
||||
foo {{number}} is here.
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
This didn't care whether the data was an array or an object, and members of each entry of the loop were "unwrapped" so to say. `{{number}}` in that example is referring to the `{{number}}` value on the entry of the list for that iterate.
|
||||
|
||||
The React equivalent to this is going to be `map`.
|
||||
|
||||
_AN IMPORTANT DISTINCTION HERE IS THAT NOW WE CARE WHETHER THIS IS AN OBJECT OR AN ARRAY BEING ACTED ON._
|
||||
|
||||
Objects are represented by `{}`, arrays by `[]`
|
||||
|
||||
"How can I tell?" you may ask. It's fairly simple, associated lists on the byond side are going to be turned into objects when they get json converted, normal lists are going to be turned into arrays.
|
||||
|
||||
`list("bla", "blo")` would become `["bla", "blo"]` and `list("foo" = 1, "bar" = 2)` would become `{"foo": 1, "bar": 2}`
|
||||
|
||||
First things first, above the `return` of the function you're making the interface in, you're going to want to add something like this
|
||||
```jsx
|
||||
const things = data.things || [];
|
||||
```
|
||||
|
||||
This ensures that you'll never be reading a null entry by mistake. Substitute `{}` for objects as appropriate.
|
||||
|
||||
If it's an array, you'll want to do this in the template
|
||||
```jsx
|
||||
{things.map(thing => (
|
||||
<Fragment>Thing {thing.number} is here!</Fragment>
|
||||
))}
|
||||
```
|
||||
|
||||
`map` is a function that calls a passed function (a lambda) on each entry, and returns the value. You should already know that returned tags and values (except `false`) get rendered, so that's how it's rendering each time.
|
||||
|
||||
A lambda is what's known as an anonymous function, it's a function that doesn't have a name that's only used for a specific usage. `map` wants a function that has one parameter, so we define one parameter then use `=>` to say the parameter has to do with the following block.
|
||||
|
||||
`parameter => ()` is just a shorthand for `parameter => {return();}`
|
||||
|
||||
This is quite a bit higher concept than ractive's each statements, so feel free to look around and ~~copy paste~~ learn from how other interfaces use this.
|
||||
|
||||
Now for objects, there's a genuinely pretty gross syntax here. We apoligize, it's related to ie8 compatibility nonsense.
|
||||
|
||||
```jsx
|
||||
{map((value, key) => {
|
||||
return (
|
||||
<Fragment>Key is {key}, value is {value}</Fragment>
|
||||
);
|
||||
})(fooObject)}
|
||||
```
|
||||
|
||||
Again, sorry for this syntax. `fooObject` would be the object being iterated on, value would be the value of the iterated entry on the list, and key would be the key. the naming of value and key isn't important here, but knowing that it goes `value`, `key` in that order is important.
|
||||
|
||||
It is sometimes better to preemptively convert an object to array before
|
||||
the big return statement, like this:
|
||||
|
||||
```jsx
|
||||
const fooArray = map((value, key) => {
|
||||
return { key, value };
|
||||
})(fooObject);
|
||||
```
|
||||
|
||||
Or if you just want to discard all keys, this will also work nicely:
|
||||
|
||||
```jsx
|
||||
const fooArray = toArray(fooObject);
|
||||
```
|
||||
|
||||
Also occasionally you'd see an else:
|
||||
|
||||
```
|
||||
{{#each data.potentially_empty_list}}
|
||||
Thing "{{name}}" is in this list!
|
||||
{{else}}
|
||||
None found!
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
This would iterate using the first contents each time, or display the second option if the list was empty.
|
||||
|
||||
To do a similar thing in JSX, just check if array is empty like this:
|
||||
|
||||
```jsx
|
||||
{fooArray.length === 0 && 'fooArray is empty.'}
|
||||
{fooArray.map(foo => (
|
||||
<Fragment>Foo is {foo}</Fragment>
|
||||
))}
|
||||
```
|
||||
|
||||
### Extra Stuff
|
||||
|
||||
I'll put some extra stuff here when I think of it.
|
||||
|
||||
## Components
|
||||
|
||||
This will be a reference of tgui components and the tgui-next equivalent.
|
||||
|
||||
### `ui-display`
|
||||
|
||||
Equivalent of `<ui-display>` is `<Section>`
|
||||
|
||||
```
|
||||
<ui-display title="Status">
|
||||
Contents
|
||||
</ui-display>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```jsx
|
||||
<Section title="Status">
|
||||
Contents
|
||||
</Section>
|
||||
```
|
||||
|
||||
A feature sometimes used is if `ui-display` has the `button` property, it will contain a `partial` command. This becomes the `buttons` property on `Section`:
|
||||
|
||||
```
|
||||
<ui-display title="Status" button>
|
||||
{{#partial button}}
|
||||
<ui-button /> // lots more button bullshit here
|
||||
{{/partial}}
|
||||
Contents
|
||||
</ui-display>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```jsx
|
||||
<Section
|
||||
title="Status"
|
||||
buttons={(
|
||||
<Button />
|
||||
)}>
|
||||
Contents
|
||||
</Section>
|
||||
```
|
||||
|
||||
### `ui-section`
|
||||
|
||||
Very important to note `ui-section` is NOT the equivalent of `Section`
|
||||
|
||||
`<ui-section>` does not have a direct equivalent, but the closest equivalent is `<LabeledList>`
|
||||
|
||||
```
|
||||
<ui-section label="power">
|
||||
No Power
|
||||
</ui-section>
|
||||
<ui-section label="connection">
|
||||
No Connection
|
||||
</ui-section>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```jsx
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="power">
|
||||
No Power
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="connection">
|
||||
No Connection
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
```
|
||||
|
||||
Important to note that `LabeledList.Item` has `buttons` as well.
|
||||
|
||||
Also good to know that if you need the contents of a `LabeledList.Item` to be colored, you can just set the `color` prop on it instead of putting a `span` inside it.
|
||||
|
||||
### `ui-notice`
|
||||
|
||||
`<ui-notice>` has a direct equivalent in `<NoticeBox>`
|
||||
|
||||
```
|
||||
<ui-notice>
|
||||
Notice stuff!
|
||||
</ui-notice>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```jsx
|
||||
<NoticeBox>
|
||||
Notice stuff!
|
||||
</NoticeBox>
|
||||
```
|
||||
|
||||
### `ui-button`
|
||||
|
||||
The equivalent of `ui-button` is `Button` but it works quite a bit differently.
|
||||
|
||||
```
|
||||
<ui-button
|
||||
state='{{data.condition ? "disabled" : null}}'
|
||||
action="ui_action"
|
||||
params={param: value}>
|
||||
Click
|
||||
</ui-button>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```
|
||||
<Button
|
||||
content="Click"
|
||||
disabled={data.condition}
|
||||
onClick={() => act(ref, "ui_action", {param: value})}/>
|
||||
```
|
||||
@@ -0,0 +1,245 @@
|
||||
# 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
|
||||
switch(action)
|
||||
if("change_color")
|
||||
var/new_color = params["color"]
|
||||
if(!(color in allowed_coors))
|
||||
return
|
||||
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, 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 { Section, LabeledList } from '../components';
|
||||
|
||||
const SampleInterface = props => {
|
||||
const { state } = props;
|
||||
const { config, data } = state;
|
||||
const { ref } = config;
|
||||
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 need to 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 { Section, LabeledList } from '../components';
|
||||
|
||||
const SampleInterface = props => {
|
||||
const { state } = props;
|
||||
const { config, data } = state;
|
||||
const { ref } = config;
|
||||
return (
|
||||
<Section title="Section name">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Variable">
|
||||
{data.var}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
```
|
||||
Reference in New Issue
Block a user