-
-
Notifications
You must be signed in to change notification settings - Fork 111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Write a object oriented UI library #968
Comments
+1, sure, please start with |
I'm in favour of this massively. Maybe out could all justify the name Textpattern 5!? If we coincided it with unlimited custom fields of various types too that seems enough to warrant a new numbered release. |
Would you be considering adopting a PHP framework as part of this? |
I would rather investigate on the side of template languages (like mustache), to be able to process forms both server and client side. |
Sure. As long as the basic structure of HTML isn't a million miles away from what we currently have and is easy to style, then it gets my vote. |
Processing client and server forms did occur to me, but I never got my head round mustache fully. Seemed useful though. I've used Twig, fwiw but found the control structures lacking. What I'm proposing here - at least initially - is just a way to maintain our notion of "checkbox", "radio set", "select input", "textbox" and so forth, along with their granular counterpart "tag", and their combined versions such as "file input", "checkbox set", "upload form", etc and just wrap them in a better, extensible manner. Like tag lego. We already have most of the components in place, it's just that they're: a) functions So I was planning on simply writing a few quick objects to replicate the existing functionality in a more OO style, and make it easier to customize (and shorter to invoke than counting which positional argument to use when you need to specify an Longer term, the objects could be invoked from a tag (e.g. Things this is NOT:
Yes, I know, Not-Invented-Here. I've tried countless widget builders over the years and every one has either left me cold, been too limited, too big, or too complex trying to do everything. This isn't one of those systems. I've made a start in a separate local branch, but my OO is flaky so it'll take a few iterations to figure out what extends what and interfaces what else. If the scope needs to expand to do more, and it makes sense to do so, let's hear proposals. |
That will be already a huge improvement, and if, one day, we are able to compose a whole admin pane from txp tags, my 10-years dream would come true. Templating is probably off-topic here, but let me explain what I mean by example. Suppose we want to allow theme authors to construct their own upload image previews, with few extra fields (name, resize, thumbnails dimensions). Some of these fields (resize) could be pre-filled on the server side, from user preferences or last used values. Other fields (name) are only available to js. So we need a template language (even our own) that can be parsed both by php and js, and easy to use for theme authors. Something in the vein of
|
Yours and mine too :-) Re: templating, we should definitely build in some kind of support for this. I don't know how yet. At the moment I have the following Objects:
Thoughts and ideas welcome. |
Story so far, via examples: TextboxesRender a bog standard input box with the given name and value. Note you can just $tag = Txp::get('\Textpattern\Widget\Textbox', 'fish', 'cod');
print $tag;
Renders:
<input type="text" name="fish" value="cod" /> You can supply attributes via the $tag = Txp::get('\Textpattern\Widget\Textbox', 'valueless')
->setAtts(array('data-whatever' => 'my-custom-attribute'))
->setAtts(array('i-am-boolen' => true))
->setBool(array(
'required',
'some-future-boolean-option'
))
->setBool('autocomplete');
print $tag;
Renders:
<input type="text" name="valueless" data-whatever="my-custom-attribute" value="" i-am-boolen required some-future-boolean-option autocomplete /> DIY tagsIf you want to delve off-piste, you can go to the lowest level with the $label = Txp::get('\Textpattern\Widget\Tag', 'label')
->setAtts(array('for' => 'terms'))
->setContent('I agree to the terms');
$tag = Txp::get('\Textpattern\Widget\Tag', 'input')
->setAtts(array(
'type' => 'checkbox',
'class' => 't-and-c',
'id' => 'terms',
'value' => 1));
echo $tag . $label;
Renders:
<input type="checkbox" class="t-and-c" id="terms" value="1" />
<label for="terms">I agree to the terms</label> Separate start / endCustom content? No problem. Here's a form with two fields inside it. Note that during rendering - using the Also, the $mail = Txp::get('\Textpattern\Widget\Textbox', 'email')
->setAtts(array('type' => 'email'));
$area = Txp::get('\Textpattern\Widget\Tag', 'textarea')
->setAtts(array('placeholder' => "Tell me something I don't know"));
$form = Txp::get('\Textpattern\Widget\Tag', 'form')
->setAtts(array('name' => 'sendy', 'id' => 'my-form'))
->setAtts(
array('action' => ''),
array('flag' => TEXTPATTERN_STRIP_NONE)
);
echo
$form->render('start'),
$mail->render(),
$area->render('full'),
$form->render('end');
Renders:
<form name="sendy" id="my-form" action="">
<input type="email" name="email" value="" />
<textarea placeholder="Tell me something I don't know"></textarea>
</form> Sky's the limit. Next up: a collection of objects in the same vein as Textbox that act as convenience wrappers around the Questions or comments / improvements on implementation welcome. If anyone wants to join the party, see 0fda690 for 'widgets' branch and current code. I don't have world class OO skillz, so anyone who can do this better in a more extensible or adaptable manner, please dive in. |
5c06fea introduces the
And when you print/render it, they're just concatenated in that order. Below is an example of it in use. It's not much different to its functional counterpart. Note that there is no (direct) implementation of setting the Note also that the 'no' value is checked in this example by setting the third parameter $radset = array(
'yes' => gTxt('yes'),
'no' => gTxt('no'),
'maybe' => gTxt('maybe'),
);
$radioset = Txp::get('\Textpattern\Widget\RadioSet', 'decisionRadio', $radset, 'no');
echo $radioset;
Renders:
<input type="radio" name="decisionRadio" id="decisionRadio-yes" class="radio" value="yes" />
<label for="decisionRadio-yes">Yes</label>
<input checked type="radio" name="decisionRadio" id="decisionRadio-no" class="radio active" value="no" />
<label for="decisionRadio-no">No</label>
<input type="radio" name="decisionRadio" id="decisionRadio-maybe" class="radio" value="maybe" />
<label for="decisionRadio-maybe">maybe</label> The convention is (as before) that if you set a value as default (checked), it gets the class Or, after building the stock collection, you can use the The TagCollection concept should be extensible to select lists and checkbox sets, as well as widget combos. I'm hoping this means we can introduce a sensible InputLabel as a convenience method, which will combine the given input widget with a label, and be able to raise a callback as we do now. I've also introduced a dedicated The main advantage over the old way of doing things is, because all tags are based on the Please take it for a spin and suggest improvements or add to the library if you wish. |
Just a minor thing: can any widgets that are generated have their attributes in the following order at the start of a HTML tag:
That makes it quicker for me to theme things, as the hooks I need are at the front of the tag so I can view them quickly. Basically, |
Now that I don't know about quite yet! I did spot yesterday that it seems to be putting them in an undesirable order. It seems to depend on how the object itself is created and which attributes are added first. It seems that booleans always go last. But if I From what I can tell so far:
It's on my list of things to investigate, unless someone else wants to step in and hazard a guess why it's doing this. |
Attribute order should be fixed in be6559e. At least, for core-defined helper widgets as far as we can. For custom built ones, it'll render the attributes in the order given. EDIT: If the core doesn't define any |
I like the way this is going!! |
810cf75 adds select list support. Examples: $optset = array(
'banana' => 'Banana',
'strawberry' => 'Strawberry',
'vanilla' => 'Vanilla',
'chocolate' => 'Chocolate',
);
$select = Txp::get('\Textpattern\Widget\Select', 'flavour_love', $optset, 'banana, chocolate');
echo $select->render();
Renders:
<select name="flavour_love[]" multiple>
<option value="banana" selected dir="auto">Banana</option>
<option value="strawberry" dir="auto">Strawberry</option>
<option value="vanilla" dir="auto">Vanilla</option>
<option value="chocolate" selected dir="auto">Chocolate</option>
</select> Note that because you supplied more than one default value, the select was automatically converted to a $select2 = Txp::get('\Textpattern\Widget\Select', 'flavour_love')
->addOption('banana', 'Banana')
->addOption('pineapple', 'Pineapple')
->addOption('strawberry', 'Strawberry', true)
->addOption('chocolate', 'Chocolate')
->setBool('required')
->setAtts(array('class' => 'selectClass', 'data-whatnot' => 'some value'))
->blankFirst();
echo $select2->render();
Renders:
<select name="flavour_love" class="selectClass" data-whatnot="some value" required>
<option value=""> </option>
<option value="banana" dir="auto">Banana</option>
<option value="pineapple" dir="auto">Pineapple</option>
<option value="strawberry" selected dir="auto">Strawberry</option>
<option value="chocolate" dir="auto">Chocolate</option>
</select> |
I should mention 3c03989 here, which introduces something we don't currently have in our arsenal: OptGroups. Here's an example if you want to directly build a grouped select list: $mcu = Txp::get('\Textpattern\Widget\Select', 'marvel_characters')
->addOptGroup('Heroes')
->addOption('spiderman', 'Spiderman')
->addOption('iron-man', 'Iron Man', true)
->addOption('thor', 'Thor')
->addOption('professor-x', 'Professor X')
->addOptGroup('Villains')
->addOption('loki', 'Loki')
->addOption('magneto', 'Magneto')
->addOption('ultron', 'Ultron')
->addOption('doctor-doom', 'Doctor Doom')
->setBool('required')
->setAtts(array('class' => 'marvelClass', 'data-mcu' => 'Marvel Cinematic Universe'))
->addOption('black-widow', 'Black Widow', false, 'Heroes');
echo $mcu; Renders: <select name="marvel_characters" class="marvelClass" data-mcu="Marvel Cinematic Universe" required>
<optgroup label="Heroes" dir="auto">
<option value="spiderman" dir="auto">Spiderman</option>
<option value="iron-man" selected dir="auto">Iron Man</option>
<option value="thor" dir="auto">Thor</option>
<option value="professor-x" dir="auto">Professor X</option>
<option value="black-widow" dir="auto">Black Widow</option>
</optgroup>
<optgroup label="Villains" dir="auto">
<option value="loki" dir="auto">Loki</option>
<option value="magneto" dir="auto">Magneto</option>
<option value="ultron" dir="auto">Ultron</option>
<option value="doctor-doom" dir="auto">Doctor Doom</option>
</optgroup>
</select> Note we accidentally forgot Black Widow (the shame of it!), and added her afterwards by specifying the group to add her to. Under normal operation, when it encounters an Alternatively, you can build your groups separately and add them as content to a select. This gives you more control over each group as you can set attributes directly on each, disable groups, and so forth. Here we add a $mcu2 = Txp::get('\Textpattern\Widget\Select', 'marvel_characters');
$optGroup1 = Txp::get('\Textpattern\Widget\OptGroup', 'Heroes')
->addOption('spiderman', 'Spiderman')
->addOption('iron-man', 'Iron Man', true)
->addOption('thor', 'Thor')
->addOption('professor-x', 'Professor X')
->addOption('black-widow', 'Black Widow')
->setAtts(array('class' => 'marvelClass', 'data-mcu' => 'Marvel Heroes'));
$optGroup2 = Txp::get('\Textpattern\Widget\OptGroup', 'Villains')
->addOption('loki', 'Loki')
->addOption('magneto', 'Magneto')
->addOption('ultron', 'Ultron')
->addOption('doctor-doom', 'Doctor Doom')
->setAtts(array('class' => 'marvelClass', 'data-mcu' => 'Marvel Villains'));
$mcu2->setContent($optGroup1.$optGroup2);
echo $mcu2; Renders: <select name="marvel_characters">
<optgroup label="Heroes" class="marvelClass" data-mcu="Marvel Heroes">
<option value="spiderman" dir="auto">Spiderman</option>
<option value="iron-man" selected dir="auto">Iron Man</option>
<option value="thor" dir="auto">Thor</option>
<option value="professor-x" dir="auto">Professor X</option>
<option value="black-widow" dir="auto">Black Widow</option>
</optgroup>
<optgroup label="Villains" class="marvelClass" data-mcu="Marvel Villains">
<option value="loki" dir="auto">Loki</option>
<option value="magneto" dir="auto">Magneto</option>
<option value="ultron" dir="auto">Ultron</option>
<option value="doctor-doom" dir="auto">Doctor Doom</option>
</optgroup>
</select> I'm deliberating over creating a 'shortcut' method to add options without needing to specify the |
Minor thing, but |
Makes sense. Since everything's an 'input' I was just trying to be more specific. Let's review the names and come up with a table of what we think makes sense for the various input controls. |
Feel free to edit/change/add to the below. I'm thinking since there are many
|
Okay, that's a pretty good suggestion. See first post where I've just put the current implementation list of helper functions. If you want to add the proposals there we can see what makes sense all in one place. Essentially, every tag is constructed from either the |
Also, if there are any convenience helpers missing that would make people's lives easier when creating tags, feel free to add them to the list (tagged as 'unimplemented' or something) and we can look to add them. Basically, I was just going through |
OK, I've added a couple more, and suggested name changes to Textbox as discussed. Hope that is clear. |
Renamed Is everyone okay with calling this a 'Widget' library? I just chose the name as that's what I call them - input widgets - but if it's better in industry parlance to call them something else, then let's rename the class now. Maybe
Is that better? Anything else we could/should use instead? EDIT: This also impacts methods such as EDIT2: And, longer term, this may affect any public-side tag implementation:
|
The other concern I have with using switches is that common UX expectations for them these days is that they instantly save a pref when switched. Our prefs panel doesn't do that (you have to then 'save' the prefs). By using a switch we risk user confusion unless you start AJAX-saving the prefs - which may be undesirable. |
That is a valid point re: immediacy. If we go the Ajax save route then it gives nobody a way of backing out changes. And it'd have to work like that for all prefs - including the admin theme switcher, the site name/URL and comments on/off (which changes the list in the left upon Save, as does toggling Advanced prefs) and so forth. That also means no Save button, which potentially renders a bunch of plugins that utilise the prefs panel useless, as they won't necessarily be wired for Ajax saves (it requires a change of type for Type 3 plugins to Type 4, at minimum). |
In terms of code it does mean some other weirdness. Anything checkboxy cascades from the UI\Checkbox class. Anything Radioish cascades from the UI\Radio class. So if we add the If we move the enforced The simplest solution here is to simply add the styles to the admin themes' CSS for handling them, then add the class when we build the controls on an as-needed basis. So the UI library stays pure and we just manually |
OK, that works for me - so I can do the (probably optional) CSS for this, how would the following current HTML be changed by your recommendation (feel free to edit the code below)?:
|
One thing we don't have in core right now but could really do with are numeric inputs and range sliders. These bring some unique challenges to a UI library because they have some optional components:
The library itself can of course manage these if you build a UI\Input control by hand and pass in the relevant params: use \Textptattern\UI\Input;
$nb = new Input('rating');
$nb->setAtts(array(
'type' => 'number',
'min' => 1,
'max' => 10
));
echo $nb; That would output: <input name="rating" type="number" min="1" max="10"> But there are some interesting caveats here. Namely we can't use '0' as a value without overriding the default flag behaviour to 'strip empty values': $nb = new Input('howmuch');
$nb->setAtts(array(
'type' => 'number',
'min' => 0,
'max' => 1,
'step' => '0.1'
), array(
'flag' => TEXTPATTERN_STRIP_NONE,
));
echo $nb; The same thing applies to range sliders. Should we introduce a Number class that can handle these in a simpler fashion that would automatically apply the correct stripping flag? e.g. $nb = new Textpattern\UI\Number('donation_amount', 6, 0, 50, 2); with the last three values being min, max, step: these could be amalgamated into a single array parameter instead so the signature is: Number($name, $val, $constraints = array('min' => 0, 'max' => '', 'step' => 1)); I kinda like that, but maybe someone has a better idea? It might not be worth going as far as creating a Range class, since it's identical under the hood to a Number: it just changes the From a usage standpoint, we have another issue with these input types: where to store the sizing info. Let's consider prefs. If we want to expose a numerical input field for, let's say SMTP Port, the value can be (ostensibly) from 1 to 65535. So it makes sense to have a number input with min=1 and max=65535. Same could be said for any numeric inputs we have that are currently using text_input (logs_expire_days, max_url_length, etc). The problem is that in the txp_prefs table we have no way of specifying these constraints. We have columns for the pref name, its value, its group ('event') and input type ( We could really do with some additional database fields for specifying things like min, max, step, pattern, size and so forth. But not all of them are applicable to all field types so we'll probably need to make some design/storage compromises. One obvious solution is to introduce a new Not sure what 'size' would mean for YesNoRadio or OnOffRadio prefs. Guess we just have to take the hit on the wasted storage space. Multi-select lists could use 'size' to dictate how many items to display in the list before a scrollbar is shown, perhaps. Textareas could specify Do we go further and introduce a That'd be a boon for us (and links to issue #1600 where we wish to validate prefs before saving them to avoid bad values being stored). And it's a boon for plugin authors who want to store prefs as they can employ the same validation rules and have core do the hard work of enforcing their rules by comparing stored values to the pattern and kicking up a fuss if it doesn't match. I'm just not sure if introducing a column specifically for 'pattern' is too restrictive or a waste of DB storage space. For numeric types, the min/max/step is actually a storage constraint as well as a pattern-matching constraint AND a browser validation constraint, even though the actual HTML input controls don't support Since we don't really need both Alternatively - and very very dirtily - we could let the
That's not very clean and purists would run a mile at the abuse of data storage "one field = one value" conventions, but it does offer us superior flexibility to handle any and all size/validation opportunities on both browser and server sides in a single compact field. I'm torn. What do y'all think? Paging @bloatware @philwareham @petecooper @jools-r for comment. |
I'm afraid I won't be much help with this, given the paged audience I will respectfully defer to others. |
No probs. Thanks for looking anyway. It's one of those conundrums of do we do it "right" (which involves a lot of tables and joins and slowdown) or do we do it "wrong" (which is way more flexible but yucky and less intuitive for plugin authors) or do we leave it in code (which is harder to maintain and not as useful to us or plugin authors) or some hybrid of the above/something else. None of them are attractive, but since we're building a UI library that is, by its nature, going to be the cornerstone of building our interfaces, it pays to do it in the best way for the project to make the leanest code and least hassle for people using it, regardless of rightness and wrongness. |
I’m for the way that benefits us best, so if two key values (or more) were in the constraints field and that gave us maximum flexibility without hard coding then that seems ok to me. |
Yeah, I'm leaning that way as it does mean we can extend this in really neat ways in future to help our UI features. I just fear the rolleyes from the 4NF crowd who stare agape at our database structure and storm the castle with searing pitchforks for crimes against database design. |
I'd report it here: setting a cf render to
|
Another issue: |
Fixed the first one. Note to self: TagCollections don't have directly addressable IDs |
Surely this can just be changed without any b/c issues anywhere we render such a widget? EDIT: Where do we render these in core? I can't find one... EDIT 2: Or do we just have to add datetime-local to the supported types here:
|
This might fix it: 0e9e37d |
Ah balls yeah, the UI library short circuits it by using |
Hmm, not very clean but since dateTime is what we refer to a date-time widget internally, I've fudged it in the renderer. Is that any closer? EDIT: and yeah yeah, braces round the ternary test. Old habits die hard, sorry. Must get out of the habit of doing that but it's a holdover from when that type of construct could cause weird logic errors if the brackets weren't in place. The parser has improved now so they're not needed and I need to move with the times. |
This has nailed it, thank you. Should we also switch to |
Sorry @Bloke, when creating a date/time cf I get this:
|
PHP 8.2.11 / MySQL 8, on a fresh CF-branch install
–^– And while playing, I noticed that several CF input fields have an invalid |
It's high time to phase out most/all of
txplib_forms.php
and parts oftxplib_html.php
. To achieve this, we should build an extensible OO user interface library using inheritance and interfaces.Benefits:
fInput()
with 12 params!) in favour of clean constructors with setters.data-
attributes.Downsides:
So far:
eInput()
,sInput()
,tInput()
<input type="checkbox" />
tag<input type="checkbox" />
tag inline set with labelswrapGroup
andwrapRegion
<form />
tag<input />
tag<input />
tag set. Primarily of use for creating a series of hidden inputs<label />
tag<input type="radio" />
tag set with on/off options<optgroup />
tag and collection of options<option />
tag<input type="radio" />
tag<input type="radio" />
tag inline set with labels<script />
tag<select />
list tag<select />
list tag built from a tree-based record set<textarea />
tag<input />
tag containing a CSRF token<input type="radio" />
tag set with yes/no optionsThe text was updated successfully, but these errors were encountered: