Map/Dictionary fields?

I have a hugo website that uses a map in it’s front-matter. A map like a bunch of key-value pairs, not like a world map.

So my frontmatter looks like:

---
title: My Title
dictionary:
    key1: some description
    anotherKey: unique to this file
    babe: A great movie
---

Is there a way of defining such a structure using Netlify? They can be unique to the file, but also shared with other files.

If you’re looking to recreate this dataset with Netlify CMS: try the object widget!

Here’s an example collection (posts) with a title and an object:

collections:
  - name: "post"
    label: "Post"
    folder: "site/content/post"
    slug: '{{title}}'
    fields:
      - {name: title, label: title, widget: string}
      - name: dictionary
        label: Dictionary
        widget: object
        fields: 
          - {name: key1, label: Key 1, widget: string}
          - {...}

Ah yes, I confuse netlify.com and Netlify CMS.

I saw Object, but that only has fixed fields right? I can’t have one file with key1: value and a different file with different key: value? And on top of that, I don’t know the keys beforehand. Any arbitrary key should be allowed.

However, I’ll look at the code of object to see if I can make my own version. Thanks!

Ah yes, you’re right: the fields of an object widget are fixed, there’s no support for variable object keys yet. You might want to look into the variable types list widget. The data structure is somewhat different, but it might be more suitable for your use case: https://www.netlifycms.org/docs/beta-features/#list-widget-variable-types

Thanks! At first glance it looks like I’ll need to make a custom widget. That’s ok, more to learn :smiley:

2 Likes

So I’ve been trying to make a new widget, but I’m running in some problems. I’m mostly basing this on https://www.netlifycms.org/docs/custom-widgets/

So I’m using a Map as the value, but when I add a new element to the map, and then call the onChange(value) callback, nothing seems to happen. However, if I change it to onChange(new Map(value)) it does update. It seems that the onChange callback requires a new object?

Also, the value doesn’t seem to be actually saved. When I fill in other widgets and refresh the page, it then asks to restore the previous values. However it doesn’t restore the map, while restores the other values just fine.

And lastly, I get uncaught exception: Object like a second after I change anything to the map. My guess is that Netlify CMS is trying to save the map (debouncing it for a second so it doesn’t save every letter I type), but fails and throws that exception. That would explain the previous problem (the non-saving one).

My complete code currently is:

    var IngredientsControl = createClass({
        getDefaultProps: function () {
            return {
                value: new Map()
            };
        },

        addElement: function (e) {
            var value = this.props.value;
            value.set("id", "Description");

            //is.props.onChange(value);
            this.props.onChange(new Map(value));
        },

        handleIdChange: function (oldId, newId) {
            console.log(oldId, newId);
            var value = this.props.value;
            var description = value.get(oldId);
            value.delete(oldId);
            value.set(newId, description);

            //this.props.onChange(value);
            this.props.onChange(new Map(value));
        },

        handleDescriptionChange: function (id, description) {
            console.log(id, description);
            var value = this.props.value;
            value.set(id.toLowerCase(), description);

            //this.props.onChange(value);
            this.props.onChange(new Map(value));
        },

        render: function () {
            var value = this.props.value;

            var handleIdChange = this.handleIdChange;
            var handleDescriptionChange = this.handleDescriptionChange;

            var items = [];
            for (var [id, description] of value) {
                var li = h('li', {},
                    h('input', { type: 'text', value: id, onChange: function (e) { handleIdChange(id, e.target.value); } }),
                    h('input', { type: 'text', value: description, onChange: function (e) { handleDescriptionChange(id, e.target.value); } })
                );
                items.push(li);
            }

            return h('div', { className: this.props.classNameWrapper },
                h('input', {
                    type: 'button',
                    value: "Add element",
                    onClick: this.addElement
                }),
                h('ul', {}, items)
            )
        }
    });

    var IngredientsPreview = createClass({
        render: function () {
            var value = this.props.value;
            var items = [];
            for (var [id, description] of value) {
                var li = h('li', {},
                    h('span', {}, id),
                    h('span', {}, ": "),
                    h('span', {}, description)
                );
                items.push(li);
            }

            return h('ul', {}, items);
        }
    });

    CMS.registerWidget('ingredients', IngredientsControl, IngredientsPreview);

What am I doing wrong?

Thanks!

There’s an undocumented onChangeObject prop that gets passed into widgets with map values, try using that instead.

On the backend it seems to change the Map into an object. Very weird.

Initially when I add a new value it seems fine:

Add:
Map { id → "Description" }

(copied from Firefox console)

Though once it enters the render function, the props.value has changed and is now and object, with at it’s root an array of tuples, with the first element of the first tuple being my map:

Render:
{…}
    __altered: false
    __hash: undefined
    __ownerID: undefined
    _root: {…}
        entries: (1) […]
            0: Array [ Map(1), undefined ]
            length: 1
            <prototype>: Array []
        ownerID: undefined
        <prototype>: Object { get: get(), update: update(), iterate: iterate(), … }
    size: 1
    <prototype>: Object { constructor: Fe(), toString: toString(), get: get(), … } 

Adding a second element makes it go all wonky, which makes sense I guess since I’m assuming it’s a map but it isn’t any more. Hard to explain, but here’s a screenshot:

image

Again, here is the new code, so you can see exactly where I log “Add” and “Render”:

    var IngredientsControl = createClass({
        getDefaultProps: function () {
            return {
                value: new Map()
            };
        },

        addElement: function (e) {
            var value = this.props.value;
            value.set("id", "Description");

            console.log("Add: ");
            console.log(value);

            this.props.onChangeObject(value);
            //this.props.onChange(new Map(value));
        },

        handleIdChange: function (oldId, newId) {
            console.log(oldId, newId);
            var value = this.props.value;
            var description = value.get(oldId);
            value.delete(oldId);
            value.set(newId, description);

            this.props.onChangeObject(value);
            //this.props.onChange(new Map(value));
        },

        handleDescriptionChange: function (id, description) {
            console.log(id, description);
            var value = this.props.value;
            value.set(id.toLowerCase(), description);

            this.props.onChangeObject(value);
            //this.props.onChange(new Map(value));
        },

        render: function () {
            var value = this.props.value;

            console.log("Render: ");
            console.log(value);
            //console.log(this.props);

            var handleIdChange = this.handleIdChange;
            var handleDescriptionChange = this.handleDescriptionChange;

            var items = [];
            for (var [id, description] of value) {
                var li = h('li', {},
                    h('input', { type: 'text', value: id, onChange: function (e) { handleIdChange(id, e.target.value); } }),
                    h('input', { type: 'text', value: description, onChange: function (e) { handleDescriptionChange(id, e.target.value); } })
                );
                items.push(li);
            }

            return h('div', { className: this.props.classNameWrapper },
                h('input', {
                    type: 'button',
                    value: "Add element",
                    onClick: this.addElement
                }),
                h('ul', {}, items)
            )
        }
    });

    var IngredientsPreview = createClass({
        render: function () {
            var value = this.props.value;
            console.log("Preview: ");
            console.log(value);
            var items = [];
            for (var [id, description] of value) {
                var li = h('li', {},
                    h('span', {}, id),
                    h('span', {}, ": "),
                    h('span', {}, description)
                );
                items.push(li);
            }

            return h('ul', {}, items);
        }
    });

    CMS.registerWidget('ingredients', IngredientsControl/*, IngredientsPreview*/);

Looked into it a bit more - you do not need to use onChangeObject for a case where your widget is reading and writing a map, only if you’re allowing nested widgets to control the individual values within the map like the object widget does.

Your onChange calls should look something like this:

// Map function is imported from immutable
this.props.onChange(this.props.value || Map()).set(key, value)

Using the Immutable.Map seems to work indeed! Thanks a lot :smiley: