Objects as Settings


  • Community Rep

    This seems like it should already be a feature. Am I maybe missing it somewhere?

    I'd like to do like:

    <div data-type="object" data-key="objname" data-attributes="{{data-type:text,data-key:text},{data-type:number,data-key:number}}"></div>
    

    have it generate a form with a text input and a number input and get a settings object like:

    {
     objname: {
      text: "Some inputed text",
      number: 9001
     }
    }
    

    I'm currently prepending the input keys and reading, parsing, and storing the object manually into settings, but the above syntax would be so much nicer.


  • GNU/Linux Admin


  • Plugin & Theme Dev

    This though (courtesy of @frissdiegurke)


  • Community Rep

    @julian @Schamper Thanks! Those docs did not have an example of exactly what I wanted to do, BUT it did give me a clue, and using the helper functions I was able to achieve what I wanted. I basically have several subforms with subforms that I wanted to store as arrays of objects with arrays of objects within. The goal being to pass that object to the template renderer without doing any additional parsing.

    I got it working perfectly now, thanks so much! 👍


  • Plugin & Theme Dev

    @yariplus do you mind sharing it? Would love to take a look at your solution.


  • Community Rep

    @Schamper It's kinda sloppy right now, but I'll post it after I clean it up.


  • Community Rep

    @Schamper Okay, my original implementation was too complex, so I reimplemented it as a Settings plugin. It works... better than I expected it too. (It borrows heavily from the array plugin, I copied it to start, so there may be stuff inside that's totally unnecessary.)

    You can do

    <div data-type="object" data-key="user" data-split="<br>" data-properties='{"firstname":"","lastname":""}'></div>
    

    And get two text fields and a settings object that looks like:

    {
     user: {
      firstname: "First Name",
      lastname: "Last Name"
     }
    }
    

    Not to special, but the magic starts if you use it inside an array:

    <div data-key="users" data-attributes='{"data-type":"object","data-attributes":{"firstname":{"data-type":"textarea"},"lastname":""}}'></div>
    

    Then you get an array of two text fields and a settings object that looks like:

    {
     users: [
      {
       firstname: "First Name",
       lastname: "Last Name"
      },
      {
       firstname: "First Name 2",
       lastname: "Last Name 2"
      }
     ]
    }
    

    Which you can then use directly as the data parameter in a template as such:

    Users:<br>
    <!-- BEGIN users -->
    {users.lastname}, {users.firstname}<br>
    <!-- END users -->
    

    It accepts all the same syntax as the other plugins, so it's completely possible to do crazy layered stuff like:

    <div data-key="users" data-attributes='{"data-type":"object","data-attributes":{"nicknames":{"data-type":"array"},"realname":"","characters":{"data-type":"object","data-attributes":{"name":"","level":{"data-type":"number"}}}}}' data-new='{"nicknames":["Poofie","Yarikins","Yari"],"realname":"Tim","character":{"name":"yariplus","level":9001}}'></div>
    

    and get:

    {
     users: [
      {
       "nicknames":["Poofie","Yarikins","Yari"],
       "realname":"Tim",
       "character":{"name":"yariplus","level":9001}
      }
     ]
    }
    

    Here's the whole plugin:

    define('settings/object', function () {
    
    	var Settings = null,
    		SettingsObject,
    		helper = null;
    
    	/**
    	 Creates a new property child-element of the object with given data and calls given callback with elements to add.
    	 @param field Any wrapper that contains all properties of the object.
    	 @param key The key of the object.
    	 @param attributes The attributes to call {@link Settings.helper.createElementOfType} with or to add as
    	 element-attributes.
    	 @param prop The property name.
    	 @param value The value to call {@link Settings.helper.fillField} with.
    	 @param separator The separator to use.
    	 @param insertCb The callback to insert the elements.
    	 */
    	function addObjectPropertyElement(field, key, attributes, prop, value, separator, insertCb) {
    		attributes = helper.deepClone(attributes);
    		var type = attributes['data-type'] || attributes.type || 'text',
    			element = $(helper.createElementOfType(type, attributes.tagName, attributes));
    		element.attr('data-parent', '_' + key);
            element.attr('data-prop', prop);
    		delete attributes['data-type'];
    		delete attributes['tagName'];
    		for (var name in attributes) {
    			var val = attributes[name];
    			if (name.search('data-') === 0) {
    				element.data(name.substring(5), val);
    			} else if (name.search('prop-') === 0) {
    				element.prop(name.substring(5), val);
    			} else {
    				element.attr(name, val);
    			}
    		}
    		helper.fillField(element, value);
    		if ($('[data-parent="_' + key + '"]', field).length) {
    			insertCb(separator);
    		}
    		insertCb(element);
    	}
    
    	SettingsObject = {
    		types: ['object'],
    		use: function () {
    			helper = (Settings = this).helper;
    		},
    		create: function (ignored, tagName) {
    			return helper.createElement(tagName || 'div');
    		},
    		set: function (element, value) {
    			var properties = element.data('attributes') || element.data('properties'),
                    attributes = {},
    				key = element.data('key') || element.data('object') || element.data('parent'),
                    prop,
    				separator = element.data('split') || ', ';
    			separator = (function () {
    				try {
    					return $(separator);
    				} catch (_error) {
    					return $(document.createTextNode(separator));
    				}
    			})();
    			element.empty();
    			if (typeof value !== 'object') {
    				value = {};
    			}
                if (typeof properties === 'object') {
                    for (prop in properties) {
                        attributes = properties[prop];
                        if (typeof attributes !== 'object') {
    						attributes = {};
    					}
                        addObjectPropertyElement(element, key, attributes, prop, value[prop], separator.clone(), function (el) {
                            element.append(el);
                        });
                    }
                }
    		},
    		get: function (element, trim, empty) {
    			var key = element.data('key') || element.data('object') || element.data('parent'),
    				properties = $('[data-parent="_' + key + '"]', element),
    				value = {};
    			properties.each(function (i, property) {
    				property = $(property);
    				var val = helper.readValue(property),
                        prop = property.data('prop'),
    					empty = helper.isTrue(property.data('empty'));
    				if (empty || val !== void 0 && (val == null || val.length !== 0)) {
    					return value[prop] = val;
    				}
    			});
    			if (empty || values.length) {
    				return value;
    			} else {
    				return void 0;
    			}
    		}
    	};
    
    	return SettingsObject;
    });
    

  • Plugin & Theme Dev

    @yariplus I'm glad you managed it so well 🙂
    I've just discovered a glitch: within your SettingsObject.get you still use the if (empty || values.length) { of array-plugin. Instead it has to be sth. like if (empty || Object.keys(value).length) { since values got renamed to value and it's not an array anymore so the emptiness-check has to be different.

    If that's fixed I don't see any reason not pushing this into core.
    If you're willing to publish it under the NodeBB licensing terms, you could create a PR with your plugin added and included within the DEFAULT_PLUGINS array.

    Thanks for creating (+ sharing) 👍

    PS: The reason for not being implemented yet is that I only thought of adding object-attributes with their own data-key="user.firstname" etc.
    But of cause it's worth adding such a settings-plugin as you built 😉 to simplify this into JSON-notation.


  • Community Rep

    @frissdiegurke Thanks! I agree to the terms. (I signed the CLAHub a way back) I'm glad you think it's worthy of core! 😄

    I fixed the empty check and fixed some spacing. And I removed checks for data-object. (I used that originally as the properties object parent, but switched to using data-parent cause it worked flawlessly inside an array.)


  • Plugin & Theme Dev

    Maybe I should add another side-note: javascript-objects have no predefined order of attributes when iterating.
    As a result not all browsers will show such fields in the same order, even so most major browsers will do.
    So the username and password fields in your example may or may not get displayed in reverse order dependent on what browser gets used.

    So make sure you use at least placeholders that identify the different fields 😉


  • Plugin & Theme Dev

    @yariplus nice! I think in all the plugins I've currently needed something like this I just ended up stringifying some JS Object, allows for custom built controls though 😉


  • Community Rep

    @frissdiegurke Ahh! Yeah, that would definitely not be good.

    I'm not sure the best way to go about this though. Using a data-order array seems the best way, the user would enter an array of keys in the order they want, and we would populate/generate the fields in that order. But, I would like this to be optional, and default to listing the properties in the order they are entered in the attributes. Though, it seems impossible to retrieve just the first set of property names because of the infinite level of complexity that could be inside the object. Mayve someone could help with a good regex?

    or I could separate the attributes and keys completely like:

    <div data-key="users" data-attributes='{"data-type":"object","data-attributes":[{},{}],"data-keys":["firstname","lastname"]}'>
    

    but that eliminates the nice JSON-like syntax.

    Yeah, I think a good regex to pull the property names would be best. A literal data-order should still be an option too, so this would be valid:

    <div data-key="users" data-attributes='{"data-type":"object","data-attributes":{"firstname":{},"lastname":{}},"data-order":["firstname","lastname"]}'>
    

  • Community Rep

    Ugh, as always I over-thought the problem. I already created an attribute data-prop to store the property name, so the user can just enter the properties' attributes as an array and declare the data-prop:

    <div data-key="users" data-attributes='{"data-type":"object","data-properties":[{"data-prop":"firstname"},{"data-prop":"lastname"}]}'></div>
    

    Unnamed data-props could default to the index in the array.

    I think this syntax makes more sense than entering the properties in JSON object format and guessing the order.


  • Community Rep

    @frissdiegurke Could I get your opinions on this? 😄

    https://github.com/NodeBB/NodeBB/pull/3071

    I made some improvements including changing the attributes declaration to an array so that they always appear in their declared order.

    I also added data-prepend and data-append for styling attributes and data-new for individual properties.
    The property name is declared using data-prop, or the default is the property's index.

    Example settings template:

    <div class="form-group panel">
        <label class="h3 col-sm-12">Player</label>
        <div data-key="player" data-type="object" data-split='<br>' data-properties='[{"data-prop":"name","data-prepend":"<label class=\"col-sm-6 col-md-2 col-lg-2\">Name </label>"},{"data-prop":"hp","data-type":"number","data-prepend":"<label class=\"col-sm-6 col-md-2 col-lg-2\">Hit Points </label>"},{"data-prop":"mp","data-type":"number","data-prepend":"<label class=\"col-sm-6 col-md-2 col-lg-2\">Magic Points </label>"},{"data-prop":"spells","data-prepend":"<label class=\"col-sm-6 col-md-2 col-lg-2\">Spells</label>","data-type":"array"}]'></div>
    </div>
    <div class="form-group panel">
        <label class="h3 col-sm-12">Monsters</label>
        <div data-key="monsters" data-attributes='{"data-type":"object","data-split":"<br>","data-properties":[{"data-prop":"name","data-new":"Mr. Scary Monster","data-prepend":"<label class=\"col-sm-6 col-md-2 col-lg-2\">Name </label>"},{"data-prop":"hp","data-type":"number","data-new":"100","data-prepend":"<label class=\"col-sm-6 col-md-2 col-lg-2\">Hit Points </label>"},{"data-prop":"mp","data-type":"number","data-new":"50","data-prepend":"<label class=\"col-sm-6 col-md-2 col-lg-2\">Magic Points </label>"},{"data-prop":"spells","data-prepend":"<label class=\"col-sm-6 col-md-2 col-lg-2\">Spells</label>","data-type":"array"}]}' data-split='<hr>'></div>
    </div>
    

    Generated settings page:

    Saved settings object:

    {
      "monsters":[
        {
          "name":"Mr. Scary Monster",
          "hp":100,
          "mp":50,
          "spells":[
            "Ice",
            "Lightning"
          ]
        },
        {
          "name":"Big Boss",
          "hp":9001,
          "mp":300,
          "spells":[
            "Fire",
            "Firaga",
            "Firagalagalala"
          ]
        }
      ],
      "player":{
        "name":"The Hero",
        "hp":500,
        "mp":30,
        "spells":[
          "Heal",
          "Magic Missle"
        ]
      }
    }
    
    yariplus created this issue in NodeBB/NodeBB

    closed Enhance settings object plugin #3071


  • Plugin & Theme Dev

    seems great 🙂
    I'm not going to add data-append and data-prepend to array-plugin since I'm working on deep html-layout so you don't have to use data-attributes anymore at all.
    I guess the data-attributes could get marked as deprecated once I'm done 😉 .
    At the same time this should allow you to define arrays with (periodically repeating) different element-schema.

    Since those changes require some core-changes of the settings module (I introduce sth. called 'scope'), afterwards applying an equivalent html-structure to your object-plugin shouldn't be hard.
    Shall I do this on the fly or do you like the object-plugin to be your 👶?


  • Community Rep

    Go for it! 😄 It's definitely klunky defining an array of data-attributes like I have... a way to define a schema from the html would definitely be nice.


Log in to reply
 

Suggested Topics

| |