Objects as Settings
-
@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; });
-
@yariplus I'm glad you managed it so well
I've just discovered a glitch: within yourSettingsObject.get
you still use theif (empty || values.length) {
of array-plugin. Instead it has to be sth. likeif (empty || Object.keys(value).length) {
sincevalues
got renamed tovalue
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 builtto simplify this into JSON-notation.
-
@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.)
-
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
-
@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"]}'>
-
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.
-
@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" ] } }
-
seems great
I'm not going to adddata-append
anddata-prepend
to array-plugin since I'm working on deep html-layout so you don't have to usedata-attributes
anymore at all.
I guess thedata-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?
-
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.