[SalesForce] Passing a map to a custom component

I need to pass a set of key/value pairs to a custom component.

The specification of the key/values needs to be done in an apex:page definition, so something like this (IMPORTANT: The following code does NOT create a map – it creates a string that we would then need to parse. If the Account.val1 value happened to contain a double quote, or some other separator character expected by the parser, it will break):

<apex:page standardController="Account">

    <c:MyComponent keyvalues="'a'=>'b'" keyvalues="c=>{!Account.val1}; d=>{!Account.val2}; f=>{!Account.val3}" />

</apex:page>

I know that it is possible to set up the component to accept a map on the keyvalues attribute:

<apex:component controller="WDSearchComponentController">
    <apex:attribute name="keyvalues" description="Translation info" type="Map" required="true"/>
</apex:component>

but how do I actually create the map from the VF page?

As I said in the above, I could technically parse the string to construct my map, but the values could really be anything (including delimiter characters), so safely parsing would be impossible.

Is there some way in the to make calls against the component controller? Or some way to create a Map inside the itself?

Note that I'm creating a component that could be used in many different environments, and we really want the user to be able to just place it into any element they wish and have it work without them having to create a helper class or anything like that.

If there were a way to explicitly call methods on the component controller from the element, that would work, but I see no way to do that. If it were possible to add sub-elements to the tag, that would also work – but I see no way to do that.

Best Answer

Updated 17/04/2018:

How this answer has remained for so long whilst being incorrect I have no idea, but the correct answer should be:

Use type="map" not type="Map".


Old Answer:

As far as I'm aware there is no way to create a map directly in Visualforce, you will have to get a Controller/Extension involved at some point.

In terms of using a Controller or an Extension, passing Maps to Components is a tricky subject... in that I've never gotten it to work personally (even though according to the documentation it should be simple), nor have I ever seen a working example.

The documentation states:

The type attribute defines the Apex data type of the attribute. Only the following data types are allowed as values for the type attribute:

  • Maps, specified using type="map". You don’t need to specify the map’s specific data type.

Based on that, the simplest example of this is demonstrated below.

BasicMapComponent

<apex:component >
    <apex:attribute name="map" description="aMap" type="Map" />
</apex:component>

BasicMapPage

<apex:page controller="BasicMapPageController">
    <c:basicmapcomponent map="{!aMap}"></c:basicmapcomponent>
</apex:page>

BasicMapPageController

public class BasicMapPageController
{
    public Map<String, String> aMap {get;set;}
}

However, this doesn't work. If you try and use this sample you'll get the following error:

Error: Wrong type for attribute <c:basicmapcomponent map="{!aMap}">. Expected Map, found MAP<String,String>

Apparently this is a long-standing bug with the platform (all of the references to it are from 2011) and based on it's age is unlikely to be fixed any time soon.

So far, there are only two viable methods I've come across to get the behaviour you're after. In terms of picking between them it comes down to if you want your clients to use clicks vs. code to configure your component.


Solution 1 (Clicks)

If you want to keep the configuration of your component completely click based and not require your clients to write any Apex then you'll have to create a custom object/setting to emulated a map, which you can then query in your components controller.

Something like the following could potentially work, although it limits you to only one set of parameters for all instances of your component in an org (you could make the key a compound between the page name and the actual key value, alternatively don't enforce unique values and trust your clients - probably not the best plan):

MapCustomObject__c

  • Key__c (String, Unique values only)
  • Value__c (String)

MapComponentController

public class MapComponentController
{
    private Map<String, String> aMap;

    public MapComponentController()
    {
        List<MapCustomObject__c> mapObjects = 
        [
            SELECT 
                Key__c, Value__c 
            FROM 
                MapCustomObject__c
        ];

        // Populate the aMap variable here
    }
}

Solution 2 (Code)

If you don't mind clients of your component needing to write a little Apex then the easiest method is to add a wrapper class which your clients need to initialize and pass into your component.

MapWrapper

public class MapWrapper
{
    private Map<Object, Object> theMap;

    public MapWrapper()
    {
        theMap = new Map<Object, Object>();
    }

    // Expose Map methods from your wrapper here, I'll do get() and values() as an example
    public Object get(Object key)
    {
        return theMap.get(key);
    }

    public List<Object> values()
    {
        return theMap.values();
    }

    // Etc, etc...
}

You can then update your component to accept an attribute of type MapWrapper instead of Map and you should be good to go.

MapComponent (note the type attribute has changed)

<apex:component >
    <apex:attribute name="mapWrapper" description="aMapWrapper" type="MapWrapper" />
</apex:component>

MapPage

<apex:page controller="MapPageController">
    <c:mapcomponent mapWrapper="{!aMapWrapper}"></c:basicmapcomponent>
</apex:page>

MapPageController

public class MapPageController
{
    public MapWrapper aMapWrapper {get;set;}

    // Your clients will have to handle building the MapWrapper
}

Obviously you can give your MapWrapper a nicer name, probably something along the lines of MyComponentNameOptions or something similar to make it less obvious to your clients what is going on (i.e. that you are using a work-around).

Related Topic