Skip to content

Value Helps ​

A value help is a picker: a dialog that answers a question. Which partner? Which cost centre? The caller awaits a promise, the user picks, the promise resolves.

js
const selection = await LaravelUi5.openValueHelp({
    namespace: 'com.laravelui5.partners.valuehelp.partners',
    scope:     'colleagues',
    mode:      'single',
});
// null = cancelled · [] = confirmed with nothing · [{key, text, data?}] = a pick

It is the global dialog's sibling — same artifact idea, same shell hosting, one addition: a return channel.

The division of labour ​

One picker, many scopes. That split is the whole design, and it is what keeps a picker safe to share:

Who decidesWhat
the pickerthe shape — which columns a row has, how it looks, how you search it
the serverwhich rows, for this actor, in this scope
the callerwhich scope to open, and single or multiple

A scope is a named facet of the picker: colleagues, userAttached. Every open names one, and the name resolves server-side to an OData entity set. The client never sends a filter, and there is no unscoped picker. Which is why a value help can be opened by an app that has no business seeing every row: the scope it may open decides what it gets.

Writing one ​

The artifact ​

php
namespace App\Sales\ValueHelps;

use LaravelUi5\OData\Edm\Contracts\Type\EntityTypeInterface;
use LaravelUi5\OData\Edm\EdmPrimitiveType;
use LaravelUi5\OData\Edm\Property\Property;
use LaravelUi5\OData\Edm\Type\EntityType;
use LaravelUi5\OData\Edm\Type\PrimitiveType;
use LaravelUi5\Sdk\Ui5\AbstractUi5ValueHelp;

class CustomerPickerValueHelp extends AbstractUi5ValueHelp
{
    public const NAMESPACE   = 'com.acme.sales.valuehelp.customers';
    public const VERSION     = '1.0.0';
    public const TITLE       = 'Customer Picker';
    public const DESCRIPTION = 'Selects a customer.';
    public const VIEW        = 'com.acme.sales.view.valuehelp.Customers';

    /** The shape every scope must project: the key, and the columns the view binds. */
    public function declaredEntityType(): EntityTypeInterface
    {
        $int32  = new PrimitiveType(EdmPrimitiveType::Int32);
        $string = new PrimitiveType(EdmPrimitiveType::String);
        $key    = new Property('id', $int32);

        return new EntityType(
            namespace: static::NAMESPACE,
            name: 'Customer',
            key: [$key],
            declaredProperties: [$key, new Property('name', $string), new Property('city', $string)],
        );
    }
}

The five constants are the same as a dialog's. declaredEntityType() declares the picker's shape: its key and the properties the shared view binds. The base throws if you leave it out — deliberately, since no default shape can exist. In practice every picker needs both: the open intent refuses a picker without a #[ContributesScope] ("Unknown scope"), and ui5:sync asks every scoped picker for its shape and stops if it cannot answer.

Register it on your module — this one has an empty default, so only modules that ship pickers implement it:

php
public function getValueHelps(): array
{
    return [new ValueHelps\CustomerPickerValueHelp($this)];
}

#[Access] is optional here, and boundary-scoped: omit it on a picker only your own app opens — the app's gate already covers it. Declare one on a picker other apps may open, where it is the only authorization at the open moment.

There is no generator for value helps; author the files by hand.

The view ​

Single sap.m.Dialog root, like every shell-hosted dialog. The table's items binding and row template are not in the view — the controller base sets them from the scope the server resolved.

xml
<mvc:View controllerName="com.acme.sales.controller.valuehelp.Customers"
          xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc">
    <Dialog title="{i18n>customerPicker.title}">
        <subHeader>
            <Bar><contentMiddle><SearchField search=".onSearch"/></contentMiddle></Bar>
        </subHeader>
        <Table id="vhTable" growing="true">
            <columns><!-- … --></columns>
        </Table>
        <beginButton><Button text="{i18n>select}" type="Emphasized" press=".onSelect"/></beginButton>
        <endButton><Button text="{i18n>cancel}" press=".onCancel"/></endButton>
    </Dialog>
</mvc:View>

The controller ​

Extend com.laravelui5.core.AbstractValueHelpController and bind the table in initDialog:

js
initDialog(oDialog) {
    this.bindSelection(this.byId('vhTable'), new ColumnListItem({ cells: [ /* … */ ] }));
},

onSelect() {
    const items = this.byId('vhTable').getSelectedItems()
        .map(i => ({ key: i.getBindingContext().getProperty('id'),
                     text: i.getBindingContext().getProperty('name') }));
    this.confirmSelection(items);
},

onCancel() {
    this.cancel();
},

bindSelection sets the selection mode from the caller's mode, binds the items to the entity set the server chose, and installs one more thing worth knowing about: if the read comes back 403, the table shows an "unable to load" illustration instead of an empty list. An empty table would read as no candidates, which is a lie. Override getReadDeniedTitle() for your own wording.

confirmSelection(items) resolves the caller's promise and closes; cancel() resolves it with null. The accessors getValueHelpEntitySet(), getValueHelpMode() and getValueHelpContext() give you what the server and caller sent.

Calling one ​

js
const picked = await LaravelUi5.openValueHelp({ namespace, scope, mode: 'single', context: {} });
Option
namespacethe picker artifact
scopewhich facet — resolved and authorized on the server
mode'single' or 'multi'; a client concern, never sent to the server
contextoptional caller hints. Untrusted — a scope that is scoped to the actor ignores it

Three answers, and they are distinct on purpose: null cancelled, [] confirmed with no selection, a non-empty array picked. Denials arrive as a rejected promise, as does opening a second picker while one is open — only one value help is open at a time. On Core without the shell the call rejects outright rather than resolving empty.

The pick-only input pattern, as the shipped apps use it:

xml
<Input value="{form>/customerName}" showValueHelp="true" valueHelpOnly="true"
       valueHelpRequest=".onBrowseCustomer"/>

valueHelpOnly is deprecated as of UI5 1.119 and still the only single-control pick-only input in sap.m; the alternative is an input plus a companion button.

Contributing a scope ​

A scope on someone else's picker is declared on your entity set — the same class that carries its #[Read]:

php
use LaravelUi5\Sdk\Weave\ValueHelp\Attributes\ContributesScope;
use LaravelUi5\Sdk\Security\Attributes\Read;

#[Read(ability: 'readServiceCustomers', role: AcmeRole::Service, note: 'Read customers in service.')]
#[ContributesScope(valueHelp: 'com.acme.sales.valuehelp.customers', scope: 'inService')]
final class ServiceCustomersSet extends AbstractEntitySet { /* … */ }

The picker app is not edited. This is the value-help arm of LUX Weave — the same move as contributing a tile to another module's dashboard.

Your set must project the picker's shape: every declared property and every key it names has to exist in your set. That is not a runtime surprise — ui5:sync aborts when it isn't true:

The sync stops whenMessage
the scope names a value help that doesn't existScope contribution targets unknown value help […]
the picker hosts scopes but declares no shape… hosts contributed scopes but declares no shape
your entity set doesn't exist in the app you named… points at entity set […] that does not exist in app […]
your set is missing a declared property or keythe missing names, keys prefixed key:

Two contributions cannot claim the same (picker, scope) pair, and one set contributes to one picker — the attribute is not repeatable.

Three gates, one picker ​

LayerGateWhereCharacter
Openthe picker's #[Access]when the open intent is dispatchedfail-fast UX; optional
Readthe resolved scope set's #[Read]at the OData boundarythe load-bearing one
Rowsthe set's own scoping (#[ScopedTo…])inside the querywhich rows

The picker's #[Access] is a courtesy — it keeps a door from opening that would show nothing. The gate that matters is the #[Read] on the set behind the scope, and a set without one is readable by every signed-in partner (The Read Gate). Gate the sets, not just the picker.

What it doesn't do ​

  • No nested value helps. A picker cannot open another picker.
  • No create-from-picker. Not found — create it is not in the contract; compose it from a dialog and an action if you need it.
  • No capability flags. A picker doesn't advertise whether it supports multi-select. Opening a single-only picker with mode: 'multi' is a bug in the caller, not a negotiated failure.
  • One scope per set. A set that should serve two pickers has to wait for the attribute to become repeatable.

See also ​