15

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.

  • What do you mean by drop any element, can you drop a String in the attribute of type Map? – Ashwani Apr 12 '14 at 6:14
  • I mean that multiple customers may wish to include the component in their page layouts, passing in a large variety of different key/value pairs in the map. I will update the problem description to make it clear that the code example where I'm trying to specify the map does not work - I need to figure out some way of making it work. If I pass in a string, I then have to parse it. This could get ugly, for example, if the keys or values contain any of the separator characters that the parser is expecting. – Kevin Day Apr 13 '14 at 20:41
12

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

  • Excellent response - I expect the component to be used in 5 to 10 places, with different configuration on each. And the config is coming from the page details - so option 1 is out. If I opt for option 2, the only way for the user to add values to the map is to write a custom page controller (apex code). This seems to be the crux of my issue: I don't see any way to actually configure the controller from within the apex:page. This is incredibly limiting when it comes to re-use. It looks like I'm going to just have to come up with my own syntax and parse. Bummer. – Kevin Day Apr 15 '14 at 0:35
  • @KevinDay Sorry I couldn't give you the answer you wanted, glad the answer helped though. This is an issue I've had to wrestle with as well. I'll update my post to include parsing your own data as an option (even though you mentioned it in your question) so that its there as an option for future StackExchange users and I'll try to remember to update my answer if the situation around passing maps or constructing them in Visualforce changes in the future. – Alex Tennant Apr 15 '14 at 7:39
  • hmmm - I wonder if there is a way to use <apex:param> to achieve this... I'll give it a try and post back later today. – Kevin Day Apr 15 '14 at 13:53
  • And the answer is 'nope'. There is no way to pass parameters from a VF page to a component controller, except using the assignTo attribute on the apex:attribute tag. Very disappointing. So it looks like parsing a string is the only way to go. – Kevin Day Apr 16 '14 at 1:09
  • 1
    As the documentation states: type="map" with a lowercase m, works as described without the need for the wrapper class or casting. – Mark Pond Apr 17 '18 at 18:56
2

I would prefer better to user the generic 'Object' type and in the controller type cast it accordingly.

  • 1
    This was the simplest solution to the Expected Map, found Map<String, String> bug. Rather than creating wrapper class I just changed type attribute to type='Object' and voila! In my scenario, I was not binding the component attribute to controller property, just using it directly in the markup. – Doug Ayers Feb 9 '17 at 15:10
2

I got same error when saving the VF page that uses the component. Then I modified the apex:attribute parameter type as type="map" from type="Map" and it worked.

1

The answer by Alex Tennant states that this won't work (and it's true):

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

This throws the error Error: Wrong type for attribute <c:basicmapcomponent map="{!aMap}">. Expected Map, found MAP<String,String>. But as kshitij says, just changing type="Map" to type="map" in the BasicMapComponent just works without issues, like this:

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.