This UI framework is mostly composed of four main parts:
- Main JS file and supporting HTML code
- UI DM datum, responsible for linking opened ui window with code
- Object specific code, that generates data for uis, and handles actions.
- SubSystem responsible for keeping track of all uis and making sure they tick.
First we have to create a way to open ui, for example, a proc that's called when we want to open ui:
/datum/mydatum/ui_interact(mob/user, var/datum/topic_state/state = default_state)
var/datum/vueui/ui = SSvueui.get_open_ui(user, src)
if (!ui)
ui = new(user, src, "uiname", 300, 300, "Title of ui", state = state)
ui.open()
On first line we check if we already have open ui for this user, if we already have, then we just open it on last line, but if we don't have existing ui, we then create a new one.
But how we pass data to it? There are two ways to do it, first one is to pass initial data in constructor: new(user, src, "uiname", 300, 300, "Title of ui", data)
. But it's recommended to use vueui_data_change
proc for data feed to ui. More information is available on different ways to provide data.
/datum/mydatum/vueui_data_change(var/list/newdata, var/mob/user, var/datum/vueui/ui)
if(!newdata)
// generate new data
return list("counter" = 0)
// Here we can add checks for difference of state and alter it
// or do actions depending on its change
if(newdata["counter"] >= 10)
return list("counter" = 0)
Every time server receives of front end state change it calls this proc to make sure that front end state didn't go too far from data it should represent. If everything is alright with newstate
and no push to front end of data is needed then it should return 0
or null
, else return data that should be pushed.
Simply use Topic
proc to get ui action calls from ui.
/datum/mydatum/Topic(href, href_list)
if(href_list["action"] == "test")
to_world("Got Test Action! [href_list["data"]]")
return
It is recommended to enable debugging for this step to make things easier.
To create ui itself, you need to create .vue
file somewhere in \vueui\src\components\view
. Depending on location of .vue file depends on its component name. Example \vueui\src\components\view\uiname.vue
file:
<template>
<div>
<p>{{ counter }}</p>
<vui-button :params="{ action: 'test', data: 'This is from ui.' }">Call topic</vui-button>
<vui-button @click="counter++">Increment counter</vui-button>
</div>
</template>
<script>
export default {
data() {
return this.$root.$data.state; // Make data more easily accessible
}
};
</script>
<style lang="scss" scoped>
p {
font-size: 3em;
}
</style>
This ui framework requires whole ui to be compiled for changes to be available. Compilation requires Node.js runtime (>=13.6.0), that is obtainable in various ways, most common is install from official site. To do initial dependency setup run npm install
to gather all dependencies needed for ui. Single compilation can be done with npm run build-dev
, but if you constantly do changes, then npm run dev
is more convenient, as it compiles everything as soon as change is detected. To make client side code better, you should also lint code with command npm run lint
.
When changes are made to ui code updated compiled code is needed to be included with PR. To compile code for production run npm run build
Currently there is multiple ways of providing data to ui, some are more advanced, others are simpler and get job done.
In other words, var monitor is just one-way link between object's vars and ui data. It is great when object has vars that can be directly passed to ui. All you need to do is to define somewhere vars to be monitored.
VUEUI_MONITOR_VARS(/datum/mydatum, mydatummonitor)
watch_var("objects_var_name", "uis_var_name")
watch_var("other_datum", "has_other_datum", CALLBACK(null, .proc/transform_to_boolean, FALSE))
In this example it monitors /datum/mydatum/var/objects_var_name
and presents it to ui as uis_var_name
. On last line we define other watcher, that has transform (sanitizer) callback function set. In this case it's set to call /datum/vueui_var_monitor/proc/transform_to_boolean
right before var is transferred to ui data list. It also allows us to pass additional options to call back function, in this case it's boolean that determines if conversion is inverting. Other parameters passed to callback right after those defined in CALLBACK
are: value of source var, last value of ui var, user that is interacting with ui, ui datum. callback(..., var/source, var/current, var/mob/user, var/datum/vueui/ui)
There is also plausibility to extend var monitors with other ways by extending default vueui_data_change
proc. For example of this, you can look at photocopier.dm
This way is more primitive, but simpler and allows reverse data flow. Let's look at example:
/datum/mydatum/vueui_data_change(var/list/newdata, var/mob/user, var/datum/vueui/ui)
if(!newdata)
. = newdata = list()
VUEUI_SET_CHECK(newdata["uis_var_name"], objects_var_name, ., newdata)
VUEUI_SET_CHECK(newdata["has_other_datum"], !!other_datum, ., newdata)
VUEUI_SET_CHECK_LIST(newdata["some_list"], other_list, ., newdata)
VUEUI_SET_CHECK_IFNOTSET(newdata["text"], "[other_datum]", ., newdata)
This code functionally is same as example that is provided for var monitors. Macro VUEUI_SET_CHECK
compare if first two params are equal, if not, then it makes them equal, and also sets third parameter to fourth one (I this case it sets .
to newdata
, what makes it return data). VUEUI_SET_CHECK_LIST
should be used if the first two params are lists. VUEUI_SET_CHECK_IFNOTSET
is almost exactly same, but it's checks if first var is not already set (is null), and if it is null, then it sets it.
VUEUI_MONITOR_VARS(/datum/mydatum, mydatummonitor)
watch_var("objects_var_name", "uis_var_name")
watch_var("other_datum", "has_other_datum", CALLBACK(null, .proc/transform_to_boolean, FALSE))
/datum/mydatum/vueui_data_change(var/list/data, var/mob/user, var/datum/vueui/ui)
var/monitordata = ..()
if(monitordata)
. = data = monitordata
VUEUI_SET_CHECK_IFNOTSET(data["text"], "[other_datum]", ., data)
Asks all uis to call object
's vueui_data_change
proc to make all uis up tp date. Should be used when bigger change was done or action done change that would affect global data.
Gets a list of all open uis for specified object. This allows to interact with individual uis.
Closes all open uis for specified object.
Gets a singular open UI for specified user and obj combination.
Determines currently active view component for this ui datum. Should be combines with check_for_change()
or push_change()
.
It is also plausible for server to send templates dynamically. First character of template should be ?
to specify to vueui that its going to be not recompiled view
component, but a template. Also template should have single root element. Example for such component would be ?<span>Time since server boot: {{ $root.$data.wtime / 2 }} seconds.</span>
Determines header component that is used with this ui. Should be set before ui.open()
.
Checks for change using ui.check_for_change()
every process()
tick of SSvueui
(what is approximately 2 seconds). This should be only used when data changes unpredictably.
Tries to open this ui, and sends all necessary assets for proper ui rendering. If ui has no data, then calls object.vueui_data_change(null ...)
to obtain initial ui data.
Sends singular asset to client for use in ui, after this call you might need to update client-side asset index. To do so call push_change(null)
.
Adds asset for ui use, but does not send it to client. It will be sent in during next ui.open()
call or if it's done manually with ui.send_asset(name)
combined with push_change(null)
.
Removes asset from future use in ui. But client-side asset index isn't updated immediately to reflect removal of asset.
Pushes data change to client. This also pushes changes to metadata, what includes: title, world time, ui status, active ui component, client-side asset index.
Checks with object.vueui_data_change
if data has changed, if so, then change is pushed. If forcedPush is true, then it pushes change anyways.
Resizes open UI to specified dimensions. Usefully when UI content changes size dramatically. Should be avoided in regular use.
This call should be used if external change was detected. It checks if user still can use this ui, and what's its usability level.
This is helper variable meant to store references, or other data that is linked to that specific ui. This is just a helper field for keeping track of what goes where.
This variable provides a way to obtain instance of ui that has invoked this Topic()
call. Fast and simple way to safetly obtain it using this var is:
var/datum/vueui/ui = href_list["vueui"]
if(!istype(ui))
return
To enable debug mode and make figuring out things easier do following steps:
- Enable development mode for ui by building it using
npm run build-dev
ornpm run dev
if you want it to auto rebuild on change. - Enable debugging for ui datum, by inserting this line anywhere. (This will always push new JS file each time open() is called and show data in JSON format at the end of ui)
#define UIDEBUG
- Use URL provided by debug info in Internet explorer / Microsoft Edge to use inspector to analyze ui behaviour.
You should look at official Vue.js guide. As it's more detailed and more accurate than any explanation that could have been written here.
To access global metadata for this ui, use this.$root.$data
or $root.$data
, depending on context. To simplify access you can use following hack that links global data to local component data. It simplifies access to data. following explanation assumes you have done so. Note: changes made on client side aren't sent to server, so please do not alter them.
<script>
export default {
data() {
return this.$root.$data;
}
};
</script>
This is outside metadata, so changes to this variable can be made, but this variable is expected to be a object or how BYOND calls it - a keyed list, using other type will cause errors. This variable is main way to interact with server without using Topic()
.
Note: state
can get very out of sync with ui.data
, this often occur on rapid changes. So to make sure latest data gets to ui datum, when ui data is needed for usage, <vui-button>
parameter push-state
should be set. Example: <vui-button push-state :params="{copy: 1}">Copy</vui-button>
in photocopier.vue - makes sure that copy amount is up to date. When state
object is huge, it's discouraged to solve this issue like this. Then it should be solved by sending needed data inside params
.
This is constantly counting counter updated every 200ms representing time since server has started. This should be used for displaying counters, timers, as it doesn't depend on pushed state so much, so it allows making better user experiences.
This uis title, mostly used by header components.
Topic status, used to determine how interactive is ui. Meanings of numbers can be seen \code\__defines\machinery.dm
.
As described, this makes them tick. This should shouldn't be used, unless you know what you are doing.
This determines what component / template should be used to display data.
Reference to ui datum is used by state updates, and <vui-button>
to make appropriate requests.
Button programmed to send provided data to ui object. Comes with icon support.
Example:
<vui-button :params="{ action: 'delete' }" icon="trash">Delete</vui-button>
Parameters:
$slot
- Contents of button.params
- key value pairs to send toTopic
of object. Can contain objects or arrays (DM keyed lists and lists respectively).unsafe-params
- Used to execute a genericTopic()
call to the game. Requires that you specify thesrc
object as a valid reference, otherwise it will not function. Should not generally be used, primary use-case is backwards compatibility with older APIs that are spread out over multiple objects.icon
- icon that should be used in that button. For available icons look at\vueui\styles\icons.scss
push-state
- Boolean determining if current ui state should be pushed on button click. This often results invueui_data_change
call right beforeTopic
call.
Events:
click
- Fires when button is clicked.
Simple progress bar for representing progress of a process or indicate status.
Example:
<vui-progress :value="50"></vui-progress>
Parameters:
value
- numerical value to display. Should be used with binding.max
- maximum value of provided value's range. Should be used with binding.min
- minimum value of provided value's range. Should be used with binding.
Wrapper for showing images added with ui proc ui.add_asset(name, image)
. Please note: images are sent to client when open()
is called, if it's added after, then send_asset(name)
should be used, also image index is sent with next data change, if immediate image change is needed then check_for_change(1)
or push_change(null)
should be used to send index.
Example:
<vui-img name="my-image-name"></vui-img>
Parameters:
name
- name of asset to show that was sent to client. Please note that regular<img>
parameters apply here.
(Please use VuiGroup and VuiGroupItem)
Helper for making item lists using legacy nano styles.
Example:
<vui-item label="Current health:">75%</vui-item>
Parameters:
label
- Label to display next to contentsbalance
- This determines how much space is used by content compared to label. This parameter value should be between 0 and 1.
Helper for making item lists. Automatically adjusts label width for optimal layout, makes space for content.
VuiGroup
is container for items. It should contain vui-group-item or any element that has CSS display: table-row
.
Example:
<vui-group>
<vui-group-item label="Current health:">75%</vui-item>
</vui-group>
Parameters:
label
- Label to display next to contents.
Helper to getting nice tooltips when you hover over text. Works with buttons.
Example:
<vui-tooltip label="VUI">VueUi UI element<vui-tooltip>
<vui-tooltip><template v-slot:label>ADV</template>Advanced use of tooltips</vui-tooltip>
Parameters:
$slot
- Contents of tooltip$slot:label
- Actual content that is always shown. If slot is set,label
parameter is ignored.label
- Actual text that is always shown
Numeric input helper to help inputting large and small numbers.
Example:
<vui-input-numeric width="2.5em" v-model="number" :min="1" :max="10"/>
Parameters:
value
- Initial value for input.button-count
- How many -/+ buttons to show on each side.min
- Minimum value.max
- Maximum value.push-state
- Boolean determining if current ui state should be pushed on input change.width
- Determines width of input text field.decimal-places
- How many decimal places are allowed.
Events:
input
- Fires when value changes. Value is number currently entered.
Search text field to filter objects in user input.
Example:
<vui-input-search :input="[{name: 'Bret'}, {name: 'Andrea'}]" v-model="output" :keys="['name']"/>
Parameters:
input
- Initial array with elements to search.keys
- Array of strings listing keys to be searched.include-score
- Includes internal search score in the results.{item: x, score: (0 to 1)}
. 0 - means perfect match.threshold
- Determines maximum score at witch results are cut off.
Events:
input
- Fires when search text changes. Event value is new sorted array.